Explain the classic Rectangle/Square problem: why does making Square a subclass of a mutable Rectangle violate the Liskov Substitution Principle, and how would you fix the design?
answer
- setWidth promises height unchanged
- Square must couple the setters
- Postcondition weakened → area 16 not 20
- Mutation creates the conflict, not maths
- Fix: immutable / compose / segregate interface
basics
~20 sA mutable Rectangle lets you set width and height independently. A Square must keep them equal, so its setters change both. Code that sets width 5, height 4 and expects area 20 gets 16 — the subclass broke a rule the base promised.
solid answer
~50 sMathematically a square *is a* rectangle, but that's about values, not about a mutable object's contract. A mutable `Rectangle` carries the invariant "width and height are independently settable" — expressed as the postcondition of `setWidth(w)`: afterwards `width == w` **and** `height` is unchanged. `Square` cannot honour that: to preserve its own invariant `width == height`, `setWidth` must also change the height. So it weakens the base postcondition, and any client doing `r.setWidth(5); r.setHeight(4); assert r.area() == 20` fails. The lesson: subtyping is determined by the *behavioural contract of the operations*, not by real-world taxonomy. Fixes: (1) make the types immutable — with no setters there is no conflicting postcondition, and a `Square` value can safely be a `Rectangle`; (2) drop inheritance and use composition, with a shared read-only `Shape`/`area()` abstraction; (3) push mutators down so only `Rectangle` has independent setters; (4) model resizing as returning a new object (`withWidth(w): Rectangle`) rather than mutating.
code
pseudocode · 13 lines// Contract of the base type (the part people forget)
Rectangle.setWidth(w): post: width == w AND height unchanged
Square.setWidth(w): width = w; height = w // height CHANGED -> contract broken
// Client written against Rectangle only
stretch(r):
r.setWidth(5); r.setHeight(4)
assert r.area() == 20 // 16 when r is a Square
// Fix: immutable values, no conflicting postcondition
Rectangle(w, h) { area() = w*h; withWidth(w2) = Rectangle(w2, h) }
square(s) = Rectangle(s, s)go deeper
Show the concrete failing sequence: setWidth(5), setHeight(4), expected area 20, got 16 — and say the subclass broke a promise the base made.
Name the violated rule precisely: setWidth's postcondition included "height unchanged", and Square weakens it. Offer immutability or composition as the fix.
Generalise: subtyping follows operation contracts, not taxonomy; mutability is what creates the conflict. Link to Interface Segregation (an over-broad base forces violations) and propose contract tests plus immutable value modelling.
Treat it as a contract-design question that recurs at API/schema scale — silently coupling previously independent fields in a new version breaks consumers identically. Discuss deliberately choosing contract breadth, sealing hierarchies, and enforcing substitutability with shared consumer-driven contract suites.
## The setup The example is the standard illustration of the Liskov Substitution Principle (LSP), which requires that objects of a subtype be usable anywhere the supertype is expected without breaking client code. Start with a mutable rectangle: ``` class Rectangle: setWidth(w) // afterwards: width == w, height unchanged setHeight(h) // afterwards: height == h, width unchanged area() -> width * height ``` The two comments are **postconditions**: what the method guarantees to the caller. Note the second half of each — *the other dimension is unchanged*. That is the part everyone forgets, and it's the part that gets violated. Now "a square is a rectangle", so: ``` class Square extends Rectangle: setWidth(w) -> width = w; height = w setHeight(h) -> height = h; width = h ``` The subclass has to do this, because a `Square` has its own **invariant**: `width == height` must hold before and after every public operation. ## Why it breaks A client written against `Rectangle`: ``` fun stretch(r: Rectangle) { r.setWidth(5) r.setHeight(4) assert r.area() == 20 // guaranteed by Rectangle's contract } ``` Pass a `Square`: `setWidth(5)` makes it 5×5, `setHeight(4)` makes it 4×4, area is 16. The assertion fails. The client did nothing wrong — it used only the documented contract of `Rectangle`. Substitution altered the correctness of the program, so LSP is violated. In contract terms: - **Postcondition weakened**: `Rectangle.setWidth` promised height unchanged; `Square.setWidth` doesn't. - **Invariant broken**: the base type's invariant "the two dimensions vary independently" is destroyed. (Equivalently, the subtype *adds* an invariant `w == h` that the base's clients never agreed to.) ## The real lesson The common wrong takeaway is "inheritance is bad" or "maths is wrong". The correct takeaway is: > **Subtype relationships are defined by the behaviour of the operations on the type, not by real-world "is-a" taxonomy.** A square *value* really is a rectangle *value*. A `Square` **mutable object** is not a `Rectangle` **mutable object**, because the operation set differs: rectangles support independent resizing, squares don't. Mutation is what introduces the conflict. This generalises: `Ostrich extends Bird` breaks if `Bird.fly()` exists; `ReadOnlyList extends List` breaks if `List.add()` exists; `SavingsAccount extends Account` breaks if `Account.withdraw()` promises unconditional success. Taxonomy tempts you; contracts decide. ## Fixes, and their trade-offs **1. Make the types immutable.** No setters means no postcondition about "the other dimension unchanged". `Rectangle(w, h)` with only `area()`, `width`, `height` accessors; `Square(s)` can be a subtype safely, or simply a factory `Rectangle.square(s)`. Resizing becomes `withWidth(w): Rectangle` returning a *new* object — and note that on a `Square` it must return a plain `Rectangle`, which is legal because the return type can be widened conceptually (or the operation just lives on `Rectangle`). This is the cleanest fix and the one most modern designs take. **2. Composition instead of inheritance.** `Square` holds a `Rectangle` internally, exposes only `side` and `area()`, and never claims to be substitutable for `Rectangle`. Clients that need "anything with an area" use a small read-only `Shape { area(): Number }` interface. **3. Interface segregation.** Split into `Shape` (read-only: `area()`), `ResizableRectangle` (independent setters). `Square` implements `Shape`; nothing pretends squares resize independently. This is the LSP↔ISP connection: an over-broad interface *forces* subtypes into violations. **4. Widen the base contract deliberately.** You may define `Rectangle.setWidth` as "sets width; other dimensions may adjust to preserve implementation invariants". Then `Square` is compliant — but every client now has to defensively re-read the height, which pushes complexity onto callers and makes the base type nearly useless. Legal, rarely worth it. Still, it makes the key point: **compliance is relative to the contract you wrote**, so the real decision happens when the base type is designed. ## Edge cases and related traps - **The problem is not detectable by the compiler.** `Square` overrides with identical signatures; type systems are silent. Only tests or reasoning catch it. - **Contract tests catch it cheaply**: write the `stretch`-style assertions once against the `Rectangle` contract and run them against every implementation. `Square` fails immediately. - **Equality and hashing** get tangled too: an asymmetric `equals` between a `Square` and an equal-sided `Rectangle` is another substitutability failure. - **Serialization/cloning**: a `Square` deserialised as a `Rectangle` with mismatched sides has no valid state — evidence the hierarchy was wrong. - **Same shape in service APIs**: a v2 endpoint that silently couples two request fields that used to be independent is the exact same violation at system scale.
- If squares really are rectangles in mathematics, where exactly does the modelling go wrong?Mathematics talks about immutable *values*; the class talks about a mutable *object* with an operation set. `setWidth` is not part of the mathematical notion of a rectangle — it is an added capability whose contract squares cannot honour. Remove mutation and the mathematical relation holds again.
- Would making Rectangle final / sealed have prevented the problem?It prevents this particular subclass, but it isn't the principle. The underlying rule is to decide the base type's contract first: if independent resizing is promised, no coupled-dimension type may subtype it, sealed or not. Sealing is a mechanism; contract design is the reasoning.
- How do you catch this class of bug automatically?Contract (a.k.a. abstract) tests: one parameterised suite asserting the supertype's documented behaviour, executed against every implementation registered in the codebase. Any new subtype that breaks substitutability fails the shared suite rather than a distant integration test.
A dimmer switch and a light switch both "turn the light on", but a dimmer promises you can set brightness independently of on/off. Wiring a plain toggle in behind the dimmer's faceplate satisfies the shape of the socket and breaks every plan the room's lighting scene made.
saying these in an interview costs you the question
- "A square is a rectangle in maths, so the inheritance is correct" — LSP is about the operations' contracts, not taxonomy
- "Just override area() to fix it" — area was never the problem; the coupled setters are
- "Have the client check `if (r is Square)`" — type checks in clients are the symptom of the violation, not a fix
- "Make Square.setHeight throw an exception" — replaces a wrong result with a broken promise; still an LSP violation
- "This proves inheritance should never be used" — overcorrection; inheritance is fine when the subtype honours the contract