Skip to main content

Liskov Substitution: Subtype Contracts

What This Concept Is​

The Liskov Substitution Principle (LSP), due to Barbara Liskov, says:

If S is a subtype of T, then objects of type T may be replaced with objects of type S without altering the correctness of the program.

In practice: wherever code expects a T, you must be able to hand it an S and everything keeps working. The subtype must honor the supertype's behavioral contract, not only its type signature.

Begin with these three contract dimensions, then include allowed failures, state histories, aliasing and any promised resource bounds:

  • Preconditions: what the method requires of callers. Subtypes must not strengthen them.
  • Postconditions: what the method guarantees after running. Subtypes must not weaken them.
  • Invariants: what must always be true of an instance. Subtypes must preserve them.

Ordinary type checking usually establishes less than behavioral compatibility. Reasoning, contract checks and formal verification can add evidence; finite tests do not prove all future implementations correct.

Why It Matters Here​

An inheritance tree can compile while violating a behavioral contract. When they do, polymorphism stops being trustworthy: callers start adding instanceof checks, special-cases, and comments that begin with "unless this is a...". That is the moment OCP dies.

LSP is also the principle that most clearly reveals the difference between "A is a kind of B" in English and "A can substitute for B" in code.

Concrete Example​

A violation under a mutable independent-dimension contract (Java declaration sketch):

class Rectangle {
protected int width, height;
public void setWidth(int w) { this.width = w; }
public void setHeight(int h) { this.height = h; }
public int area() { return width * height; }
}

class Square extends Rectangle {
@Override public void setWidth(int w) { width = w; height = w; }
@Override public void setHeight(int h) { width = h; height = h; }
}

void expectDoubleWidth(Rectangle r) {
r.setWidth(10); r.setHeight(5);
assert r.area() == 50; // holds for Rectangle, fails for Square (area=25)
}

Rectangle promised that setWidth does not affect height. Square weakened that postcondition. Any caller that relies on Rectangle's contract can now be surprised by a Square.

A real-world analogue appears in the OOAD treatment: a Pixel is not a SolidRectangle, even if every pixel happens to be a degenerate rectangle.

Common Confusion / Misconception​

"If it compiles, LSP holds." The compiler checks method signatures. LSP checks behavior. A subclass can implement every method and still violate LSP by throwing where the parent did not, by returning null where the parent returned a list, or by demanding callers hold a lock the parent did not require.

A second misconception: "LSP is just about exceptions." Exceptions are one symptom. The deeper problem is subtypes lying about what they are. For a custom interface promising successful append, a read-only implementation violates that promise. Java List explicitly makes some mutations optional, so UnsupportedOperationException can satisfy its documented contract. Always read the actual supertype contract before declaring a violation.

How To Use It​

When drawing an inheritance relationship, run the substitution check explicitly:

  1. Write the supertype's behavioral contract in prose: preconditions, postconditions, invariants.
  2. For each subtype method, check whether it keeps its end of that contract.
  3. If the subtype wants to change the contract, it is not really a subtype. Prefer composition (Square has a side length, not is-a Rectangle).
  4. Type-specific branches are a signal to investigate; diagnostics or optional capabilities may justify them. Identify an actual valid client whose guaranteed behavior fails.

Check Yourself​

  1. Why is "Square extends Rectangle" an LSP violation even though every square is geometrically a rectangle?
  2. Why can subclasses weaken preconditions but not postconditions?
  3. How is LSP connected to the Open-Closed Principle?

Mini Drill or Application​

Pick an inheritance relationship in code you know. Do all four:

  1. Write the parent type's contract as three short lines: preconditions, postconditions, invariants.
  2. For each subclass, mark each line as keeps or breaks.
  3. For any breaks, decide whether inheritance was a modeling mistake.
  4. Propose a composition-based alternative for at least one case.

Read This Only If Stuck​

Engineering extension: Sequence contracts and history​

Single-call pre/postconditions are not the whole story for mutable objects. A read-only query must not silently reset an account after each call if clients rely on a stable history. Record legal operation sequences, failure-state preservation and aliases. Two implementations can expose the same behavior with different representations and costs. Try: withdraw 12 from balance 10, then query twice. Check: rejection leaves both observations at 10 and history unchanged; passing that case is evidence for the implemented types, not a proof about arbitrary future subclasses.

Use the design contracts and practice feedback after an independent attempt.