Practice · SOLID · LSP · Card 1
Why does this subclass break code that worked with the parent?
A Rectangle. A Square that inherits from it. A test that passes for Rectangle and fails for Square, using only public methods. Why?
The code
A textbook inheritance setup. The Square enforces its own invariant: width == height.
class Rectangle
attr_accessor :width, :height
def area
width * height
end
end
class Square < Rectangle
def width=(value)
super(value)
@height = value
end
def height=(value)
@width = value
super(value)
end
end
# This test passes for Rectangle, fails for Square:
def test_resize(shape)
shape.width = 3
shape.height = 5
assert_equal 15, shape.area
end The question
Why does the test fail for Square? And what does that tell you about the Square-extends-Rectangle relationship?
Take a moment. The test only uses methods that both classes have. Yet it fails for the subclass. What contract did Square break by overriding the setters?
What's broken
When you call square.width = 3, Square also sets height to 3. Then square.height = 5 sets both to 5. square.area returns 25, not 15.
The Rectangle class has an implicit contract that callers depend on: setting width does not change height, and vice versa. Square overrode the setters in a way that breaks that contract. Any caller that was written assuming Rectangle behavior now misbehaves when handed a Square. The substitution failed.
"Liskov Substitution" is named after this: if S is a subtype of T, you should be able to substitute S anywhere T was expected without breaking the program. Square fails the substitution because its setters mutate state Rectangle\'s setters never touched.
The real lesson
"A square is a rectangle" is true in geometry and false in software. In geometry, a square is just a rectangle with equal sides. In software, the type system encodes behavior, and the behavior of Square (its setters are coupled) is fundamentally different from the behavior of Rectangle (its setters are independent).
The fix isn\'t to find a cleverer way to override setters. The fix is to recognize that Square is not really-a-Rectangle. Two healthier shapes:
- Independent classes. Both implement an
areamethod via duck typing. No inheritance. - A Shape interface (module). Rectangle and Square each include it, each provide their own setters. No "is-a" relationship between them.
The Ruby-flavored version of this lesson is duck typing over inheritance. If two things behave alike in some methods, give them those methods. You don\'t need a class hierarchy to express it.
Theory
For the full walkthrough on duck typing as Ruby's LSP, read SOLID · LSP · Duck Typing and LSP · Contract Breaking.