The "trick" of the classic Square-Rectangle LSP problem is that if we define a rectangle as an object that must satisfy the contract...
rectangle.setWidth(40);
rectangle.setHeight(50);
assert(rectangle.getWidth() == 40);
...then extend it to a square, then we violate that contract. This is because we are mixing up a custom definition of a rectangle and a classical definition. As such, we are trying to make the classical definition fit into a custom definition, which doesn't work. In essence, it is a parlour trick intended to make you think about sub-type behaviour and contractual obligations. We could just as easily show that the above assertion of a Rectangle object fails for even a rectangle with some cunning rewording:
rectangle.setTopEdgeLength(40);
rectangle.setBottomEdgeLength(50);
assert(rectangle.getTopEdgeLength() == 40);
Anyhow, enough of the riddles, let's have a look at the...
Classical Definition
Correctly put, squares and rectangles are both types of quadrilaterals, a quadrilateral being a polygon with 4 edges and 4 vertices. Classically, a square is a rectangle, but let's have a look at the Wikipedia definitions of a rectangle and a square:
- Rectangle: A quadrilateral with four right angles
- Square: A quadrilateral with four right angles and four equal sides
Notice something there? The only commonalities between a rectangle and a square are that both are quadrilaterals, and both have four right angles. No mention at all of rules around the length of the sides, as these are derived from the contract, not part of the contract. We can demonstrate this with some simple logical statements:
- if a shape is a quadrilateral and the shape has four right angles, then opposite sides will be of equal length
- if a shape is quadrilateral and the shape has four right angles and four equal sides, then the length of any side will be equal to the length any other side (this one is actually a tautology)
If we want to maintain the classical definition of rectangles and squares and represent them in code with the setWidth()
and setHeight()
methods in place, then the contract needs to be:
square.setWidth(40);
square.setHeight(50);
assert(square.isQuadrilateral());
assert(square.hasFourRightAngles());
Whether the width is still what we set it in line 1 is irrelevant, as that rule does not form part of the contractual obligations of the parent object Rectangle.
To sum up, the initial problem is one of contractual definitions. I'm not saying you shouldn't have a Rectangle object that satisfies rectangle.setWidth(40); rectangle.setHeight(50); assert(rectangle.getWidth() == 40);
, but if you define your Rectangle object in that fashion, then when you extend it you need to ensure that the contract is still valid, otherwise you violate LSP.
setWidth(n); setHeight(m); assert(getWidth() == n);
), then it does violate LSP.