Search Tech Journey

Find topics, journeys and posts

back to blog
engineeringadvanced 12m2026-06-03

Day 05 — SOLID Principles + Strategy / Factory / Observer Patterns in Python

Clean OO design isn't legacy folklore — it's how you keep agent frameworks, data pipelines, and microservices maintainable. SOLID + a handful of patterns is the…

SOLID is a memorable acronym, but it's really five lenses on the same goal: make code easy to change without fear. Pair them with three workhorse patterns (Strategy, Factory, Observer) and you cover 80% of day-to-day design decisions.

🧠 Concept

Why it matters & the mental model.

1. Single Responsibility (S)

A class/module should have one reason to change. Not "do one thing" (too vague), but "have one stakeholder". An invoice class that formats PDF, computes tax, and emails the customer has three stakeholders (design, finance, comms) → three reasons to change → split.

2. Open/Closed (O)

Code should be open for extension, closed for modification. Concretely: instead of if file_type == 'csv': ... elif 'parquet': ..., define an abstract Reader and let consumers plug in new readers without touching the dispatcher. This is exactly what Strategy gives you.

3. Liskov Substitution (L)

Subtypes must honour the supertype's contract — same exceptions, same invariants, no stricter pre-conditions. The classic violation: Square(Rectangle) overriding set_width to also set height. Code that holds a Rectangle reference will misbehave.

4. Interface Segregation (I)

Don't force clients to depend on methods they don't use. Better five small Protocols (Reader, Writer, Closeable, Seekable, Resettable) than one fat IO. In Python use typing.Protocol for structural typing — no inheritance needed.

🛠 Deep Dive

Internals, code, architecture.

5. Dependency Inversion (D)

Depend on abstractions, not concretions. The high-level pipeline shouldn't import boto3 directly; it should accept an ObjectStore protocol. This is what makes unit testing possible (inject a fake) and what lets you swap S3 → GCS without surgery.

6. Strategy pattern — the SRP/OCP workhorse

The importer doesn't know or care which parser it has; it just calls parse(stream). Adding a new format = one new class, zero edits to the importer.

7. Factory — taming construction

When choosing the right concrete type depends on input data or config, hide the if/elif behind a ParserFactory.create(ext). Variants:

  • Simple factory (a function): good enough 80% of the time.
  • Factory Method (subclasses override create_*): when families of products vary together.
  • Abstract Factory (factory of factories): when you need plug-replaceable suites (UI toolkits, cloud SDKs). In Python, often the cleanest factory is a dict: READERS = \{'csv': CsvReader, 'json': JsonReader\}; READERS[ext]().

8. Observer / Event pattern — decoupled side-effects

Used everywhere: progress bars, audit logs, metrics. Producer emits event → multiple subscribers react independently. Python's blinker library is delightful; the stdlib offers dataclass + list[Callable].

class EventBus:
    def __init__(self): self.subs = collections.defaultdict(list)
    def on(self, name, cb): self.subs[name].append(cb)
    def emit(self, name, **payload):
        for cb in self.subs[name]: cb(**payload)

Pitfalls: subscribers leaking memory (use weak refs), ordering assumptions (don't), exceptions in one subscriber breaking the chain (try/except per call).

🚀 In Practice

Trade-offs, exercises, what to ship today.

9. Idiomatic Python flavour

  • Prefer Protocol over ABCs unless you need shared default implementations.
  • Use @dataclass(frozen=True, slots=True) for value objects — gives equality, hash, immutability, fewer attribute lookups.
  • functools.singledispatch is a built-in factory: register handlers by type.
  • Avoid deep inheritance trees; composition is almost always clearer.

10. Smell-driven refactoring

Look for these and apply the patterns:

  • Long if/elif on type or string → Strategy or dict-factory.
  • Class doing IO + business logic → split, inject (DI).
  • God-class with mixed concerns → SRP carve-out.
  • Tests that need to monkey-patch globals → DI violation, add a parameter.

11. Anti-patterns

  • Anaemic domain model: data class + service everywhere → behaviour scattered.
  • Pattern fever: every problem dressed as Strategy + Visitor + Decorator. Reach for the simplest thing that works; promote to a pattern when a second variant appears.

12. What to take away

Senior reviewers ask: "How would you test this without spinning up Postgres?" If you can't answer "inject a Repository protocol", you're failing DIP. The recovery is always: identify the boundary, extract a Protocol, accept it as a parameter.

Key points

    Resources

    Practice Problem: LRU Cache (Medium)