SOLID, Part 2 — Liskov, Interface Segregation and Dependency Inversion
Shelfwise's dispatch job loops over every product in paid orders and calls product.ship(). The night the first e-book sold, the job crashed: EBook.ship() raised NotImplementedError. The last three SOLID letters explain why that happened and how to design so it cannot.
The rule
Keep the parent's promises, keep interfaces small, and depend on abstractions.
L — Liskov Substitution Principle (LSP). Anywhere code uses a parent type, any subtype must work without surprises. A subclass may do its job differently, but it may not refuse the job, demand more from callers, or give back less than the parent promised. EBook.ship() raising an error breaks every caller that trusted Product.ship().
I — Interface Segregation Principle (ISP). No class should be forced to depend on methods it does not use. The e-book crash has a root cause: Product promised ship() and download_link() to everyone. Split the fat interface:
class Product(ABC):
@abstractmethod
def price(self): ...
class Shippable(ABC):
@abstractmethod
def ship(self, address): ...
class Downloadable(ABC):
@abstractmethod
def download_link(self): ...
class Book(Product, Shippable): ...
class EBook(Product, Downloadable): ...The dispatch job now asks for Shippable things only, and an e-book is never handed to it.
D — Dependency Inversion Principle (DIP). High-level policy (checkout rules) should not depend on low-level details (one payment provider's client). Both should depend on an abstraction.
class Checkout:
def __init__(self, gateway: PaymentGateway): # any gateway
self.gateway = gatewayBefore, Checkout created AcmePayClient() inside itself, so changing provider meant editing checkout. Now the detail plugs into an abstraction that checkout owns, and a test can pass in a FakeGateway. Passing the dependency in like this is called dependency injection.
The shortcut
Run three quick checks on any class:
- L: Would any subclass surprise a caller of the parent? Look for
NotImplementedError, extra preconditions, or weaker results. - I: Does any implementer leave a method empty or raising? The interface is too fat.
- D: Does high-level code call a constructor of a concrete detail (
AcmePayClient())? Inject an abstraction instead.
Liskov check
EBook.ship()raises. A caller ofProduct.ship()gets a surprise. Violation.
The trap is fixing LSP with a type check (if not isinstance(p, EBook): p.ship()). That hides the symptom and brings back the ladder that polymorphism removed.
Drills
Answer fast — trust the rule.
FreeSample(Book) overrides price() to raise an error, because samples are not sold. Which principle does this break?
A Printer interface has print(), scan() and fax(), and the cheap printer's fax() does nothing. What does ISP suggest?
Which constructor follows Dependency Inversion?
Why does DIP make Shelfwise's checkout easier to test?
Remember
- Keep the parent's promises (L), keep interfaces small (I), depend on abstractions (D).
- Checks: surprising subclasses, empty or raising methods, policy code constructing concrete details.
- Trap: patching LSP with a type check instead of fixing the interface.
