CodeOath
← All posts
Architecture & Patterns80 min total · 26 parts

ACID, SOLID, and Common Design Patterns: A Software Design Reference

Contents — Part 9 of 26: Liskov Substitution Principle
Part 9 of 26 · ~2 min

Liskov Substitution Principle

A subtype must be usable anywhere its base type is expected, without the caller needing to know or care which one it actually got. This is a stricter requirement than "compiles and doesn't throw" — it means the subtype can't narrow preconditions, widen postconditions, or violate any behavioral assumption a caller reasonably makes about the base type.

The textbook violation is Square inheriting from Rectangle:

public class Rectangle
{
    public virtual int Width { get; set; }
    public virtual int Height { get; set; }
    public int Area => Width * Height;
}

// Looks reasonable in isolation — a square IS-A rectangle, mathematically.
public class Square : Rectangle
{
    public override int Width
    {
        get => base.Width;
        set { base.Width = value; base.Height = value; } // keeps it square
    }
    public override int Height
    {
        get => base.Height;
        set { base.Width = value; base.Height = value; }
    }
}

// This function is completely reasonable against the Rectangle contract...
void ResizeAndCheck(Rectangle r)
{
    r.Width = 5;
    r.Height = 4;
    Debug.Assert(r.Area == 20); // passes for any real Rectangle...
    // ...but FAILS for a Square, because setting Height silently changed
    // Width too. The caller's reasonable assumption about Rectangle broke.
}

Square compiles, technically "is" a Rectangle, and still breaks every caller that assumes setting one dimension doesn't affect the other. The fix is usually to recognize that the inheritance relationship was wrong in the first place — Square and Rectangle should not be in an inheritance relationship at all if their contracts genuinely differ this much; a shared IShape interface with just an Area property, implemented independently by each, avoids the trap entirely.

public interface IShape
{
    int Area { get; }
}

public class Rectangle : IShape
{
    public int Width { get; set; }
    public int Height { get; set; }
    public int Area => Width * Height;
}

public class Square : IShape
{
    public int Side { get; set; }
    public int Area => Side * Side;
}