class Rectangle {
constructor(protected w: number, protected h: number) {}
setWidth(w: number) { this.w = w; }
setHeight(h: number) { this.h = h; }
area() { return this.w * this.h; }
}
class Square extends Rectangle {
setWidth(w: number) { this.w = w; this.h = w; }
setHeight(h: number) { this.w = h; this.h = h; }
}
A square breaks the promise that a rectangle's setWidth makes, so callers get wrong results. LSP (the Liskov Substitution Principle) says that code written for a base type must keep working when you give it any subtype. Subtyping is about behaviour, not just matching method signatures.
A mutable (changeable) Rectangle promises that setWidth changes only the width. A caller may write r.setWidth(5); r.setHeight(4); and expect area() to be 20. With a Square, setting the height also changes the width, so the area is 16.
Square compiles and has the same methods as the class. But it breaks the postcondition of setWidth. A postcondition is what a method promises will be true after it runs. So Square is not substitutable, even though a square is a rectangle in geometry.
The formal rules. A subtype:
- cannot strengthen preconditions (it cannot demand more from its inputs)
- cannot weaken postconditions (it cannot promise less about its results)
- must keep the base type's invariants (the facts that are always true about the object)
Fixes:
- make the shapes immutable (they cannot change after creation)
- or have both implement a
Shapeinterface witharea(), instead ofSquareinheriting fromRectangle
- Subtypes must be usable in place of the base type without breaking callers.
- Square breaks the postcondition that setWidth leaves the height unchanged.
- A subtype must not strengthen preconditions or weaken postconditions, and must keep the invariants.
- Fix it with immutability, or with a common interface instead of inheritance.