質問

I've read some of article about the practice that making Square an inheritance class of Rectangle class is a bad practice, saying it violate the LSP (Liskov substitution principle). I still don't get it, I made a example code in Ruby:

class Rectangle
    attr_accessor :width, :height
    def initialize(width, height)
        @width = width
        @height = height
    end
end

class Square < Rectangle
    def initialize(length)
        super(length, length)
    end
    def width=(number)
        super(number)
        @height = number
    end

    def height=(number)
        super(number)
        @width = number
    end
end


s = Square.new(100)

s.width = 50

puts s.height

Could anyone tell me what's wrong with it?

役に立ちましたか?

解決

I'm not always keen on Liskov since it seems to limit what you can do with inheritance based on behaviour rather than "essence". In my view, inheritance was always meant to be an "is a" relationship, not an "acts exactly like" one.

Having said that, the wikipedia article goes into detail as to why this is considered bad by some, using your exact example:

A typical example that violates LSP is a Square class that derives from a Rectangle class, assuming getter and setter methods exist for both width and height.

The Square class always assumes that the width is equal with the height. If a Square object is used in a context where a Rectangle is expected, unexpected behavior may occur because the dimensions of a Square cannot (or rather should not) be modified independently.

This problem cannot be easily fixed: if we can modify the setter methods in the Square class so that they preserve the Square invariant (i.e., keep the dimensions equal), then these methods will weaken (violate) the postconditions for the Rectangle setters, which state that dimensions can be modified independently.

So, looking at your code alongside the equivalent Rectangle code:

s = Square.new(100)            r = Rectangle.new(100,100)
s.width = 50                   r.width = 50
puts s.height                  puts r.height

the output would be 50 on the left and 100 on the right.

But, this is the important bit from the article, in my view:

Violations of LSP, like this one, may or may not be a problem in practice, depending on the postconditions or invariants that are actually expected by the code that uses classes violating LSP.

In other words, provided the code using the classes understands the behaviour, there is no issue.

Bottom line, a square is a proper subset of a rectangle, for a loose-enough definition of rectangle :-)

他のヒント

What's wrong with it from an Liskov substitution principle (LSP) perspective is that your Rectangles and Squares are mutable. That means that you have to explicitly reimplement the setters in the subclass, and lose the benefits of inheritance. If you make Rectangles immutable, i.e., if you want a different Rectangle you create a new one rather than altering the measurements of an existing one, then there's no problem with violating LSP.

class Rectangle
  attr_reader :width, :height

  def initialize(width, height)
    @width = width
    @height = height
  end

  def area
    @width * @height
  end
end

class Square < Rectangle
  def initialize(length)
    super(length, length)
  end
end

Using attr_reader gives getters but not setters, hence the immutability. With this implementation both Rectangles and Squares provide visibility to height and width, for a square those will always be the same, and the concept of area is consistent.

Consider the abstract base class or interface (whether something is an interface or abstract class is an implementation detail rather irrelevant to the LSP) ReadableRectangle; it has read-only properties Width and Height. It would be possible to derive from that a type ReadableSquare, which has the same properties but contractually guarantees that Width and Height will always be equal.

From ReadableRectangle, one could define concrete type ImmutableRectangle (which takes a height and width in its constructor, and guarantees that the Height and Width properties will always return the same values), and MutableRectangle. One could also define concrete type MutableRectangle, which allows the height and width to be set at any time.

On the "square" side of things, an ImmutableSquare should be substitutable for both an ImmutableRectangle and a ReadableSquare. A MutableSquare, however, is only substitutable for a ReadableSquare [which is in turn substitutable for a ReadableRectangle.] Further, while the behavior of an ImmutableSquare is substitutable for an ImmutableRectangle, the value gained by inheriting a concrete ImmutableRectangle type would be limited. If ImmutableRectangle were an abstract type or interface, the ImmutableSquare class would only need to use one field rather than two to hold its dimensions (for a class with two fields, saving one is no big deal, but it's not hard to imagine classes with a lot more fields, where the savings could be significant). If, however, ImmutableRectangle is a concrete type, then any derived type would have to have all the fields of its base.

Some types of square are substitutable for the corresponding types of rectangles, but a mutable square is not substitutable for a mutable rectangle.

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top