Design Patterns, Explained Simply · Foundations: Thinking in Patterns

SOLID, Part 2: Liskov, Interface Segregation, Dependency Inversion

The L, I and D of SOLID: subclasses must keep their parent's promises, interfaces should stay small, and high-level code should depend on abstractions.

The problem

Three bugs from one codebase:

  1. Square extends Rectangle. Code that sets width 5 and height 4 expects area 20, but on a square it gets 16, because setting the height also changed the width.
  2. A Printer interface has print(), scan() and fax(). A cheap printer must implement fax() by throwing an exception, and a report job crashes when it calls the wrong method.
  3. OrderService creates a MySqlDatabase inside itself. To unit test it you need a real MySQL, and switching to Postgres means editing business logic.

Real-world analogy

  • Liskov: if a rental company promises "any car in this class", a car where the brake pedal accelerates breaks the promise, even if it is technically a car.
  • Interface segregation: a TV remote with 60 buttons, most of which do nothing for your TV, is worse than a small remote with the buttons you need.
  • Dependency inversion: a lamp plugs into a standard socket. Neither the lamp nor the power station knows about the other, only about the socket standard.

The idea

  • L: Liskov Substitution Principle (LSP). Anywhere code uses a parent type, a subtype must work without surprises: same promises, no stricter inputs, no weaker results, no new exceptions. "Is-a" in real life (a square is a rectangle) does not guarantee a correct subtype in code.
  • I: Interface Segregation Principle (ISP). Clients shouldn't depend on methods they don't use. Prefer several small, focused interfaces (Printer, Scanner) to one large one.
  • D: Dependency Inversion Principle (DIP). High-level policy (OrderService) and low-level details (MySqlDatabase) both depend on an abstraction (OrderStore), and the abstraction belongs to the high-level side. Details are passed in, not created inside.

The code

The demo shows the Liskov violation, then the fix: shapes that don't pretend to share mutable width and height. It finishes with an order service that depends on a small storage abstraction, so an in-memory store can stand in for the database.

A broken subtype, then small abstractionsTry it yourself
class Rectangle:
    def __init__(self, w, h):
        self.w, self.h = w, h

    def set_height(self, h):
        self.h = h

    def area(self):
        return self.w * self.h


class Square(Rectangle):  # violates LSP: changes width when height is set
    def set_height(self, h):
        self.w = self.h = h


def stretch(rect):  # written against Rectangle's promise
    rect.set_height(4)
    return rect.area()


print("rectangle 5 wide ->", stretch(Rectangle(5, 2)))
print("square 5 ->", stretch(Square(5, 5)), "(caller expected 20)")


# Fix: immutable shapes that only share what is really common
class Rect:
    def __init__(self, w, h):
        self.w, self.h = w, h

    def area(self):
        return self.w * self.h


class Sq:
    def __init__(self, side):
        self.side = side

    def area(self):
        return self.side * self.side


print("areas:", [s.area() for s in [Rect(5, 4), Sq(4)]])


# Dependency inversion: the service depends on a tiny "store" abstraction
class InMemoryOrders:
    def __init__(self):
        self.rows = []

    def save(self, order):
        self.rows.append(order)


class OrderService:
    def __init__(self, store):  # any object with save() works
        self.store = store

    def place(self, item):
        self.store.save(item)
        return f"placed {item}"


store = InMemoryOrders()
print(OrderService(store).place("book"), "| saved:", len(store.rows))

Output

rectangle 5 wide -> 20
square 5 -> 16 (caller expected 20)
areas: [20, 16]
placed book | saved: 1

stretch() was written for rectangles and returns 16 for a square instead of 20. Nothing crashed: the bug is a silently wrong answer, which is why LSP violations are hard to spot.

Common mistakes

  • Modelling real-world taxonomy instead of behaviour. "A square is a rectangle" is true in geometry but false for a mutable Rectangle class. Subtype by behaviour (promises), not by vocabulary.
  • Throwing "not supported" from an overridden method. That is an LSP and ISP smell: the interface is too wide for this class.
  • Confusing dependency inversion with dependency injection. DIP is the design rule (depend on abstractions you own). DI is one technique for supplying them (the next lesson).

Key points

  • LSP: a subtype must keep every promise of its parent. If callers need instanceof checks, the hierarchy is wrong.
  • ISP: small, client-focused interfaces beat one large interface.
  • DIP: business logic depends on abstractions; details are plugged in from outside.
  • Together these make code easy to test (swap in fakes) and easy to change (swap implementations).
  • Further reading: Liskov substitution principle and SOLID.

Practice questions

Why is `Square extends Rectangle` the classic Liskov violation?

Answer

A mutable Rectangle promises that setting the height leaves the width unchanged. A Square must keep both sides equal, so it breaks that promise. Code written for rectangles (set width 5, height 4, expect area 20) gets 16. The fix is to model shapes by what they really share, such as an immutable Shape with area().

What is a sign that an interface violates the Interface Segregation Principle?

Answer

Implementations that leave methods empty or throw "not supported", and clients that use only a small part of the interface. Split it into smaller interfaces focused on what each client needs.

What is the difference between dependency inversion and dependency injection?

Answer

Dependency inversion is a design principle: high-level code depends on abstractions it owns, not on low-level details. Dependency injection is a technique: dependencies are passed in (constructor, setter or framework container) instead of created inside. DI is the usual way to apply DIP.

How do the SOLID principles make unit testing easier?

Answer

Because classes depend on small abstractions that are passed in, a test can supply fakes (an in-memory store, a fixed clock) instead of real databases or network calls, and each class has one responsibility to test.

Open in the app to mark lessons complete, take notes and track your progress.