the caller Β· fixed code
s.setWidth(5);
s.setHeight(4);
// expect area == 20
pass in a substitute
what the bench sees
width Γ— height
β€” pick a substitute
The substitution test
The caller code never changes. Pass a Rectangle and it should hold its promise β€” pass a Square and watch what happens to the width you set.
β–Έ Watch the box animate when you pass a Square: setHeight(4) quietly drags the width down too.
the caller Β· fixed code
bird.fly();
// expect it takes off
pass in a substitute
what the bench sees
🐦
a Bird, ready to fly()
β€” pick a substitute
The substitution test
The base Bird promises fly() works. A Sparrow keeps that promise. A Penguin can't fly β€” so what does its fly() do?
β–Έ A subtype that overrides a method just to throw is a degenerate override β€” a loud LSP smell.
the corrected design β€” every substitute passes
We stop forcing bad extends trees. Shapes share a tiny immutable Shape interface (just area(), no setters to break). Birds split into a plain Bird and a separate FlyingBird capability. Click any substitute below β€” they all keep their promises now.
βœ“ Substitutability restored
Pick a substitute above to see why it now passes the bench with no surprises.
β–Έ The rule: when a subtype can't keep the base's full contract, prefer a shared interface or composition over inheritance.