Clean Architecture: A Craftsman's Guide

Chapter 8: The Dependency Rule

Chapter 7

The Liskov Substitution Principle

Page 183 of 432 • 18 min read

Barbara Liskov wrote the original definition of the Liskov Substitution Principle in 1988, though it wasn't called that at the time. It is as follows:

"If for each object o1 of type S there is an object o2 of type T, such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2, then S is a subtype of T."

Let's understand this principle through a practical example. Consider a program that works with rectangles. In a mathematical sense, a square is a rectangle. It has four sides and four right angles. But in software design, inheriting Square from Rectangle can lead to subtle bugs.

The Rectangle Problem

Imagine we have a Rectangle class with width and height properties. We write a function that calculates the area of a rectangle:

function calculateArea(r: Rectangle): number {
    return r.getWidth() * r.getHeight();
}

Now suppose we create a Square class that inherits from Rectangle. A square, by definition, has equal width and height. So we might override the setters to maintain this invariant:

If we pass a Square object to the calculateArea function, it will work correctly. But what if we have code that sets width and height independently?

Your note

This is where inheritance breaks down. The Square cannot be substituted for Rectangle in all contexts, violating LSP.

Solution: Composition Over Inheritance

Rather than using inheritance, we should use composition. Both Rectangle and Square can implement a common Shape interface, but they don't inherit from each other:

interface Shape {
    area(): number;
}

class Rectangle implements Shape {
    constructor(
        private width: number,
        private height: number
    ) {}
    
    area(): number {
        return this.width * this.height;
    }
}

class Square implements Shape {
    constructor(private side: number) {}
    
    area(): number {
        return this.side * this.side;
    }
}

This approach respects the Liskov Substitution Principle because Square is no longer claiming to be a Rectangle. They are both independent implementations of Shape, and either can be substituted wherever a Shape is expected.

Real-World Applications

In large software systems, violating LSP often leads to:

  • Fragile base class problems
  • Unexpected behavior in polymorphic code
  • Tight coupling between parent and child classes
  • Difficulties in testing and maintenance

The key insight is that inheritance should be used only when the child class truly is a subtype of the parent in all contexts, not just in name or structure.

Chapter 7 of 34 • 42% complete

Action completed