Liskov substitution principle
When extending a class, you should be able to pass objects of the subclass wherever the parent was expected, without breaking the client code.
Most design principles are open to interpretation and taste. This one comes with a checklist, which makes it the easiest of the five to actually verify - and the one whose violations hurt most, because they only show up at runtime, in somebody else's code.
That "somebody else" is the point. The principle matters most when you are writing libraries and frameworks, where your subclasses end up inside programs you cannot see, let alone fix.
Formal statement
Barbara Liskov's original formulation is stated in terms of a proof obligation: let q(x) be
a property provable about objects x of type T. Then q(y) should be provable for objects
y of type S, where S is a subtype of T. Translated out of proof-theory language, a
subtype has to honor every contract the base type made, phrased as three families of
condition:
- Preconditions cannot be strengthened. If the base method accepts any
int, the override cannot start rejecting negatives - every caller who has been passing negatives for two years is now broken by code they never touched. - Postconditions cannot be weakened. If the base method guarantees the connection is closed when it returns, the override cannot leave it open "for performance," because the caller's cleanup logic was written trusting that guarantee.
- Invariants must be preserved. Whatever must always be true of an object for it to make
sense - a bird's ability to eat, a document's
filenamenever being empty - has to remain true across the entire hierarchy, not just at the base type.
Everything else people quote about Liskov - matching or widening parameter types, matching or narrowing return types, never throwing a new exception type - is a specific, checkable instance of one of these three conditions applied to a statically typed method signature.
The checklist
A subclass stays substitutable when its overrides obey all of these:
- Parameter types match or get more abstract. The base declares
feed(Cat c). Overriding it asfeed(Animal c)is fine, because callers still pass cats and cats are animals. Overriding it asfeed(BengalCat c)is not, because the client's ordinary cat no longer fits. - Return types match or get more specific. The inverse rule.
buyCat(): Catmay be narrowed tobuyCat(): BengalCat, because the caller wanted a cat and got one. Widening it tobuyCat(): Animalbreaks code that was built around cat-shaped results. In dynamically typed languages the equivalent sin is returning a number where the base returned a string. - No new exception types. Client code catches what the base method is documented to
throw. An override that raises something unrelated slips straight past the
try/catchand takes the process with it. - No stronger pre-conditions. If the base accepts any
intand your override rejects negatives, code that has been passing negative values for two years starts throwing. - No weaker post-conditions. If the base method always closed its database connections, your connection-reusing override leaves callers - who terminate right after the call because they trusted the contract - leaking connections into the void.
- Invariants are preserved. The conditions under which an object makes sense at all. Cats have four legs and a tail; your subclass does not get to remove one. This is the rule most often broken by accident, because invariants live partly in interface contracts, partly in assertions, and partly in unit tests nobody reread.
- Private state stays private. Reflection, and languages with no real access control, make it possible to reach into a superclass's private fields. Possible is not permission.
Statically typed languages such as Java and C# enforce the first three at compile time, which means the interesting failures are almost always in the last four.
The example
A Document class can be opened and saved. Somebody adds ReadOnlyDocument, and since
saving makes no sense there, the override throws. Reasonable-looking, and wrong: the client
code now has to check the concrete type before saving anything, which drags it into knowing
about document subclasses and quietly breaks the open/closed principle too. Add another
document type and the client changes again.
The fix is not a smarter override, it is a redesigned hierarchy. A subclass should add to the base behavior, so make the read-only document the base class and let a writable document extend it with the ability to save. Now the type system says exactly what is true, and the type check disappears.
A second example
The other canonical case: Bird declares fly(), Sparrow inherits it happily, and
Ostrich overrides it to throw, because ostriches cannot fly and the hierarchy never asked
their opinion. Aviary.releaseAll() now needs an instanceof check before calling fly()
on anything, which is exactly the symptom from the document example, in feathers.
The fix is the same shape too: flight was never a property of "being a bird" in the first
place, so it should never have been on Bird. Move it to a FlyingBird capability that only
the birds which actually fly implement, and Aviary iterates the list that is honest about
what it contains.
The smell
Two reliable tells, both visible without reading a single postcondition:
- A subclass overrides a method to throw
UnsupportedOperationException(or your language's equivalent). That override is not implementing the base behavior, it is refusing it - the base class promised something this subclass cannot deliver. - A caller does an
instanceof(oris not ReadOnlyDocument) check before calling a method the interface already declares. If polymorphism worked, that check would be unnecessary. Its presence means some concrete type in the hierarchy is not actually substitutable.
Patterns that lean on it
Template Method depends
entirely on every subclass honoring the base algorithm's contract - a subclass that skips a
step or reverses an invariant the template relies on breaks every caller of the template, not
just its own callers. The Null Object
pattern exists specifically to avoid the failure mode above: instead of a subclass throwing
or returning null where a real implementation was expected, a null object implements the same
interface with a harmless no-op, so every caller stays substitutable and nobody needs an
instanceof guard.
Where it goes wrong
There is no "overkill" version of this one in the way there is for the other principles - substitutability is not an optional flourish. What does go wrong is over-application:
- Not every awkward subclass needs the hierarchy inverted. Sometimes the honest answer is that these two things were never in an "is a" relationship, and composition dissolves the problem entirely.
- Do not contort a design to preserve an invariant nobody depends on. The safest way to extend a class is to add fields and methods and touch nothing existing, but "safest" is not always "possible", and a heroic hierarchy built to satisfy a rule literally can be worse than the violation.
- Beware the rectangle-and-square rabbit hole. Long arguments about whether a square is a rectangle are usually a sign that the model, not the principle, needs the attention.
The practical test costs nothing: take every place the superclass is used, imagine your subclass there instead, and ask whether anything gets surprised. The honest cost of chasing substitutability past the point it is needed is a hierarchy redesigned around a case that never actually occurs in the calling code - solving a problem the client base does not have.
Try it yourself: Rework the Document/ReadOnlyDocument hierarchy so ReadOnlyDocument
no longer overrides save() with a thrown exception, then rewrite Project.saveAll() so it
no longer needs a type check against ReadOnlyDocument before calling it.
Check yourself
A superclass method is feed(Cat c). Which override keeps the subclass substitutable?