SOLID II: Liskov Substitution, Interface Segregation & Dependency Inversion
Clean Code, SOLID & Refactoring
Chapter 7 · SOLID II: Liskov Substitution, Interface Segregation & Dependency Inversion
The remaining three SOLID principles, each verified with a genuine, reproduced failure — a wrong number, a crash, and a hardcoded dependency that blocks extension. The last one connects directly to a course you've already completed.
Liskov Substitution: a Wrong Answer, Not Just an Awkward Design
resize_and_check(rect) calls set_width(5) then set_height(10) and checks the resulting area against 5 × 10 = 50. For a real Rectangle(2, 2): expected 50, actual 50 — correct. For a Square(2, 2), substituted in exactly where a Rectangle was expected: expected 50, actual 100 — set_height(10) silently forced the width to 10 too, so the area is 10 × 10, not 5 × 10. The function did nothing wrong; the substitution itself broke correctness.
Square's own override silently changes what "setting the width" means. Liskov Substitution is violated the moment a subtype's own behavior surprises code that only knows about the base type.
Interface Segregation: a Forced Method That Crashes Generic Code
lunch_break(workers) calls .eat() on every Worker in a list, trusting that every Worker honors the full interface. With [HumanWorker(), RobotWorker()]: it correctly crashes — NotImplementedError: Robots don't eat — the instant it reaches the robot.
Worker into Workable and Eatable, with RobotWorker implementing only Workable: lunch_break_fixed(workers), filtering with isinstance(w, Eatable), correctly returns ['Human eating lunch'] — the robot is never even asked to eat, because it was never claimed to be able to.
Dependency Inversion: the Same Principle, One Course Later
NotificationServiceGood(EmailSender()) correctly returns "Emailing: hello"; NotificationServiceGood(SmsSender()) correctly returns "Texting: hello". NotificationServiceGood's own source, captured before and after using both: byte-identical, True. NotificationServiceBad, by contrast, can only ever email — adding SMS support would require editing it directly.
PricingEngine depended only on InventoryPort/NotificationPort — never on RealInventoryAdapter or FakeInventoryAdapter by name — and measured an ~18,111× testability payoff from it. This chapter's NotificationServiceGood is the identical pattern, one level smaller: depend on NotificationSender, never on EmailSender or SmsSender directly. Dependency Inversion is the SOLID principle; Hexagonal Architecture is what it looks like applied to a whole application.
Where This Connects
| This chapter's finding | What it connects to |
|---|---|
| Square's own override silently breaking a caller's assumption | Design Patterns Chapter 8's State pattern — a subtype changing behavior in a way callers don't expect is exactly the risk State's own explicit transitions guard against |
lunch_break crashing on a method the interface never should have forced | Chapter 4's Long Parameter List — both smells share the same root cause: a contract asking for more than every real user of it can honestly provide |
| A byte-identical service regardless of injected dependency | Software Architecture Fundamentals Chapter 7's own ~18,111× testability finding — the same principle, verified at two different scales, one course apart |
Hands-On Exercises
Write a second test against this chapter's own resize_and_check, calling set_height(10) before set_width(5) instead of after, against a fresh Square(2, 2). Verify whether reversing the call order changes the outcome, and explain what this reveals about the nature of the LSP violation.
Add a third worker type to this chapter's own segregated hierarchy, VendingMachineWorker, implementing neither Workable nor Eatable (it just dispenses snacks). Verify lunch_break_fixed correctly excludes it from the list, and verify a similarly-named shift_schedule(workers) function filtering by Workable also correctly excludes it.
Add a third sender type to this chapter's own NotificationSender hierarchy, PushNotificationSender. Verify NotificationServiceGood's own source is still byte-identical after adding it, and explain — using this chapter's own explicit connection to Software Architecture Fundamentals Chapter 7 — which specific verified finding from that chapter this result reproduces.
Chapter 7 Quick Reference
- Liskov Substitution, verified: substituting a
Squarefor aRectangleproduced a real wrong area (100instead of50) in code written correctly for the base type - Interface Segregation, verified: a fat interface forced
RobotWorkerto implementeat()nonsensically, crashing generic code; segregated interfaces filtered byisinstanceexcluded it correctly instead - Dependency Inversion, verified: a service depending on an abstraction stayed byte-identical regardless of which concrete sender was injected — the same principle Software Architecture Fundamentals Chapter 7 measured at ~18,111×
- Next chapter: The Refactoring Catalog: Core Techniques — the concrete moves that turn a violation into compliance