SOLID Part 2 — ISP, DIP, and Design Patterns (Strategy, Factory, Observer)
Session 11 of the 48-session learning series.
Why this session matters
This is Session 11 of 48 in the OOP & Languages track. It builds on the rhythm of one focused topic, paced so you have time to actually absorb it rather than rush.
Agenda
- ISP — don’t force consumers to depend on interfaces they don’t use
- DIP — depend on abstractions, not concretions; the inversion shape
- Strategy / Factory / Observer — the three patterns you’ll actually use
- Patterns in Python — functions and Protocols beat heavy class hierarchies
- When patterns help vs hurt — a short anti-pattern guide
Pre-read (skim before the session)
- ArjanCodes — Design Patterns Tour (video)
- Gang of Four — Design Patterns (book index)
- Cosmic Python — Architecture Patterns with Python
- PEP 544 — Protocols
Deep dive
1. ISP — Interface Segregation Principle
No client should be forced to depend on methods it does not use.
The smell: a 'fat' interface where most implementers only care about half the methods, raising NotImplementedError for the rest.
Bad — one giant PrinterDevice interface:
class PrinterDevice(Protocol):
def print(self, doc): ...
def scan(self, doc): ...
def fax(self, doc): ...
def staple(self, doc): ...
A basic printer doesn't fax or staple. It either fakes the methods or breaks LSP.
Good — small, role-based interfaces:
class Printer(Protocol):
def print(self, doc): ...
class Scanner(Protocol):
def scan(self, doc): ...
class Fax(Protocol):
def fax(self, doc): ...
class Stapler(Protocol):
def staple(self, doc): ...
class MultiFunctionDevice(Printer, Scanner, Fax): ... # opts in by composition
Consumers depend only on what they need: def archive(scanner: Scanner) doesn't drag in print/fax.
2. DIP — Dependency Inversion Principle
High-level modules should not depend on low-level modules. Both should depend on abstractions.
The shape: when a high-level policy module needs to talk to a low-level infrastructure module, invert the relationship by introducing an abstraction the high-level module owns and the low-level module implements.
Before:
# high-level
class OrderService:
def __init__(self):
self.db = PostgresOrderRepo() # direct dep on postgres
After:
class OrderRepo(Protocol):
def save(self, order: Order) -> None: ...
def get(self, id: str) -> Order: ...
class OrderService:
def __init__(self, repo: OrderRepo):
self.repo = repo
# in adapter layer (lives near infra)
class PostgresOrderRepo:
def save(self, order): ...
def get(self, id): ...
Now OrderService knows about OrderRepo (its own abstraction) and Postgres knows about OrderRepo (implements it). The arrow of dependency inverted — high-level no longer imports the low-level.
This is the backbone of clean / hexagonal / onion / ports-and-adapters architecture. The pattern is everywhere because it makes substitution (and testing) trivial — swap in an InMemoryOrderRepo in tests, no patches.
3. Strategy pattern (you saw it in S04)
Encapsulate an algorithm in an object; swap implementations at runtime.
class Sort(Protocol):
def sort(self, items: list) -> list: ...
class Quicksort:
def sort(self, items): ...
class Mergesort:
def sort(self, items): ...
class Pipeline:
def __init__(self, sorter: Sort):
self.sorter = sorter
def run(self, items):
return self.sorter.sort(items)
In Python, Strategy is often just a callable:
class Pipeline:
def __init__(self, sort_fn):
self.sort_fn = sort_fn # any callable list→list
def run(self, items):
return self.sort_fn(items)
pipeline = Pipeline(sort_fn=sorted)
Don’t build a class hierarchy when a function works.
4. Factory pattern
Encapsulate construction so consumers ask 'give me an X for this input' instead of
if/elif/elifover types.
def payment_for(method: str) -> PaymentStrategy:
return {
"card": CardPayment(),
"upi": UPIPayment(),
"paypal": PayPalPayment(),
}[method]
In Python it’s usually just a function returning the right object. The full GoF AbstractFactory is overkill 90% of the time.
Use a factory when:
- Construction is non-trivial (config, secrets, connection pools).
- You’re hiding which concrete type from the caller.
- You want a single registration point (
register("card", CardPayment)) for plugins.
5. Observer pattern (a.k.a. pub/sub)
Subjects publish events; observers subscribe; subject doesn’t know who’s listening.
from collections import defaultdict
class EventBus:
def __init__(self):
self._subs: dict[type, list] = defaultdict(list)
def subscribe(self, event_type, handler):
self._subs[event_type].append(handler)
def publish(self, event):
for h in self._subs[type(event)]:
h(event)
bus = EventBus()
bus.subscribe(OrderPlaced, send_confirmation_email)
bus.subscribe(OrderPlaced, update_inventory)
bus.publish(OrderPlaced(order_id="..."))
In-process Observer is a stepping-stone to real pub/sub (Kafka, NATS, Pub/Sub) across services. The shape is the same; the bus is just remote.
6. Patterns in Python — don't over-Java-ify
A lot of GoF patterns dissolve in Python because:
- First-class functions → Strategy / Command often become functions.
- Decorators → the Decorator pattern is literally syntax.
Protocol→ no need for abstract base classes for most interfaces.- Module-level state → Singleton is
instance = ClassName()at module load.
Reach for a class hierarchy only when:
- Multiple methods share state (
__init__collaborators). - The pattern truly has multiple methods (Strategy with
sort+validate+report). - Polymorphism is genuinely needed across many call sites.
7. Anti-patterns to call out in code review
- God classes — OrderService with 27 methods. Split by SRP.
- Anaemic models — dataclasses with no behaviour + a parallel
OrderManager. Either grow the model or accept it's a DTO. - Premature factories — a factory for one concrete type.
- Singletons hidden behind globals — untestable. Make them injectable.
isinstanceladders — you wanted polymorphism; refactor with strategies or pattern matching.- Layer cake with no behaviour — 5 layers, each forwards to the next. Cut layers.
8. A small worked refactor
Before — a notification function with 4 if-branches and infrastructure baked in:
def notify(user, channel, msg):
if channel == "email":
smtp = smtplib.SMTP("mail.local"); smtp.sendmail(...)
elif channel == "sms":
twilio.send(user.phone, msg)
elif channel == "push":
fcm.send(user.device_token, msg)
elif channel == "slack":
slack.post_message(user.slack_id, msg)
After — strategy + DIP:
class Notifier(Protocol):
def notify(self, user, msg): ...
class EmailNotifier: ...
class SmsNotifier: ...
class PushNotifier: ...
class SlackNotifier: ...
def get_notifier(channel: str) -> Notifier:
return REGISTRY[channel] # factory
# usage in service
class NotificationService:
def __init__(self, notifier: Notifier):
self.notifier = notifier
def notify(self, user, msg):
self.notifier.notify(user, msg)
Wiring (e.g. in your app composition root) is the only place that knows about all four implementations.
9. When patterns hurt
- You added a Strategy + Factory + Observer for a CLI tool that runs once a week.
- You wrapped every dataclass in an interface 'in case we add a second one'.
- You have 5 layers of
OrderHandlerFactoryProviderbecause the framework demanded it.
Reach for the simplest thing that still passes the real test: can two engineers safely change two parts of the system at the same time?
10. What's next (Session 16 — Concurrency)
We shift from class-shape questions to runtime-shape questions: threads, asyncio, the GIL, actor models, and where each one is the right answer.
Reading material
Books:
- Design Patterns: Elements of Reusable Object-Oriented Software — Gamma, Helm, Johnson, Vlissides ("Gang of Four")
- Head First Design Patterns, 2nd ed. — Freeman, Robson (easier on-ramp than GoF; Strategy / Factory / Observer covered)
- Clean Architecture — Robert C. Martin (the DIP chapter is gold; "The Dependency Rule")
Papers / essays:
- The Dependency Inversion Principle (Robert C. Martin, 1996) — the original DIP essay.
- The Interface Segregation Principle (Robert C. Martin) — original ISP essay.
Official docs:
- Python
typing.Protocol(PEP 544) — duck typing meets static checking. - Python
abc— Abstract Base Classes - Java
java.util.function— functional interfaces for Strategy
Blog posts:
- Refactoring Guru — Design Patterns catalog — code in Python/Java/TS/C#, UML diagrams, when-to-use.
- Strategy vs Template Method — Martin Fowler — the distinction matters in real codebases.
- Composition over inheritance — Real Python
In-depth research material
- python-patterns — github.com/faif/python-patterns — ~41k ★ idiomatic Python implementations of every GoF pattern.
- Java Design Patterns — github.com/iluwatar/java-design-patterns — ~89k ★, the most-starred patterns repo on GitHub.
- Plugin pattern in modern Python — testing.googleblog.com
- The Pragmatic Engineer — "On Patterns" deep dive — when patterns become anti-patterns.
- Programming Throwdown — Design Patterns episode — extended discussion of pattern overuse.
- Sandi Metz — Nothing is Something (talk + companion blog) — the polymorphism mental model.
Videos
- Strategy Pattern — Design Patterns (ep 1) — Christopher Okhravi · 35 min — the most popular design-pattern video on YouTube (1.6M views). Mandatory.
- Observer Pattern — Design Patterns (ep 2) — Christopher Okhravi · 50 min — pub/sub mechanics with crisp code walkthrough.
- Factory Method Pattern — Design Patterns (ep 4) — Christopher Okhravi · 27 min — the canonical creational pattern explainer.
- Depend on Abstractions not Concretions (DIP) — Christopher Okhravi · 12 min — exactly what this session needs on DIP.
- Interface Segregation Principle (SOLID) — Christopher Okhravi · 5 min — quick ISP refresher; pairs with the longer DIP video.
LeetCode — Design Twitter
- Link: https://leetcode.com/problems/design-twitter/
- Difficulty: Medium
- Why this problem: User→tweets list + followee set; getNewsFeed merges via min-heap of top-k by timestamp.
- Time-box: 30 minutes. Look up the editorial only after.
Post-session checklist
By the end of this session you should be able to:
- Spot an ISP violation (a fat interface) and split it into role-based interfaces.
- Apply DIP to invert a direct DB dependency.
- Implement Strategy, Factory, Observer in idiomatic Python (functions where possible).
- Call out 3 anti-patterns and propose refactors.
- Solve
design-twitterusing composition + min-heap merge.
Generated from sessions_data.py + content_part*.py. To edit a video / leetcode / title, edit the data file and re-run write_sessions.py.