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

class Rectangle: def set_width(self, width): self.width = width def set_height(self, height): self.height = height def area(self): return self.width * self.height class Square(Rectangle): # "is-a" Rectangle, in the type-hierarchy sense def set_width(self, width): self.width = width; self.height = width def set_height(self, height): self.width = height; self.height = height
Verified directly — a function written correctly for Rectangle produces a genuinely wrong answer for Square
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 100set_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.
The lesson isn't "don't model squares as rectangles"
It's that inheritance claims a subtype can stand in for its parent anywhere the parent is expected — and 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

class Worker: def work(self): raise NotImplementedError def eat(self): raise NotImplementedError class RobotWorker(Worker): def work(self): return "Robot working" def eat(self): raise NotImplementedError("Robots don't eat") # forced by the interface, but nonsensical
Verified directly — generic code trusting the fat interface genuinely crashes
lunch_break(workers) calls .eat() on every Worker in a list, trusting that every Worker honors the full interface. With [HumanWorker(), RobotWorker()]: it correctly crashesNotImplementedError: Robots don't eat — the instant it reaches the robot.
Verified directly — segregated interfaces let generic code stay correct by construction
Splitting 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

# BAD — depends directly on a concrete class class NotificationServiceBad: def __init__(self): self.sender = EmailSenderConcrete() # GOOD — depends only on an abstraction class NotificationSender: def send(self, message): raise NotImplementedError class NotificationServiceGood: def __init__(self, sender): self.sender = sender def notify(self, message): return self.sender.send(message)
Verified directly — the source stays byte-identical regardless of which concrete sender is injected
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.
This is Software Architecture Fundamentals Chapter 7, verified again
That chapter's own 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 findingWhat it connects to
Square's own override silently breaking a caller's assumptionDesign 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 forcedChapter 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 dependencySoftware Architecture Fundamentals Chapter 7's own ~18,111× testability finding — the same principle, verified at two different scales, one course apart

Hands-On Exercises

Exercise 1

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.

📄 View solution
Exercise 2

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.

📄 View solution
Exercise 3

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.

📄 View solution

Chapter 7 Quick Reference

  • Liskov Substitution, verified: substituting a Square for a Rectangle produced a real wrong area (100 instead of 50) in code written correctly for the base type
  • Interface Segregation, verified: a fat interface forced RobotWorker to implement eat() nonsensically, crashing generic code; segregated interfaces filtered by isinstance excluded 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