Liskov Substitution Principle (LSP)
Any subclass must honor its parent's behavioral contract β swapping one for the other should never break the caller.
Intent & Description
π― Intent
Subtypes must honor the full behavioral contract of their base type β not just the method signatures. A caller using the base type must never need to know or care which subtype is actually running.
π Context
The Square-extends-Rectangle trap: Square forces width == height on every setWidth/setHeight call. Any caller that independently sets width and height on a Rectangle is silently broken when handed a Square. The code compiles; the behavior is wrong.
π‘ Solution
If Square and Rectangle break substitutability, they don’t have an inheritance relationship β model them as separate subclasses of a shared Shape base. Subclasses should extend behavior without contradicting their parent’s contract.
Real-world Use Case
π TL;DR
Subtypes must honor their parent’s contract. If substituting a subclass breaks a caller β fix the hierarchy, not the caller.
Advantages
- Reliable polymorphism β callers never need defensive type checks
- Inheritance hierarchies are predictable and safe to extend
- Surfaces “is-a” misuse early β sometimes composition is the real answer
Disadvantages
- Limits how dramatically subclasses can diverge from the parent’s contract
- Requires careful upfront hierarchy design
- Forces honest modeling β not every code-reuse opportunity is a real “is-a” relationship
// Before: Violates LSP
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
setWidth(width) {
this.width = width;
}
setHeight(height) {
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Rectangle {
constructor(side) {
super(side, side);
}
setWidth(width) {
this.width = width;
this.height = width; // Breaks rectangle behavior
}
setHeight(height) {
this.width = height;
this.height = height; // Breaks rectangle behavior
}
}
// After: LSP applied
class Shape {
getArea() {
throw new Error('Must implement getArea');
}
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Shape {
constructor(side) {
super();
this.side = side;
}
getArea() {
return this.side ** 2;
}
}