Liskov Substitution Principle

Topics Covered

What Is Liskov Substitution

The Cost of Violating LSP

LSP vs Type Checking

The Rectangle-Square Problem

The Violation

The Fix

Practice Problems

Behavioral Contracts and Preconditions

Preconditions

Postconditions

Invariants

The Memory Aid

LSP in Practice

Common LSP Violations

The Substitution Test

The Real-World IS-A Trap

Designing LSP-Compliant Hierarchies

Every SOLID principle protects your codebase from a specific category of damage. The Liskov Substitution Principle (LSP) guards against the most insidious kind: code that compiles, passes type checks, and looks correct, but breaks at runtime because a subtype does not actually behave like its parent.

Barbara Liskov formulated the principle in 1987: If S is a subtype of T, then objects of type T can be replaced with objects of type S without altering the correctness of the program. The key word is "correctness" -- not "compiles" and not "runs without crashing." It means the program produces the same observable behavior. Every postcondition still holds. Every invariant remains intact. Every caller's assumptions stay valid.

Why does this matter? Because polymorphism is the backbone of object-oriented design. You write a function that accepts a Shape and calls area(). You pass it a Circle, a Triangle, a Polygon. The function does not know or care which concrete type it received. This only works if every subtype honors the contract of Shape. The moment one subtype silently changes what area() means -- returning perimeter instead, throwing an exception, or producing a negative number -- every function that depends on Shape becomes unreliable.

Square substituting for Rectangle and breaking client code expectations

The Cost of Violating LSP

LSP violations do not announce themselves. They hide behind green test suites because developers test each subtype in isolation rather than through the parent type's interface. The bug surfaces when someone writes generic code -- a function that processes a list of Shape objects, a handler that accepts any PaymentMethod, a pipeline that works with any DataSource. That generic code was written against the parent's contract. When a subtype violates the contract, the generic code fails in ways the author never anticipated.

The debugging cost is high because the failure is far from the cause. The function that crashes is correct. The subtype that violates the contract is in a different file, written by a different developer, possibly months earlier. The connection between them is invisible unless you understand LSP.

Key Insight

LSP is not about inheritance mechanics -- it is about behavioral compatibility. A subtype can add new methods, new fields, and new capabilities. What it must not do is change the meaning of existing methods. The parent type makes promises to its callers. Every subtype must keep those promises.

LSP vs Type Checking

Modern type systems catch many errors at compile time. If Shape declares area() -> float, the compiler ensures every subtype returns a float. But LSP goes beyond types. The compiler cannot verify that area() returns a positive number, that withdraw() never puts a bank account below zero, or that sort() actually produces a sorted list. These are behavioral contracts -- promises about what methods do, not just what types they return. LSP demands that subtypes honor these behavioral contracts, not just the type signatures.