Substitution in 3D
The glowing pedestal accepts any shape. Press Swap In to replace the parent with a subtype. Press Violation to see what happens when the contract breaks.
Code example
The classic Rectangle / Square trap and how to fix it. Toggle between C# and JavaScript.
// ? BAD — Square breaks Rectangle's area contract
public class Rectangle
{
public virtual int Width { get; set; }
public virtual int Height { get; set; }
public int Area() => Width * Height;
}
public class Square : Rectangle // LSP violation!
{
public override int Width { set { base.Width = base.Height = value; } }
public override int Height { set { base.Height = base.Width = value; } }
}
// This breaks when Square is substituted for Rectangle:
Rectangle r = new Square();
r.Width = 4; r.Height = 5;
// Expected: 4—5=20. Actual: 5—5=25. Contract violated!
// ? GOOD — both implement the same interface independently
public interface IShape
{
int Area();
}
public class Rectangle : IShape
{
public int Width { get; }
public int Height { get; }
public Rectangle(int w, int h) { Width = w; Height = h; }
public int Area() => Width * Height; // w — h
}
public class Square : IShape
{
public int Side { get; }
public Square(int s) { Side = s; }
public int Area() => Side * Side; // s—
}
// ? BAD — Square inherits Rectangle and breaks area
class Rectangle {
constructor(w, h) { this.width = w; this.height = h; }
setWidth(w) { this.width = w; }
setHeight(h) { this.height = h; }
area() { return this.width * this.height; }
}
class Square extends Rectangle { // LSP violation!
setWidth(s) { this.width = this.height = s; }
setHeight(s) { this.width = this.height = s; }
}
const r = new Square(0, 0);
r.setWidth(4); r.setHeight(5);
// Expected: 4—5=20. Actual: 5—5=25 ? contract broken!
// ? GOOD — separate classes, common interface (duck typing)
class Rectangle {
constructor(w, h) { this.width = w; this.height = h; }
area() { return this.width * this.height; }
}
class Square {
constructor(s) { this.side = s; }
area() { return this.side ** 2; }
}
// Both satisfy the IShape contract: .area() works correctly
function printArea(shape) { console.log(shape.area()); }
printArea(new Rectangle(4, 5)); // 20
printArea(new Square(5)); // 25
Key concepts
Substitutability
Wherever a base type is expected, a subtype can be used without the caller needing to know — or care — which concrete type it is.
Contract
A class has a contract: preconditions, postconditions, and invariants. Subtypes must honour all of them, not just the method signatures.
The Square Problem
Mathematically a Square is a Rectangle. But in code, Square overriding Rectangle's setters breaks the width—height invariant. Inheritance ? is-a.
Behavioural Subtyping
LSP is about behaviour, not syntax. A subtype that compiles but changes expected outputs is just as much a violation as a type error.