Liskov Substitution Principle
Code written against a base type must keep working when it is handed any subtype. A subtype may promise more, but never less.
Overview
The Liskov Substitution Principle (LSP) is the L in SOLID, and the only one named after its author. Barbara Liskov stated it in a 1987 OOPSLA keynote, Data Abstraction and Hierarchy: if for every object of type S there is an object of type T such that any program written for T behaves the same when given the S object, then S is a subtype of T. Liskov and Jeannette Wing made it precise in 1994 as the subtype requirement: any property provable about objects of the supertype should also hold for objects of the subtype.
The 1996 paraphrase by Robert C. Martin is the one interviewers expect: functions that use references to base classes must be able to use objects of derived classes without knowing it. The word that matters is behavior. A subclass that compiles is not automatically a subtype. It is a subtype only if callers of the base class cannot tell the difference.
- Substitution property: programs written for T behave the same with S — Liskov, Data Abstraction and Hierarchy — 1987
- Subtype requirement: properties provable for T hold for S — Liskov and Wing, A Behavioral Notion of Subtyping — 1994
- Functions using base-class references work with derived classes without knowing it — Martin, C++ Report — 1996
Rectangle and Square
A square is a rectangle in geometry, so it is tempting to write class Square(Rectangle). A square has to keep its sides equal, so setting the width also sets the height. That breaks code written for rectangles, which expects the two sides to change independently.
stretch is correct for every Rectangle and wrong for every Square. Martin's summary: a square may be a rectangle, but a Square object is not a Rectangle object, because its behavior is different. In object-oriented design the is-a relationship is about behavior clients can observe, not about geometry.
The rules
Liskov and Wing's definition gives a checklist. Each rule says what a subtype method may change compared to the supertype method it replaces.
- Signature: arguments — accept a wider type (contravariance) — demand a narrower type
- Signature: result — return a narrower type (covariance) — return a wider type
- Signature: exceptions — throw a subset of the supertype's exceptions — throw new ones
- Precondition — require less of the caller — require more
- Postcondition — promise more — promise less
- Invariant — keep every invariant of the supertype — break one
- History (constraint) — add state changes the supertype already allows — allow state changes the supertype rules out
The history rule is the one people forget. It says a subtype cannot introduce a way of changing state that the supertype forbids. Liskov and Wing's example is a mutable type presented as a subtype of an immutable one: a caller holding the immutable view can suddenly see the value change.
Smells
- An override that throws unsupported — a read-only list whose add throws — the caller's precondition was met, the postcondition is not
- An override that silently does nothing — a logger subclass that drops messages — postcondition weakened
- Type checks in client code — if isinstance(shape, Square) — the client no longer trusts the base contract
- Tighter input rules — base accepts any amount, subclass rejects amounts over 1,000 — precondition strengthened
- A mutable subclass of an immutable class — a MutablePoint under Point whose set_x shows through Point.x() — history rule
The fix
When a subtype cannot honor the contract, do not force the inheritance. Give both classes a smaller shared abstraction that they can both honor, or make the shared type immutable so the setters that caused the trouble do not exist.
The shared contract is now area(), which both honor. Nothing promises independent sides, so nothing can break that promise.
What you will be able to do
- State the principle in Liskov's terms and in Martin's, and say why behavior is the point.
- Walk through Rectangle and Square and show the exact call that breaks.
- Check an override against the precondition, postcondition, invariant and history rules.
- Fix a bad hierarchy by shrinking the shared contract or removing mutation.
Interview questions
- What is the Square-Rectangle problem and how do you fix it?
- What are the formal rules of the Liskov Substitution Principle?
- How do you test for LSP violations?
- Is a read-only collection that throws on add() an LSP violation?
- How does LSP relate to the Open/Closed Principle?
Sign up free to read the full topic
More Low-Level Design topics
Start free on Cariara · Coding problems · System design studio