PART 1: The Patterns

Chapter 1: Introduction to Design Patterns

Design patterns act like sturdy bridges across recurring software problems. They offer familiar shapes that developers recognise and reuse, which keeps projects from sliding into tangled thickets of guesswork. The aim of this chapter is to set the stage: what patterns are, why they matter, how to read their diagrams, and how this book presents them using clear pseudocode and compact Python examples.

How to Use This Book

This book is divided into two parts that work together. Part 1 introduces each pattern in a concise form so you can learn its shape, intent, and basic structure quickly. You see the essential diagram, the core idea, and a small Python demonstration. Think of Part 1 as a map of the pattern landscape.

Part 2 expands on that foundation. It explores how patterns interact, how they evolve in real projects, how to choose between them, and how they behave under testing and refactoring. These chapters widen the view and show the judgement behind applying patterns well.

If you are new to design patterns, start with Part 1 to learn each pattern cleanly. Once you feel comfortable with the basics, move into Part 2 for deeper insight and practical guidance. Readers who already know some patterns may jump between parts freely, using Part 1 for reference and Part 2 for the broader picture.

What Design Patterns Are

Design patterns are repeatable solutions for common problems in software architecture. They do not provide finished code; instead they describe the relationships, roles and behaviours that solve a particular type of challenge. You can think of them as reusable blueprints that help you construct flexible and maintainable systems. Each pattern has a name, a purpose, a set of participants and a typical flow of interactions. Once you have learned these shapes you start recognising them in real projects, which makes it easier to understand unfamiliar code quickly.

💡 A pattern is not a recipe. It gives you structure and intent while leaving the final implementation open for adaptation.

Why Patterns Matter

Patterns help teams communicate clearly. When someone says that a system uses the Observer pattern or that a component behaves like a Proxy, everyone knows the general shape without reading every line. They also provide shortcuts to experience because each pattern carries the lessons of many earlier designs. By using patterns you gain structure, shared vocabulary and a way to reason about change without rewriting entire systems.

Patterns also encourage stable architecture. They help you separate concerns, reduce duplication and build systems that grow without collapsing under their own weight. This keeps projects predictable even as they expand.

How to Read Pattern Diagrams

Pattern diagrams show the relationships between elements such as classes, interfaces and objects. They do not show full behaviour; instead they highlight the key roles that make the pattern work. The drawings often use simple boxes for components and arrows for connections. A single arrow usually shows direction of communication, while double arrows show mutual knowledge or interaction. Boxes may include labels like ConcreteProduct or Invoker to show how each element participates in the pattern.

Diagrams may omit internal details. For example, a box might show methods … to indicate that the internal list exists but is not central to the pattern. This keeps the focus on structure rather than the noise of implementation.

⚠️ A diagram is only a guide. Always read the surrounding text to understand the intent behind the shapes.

Pseudocode Conventions

Pseudocode lets you see the essential shape of a pattern without binding it to a specific programming language. This book uses a simple, readable style that resembles structured programming. Blocks appear with braces containing content omitted when the detail is irrelevant, such as { … }. Names use clear roles like Subject or Creator. Control flow uses plain words so readers can follow the logic without worrying about syntax rules.

The aim is clarity. Pseudocode examples show the minimum structure required to understand the pattern. All optional or distracting details are trimmed away so the pattern stands on its own.

Python Demonstration Rules

Python examples give each pattern a place to run. They are intentionally small so they fit comfortably alongside the pseudocode without overwhelming the structure. They use two-space indentation for Kindle formatting and avoid unnecessary features so the focus stays on the pattern itself. Each example runs as written, keeping the behaviour concrete while still matching the broader pattern shape.

When pattern terminology appears inside code comments or identifiers it appears exactly as written in the pattern description. This keeps everything aligned so readers can map the concepts cleanly.

Chapter 2: The Gang of Four Families

The classic design patterns are grouped into three families. Each family focuses on a particular kind of problem that appears again and again in software. These groups help you navigate the pattern landscape by showing where each idea belongs and how patterns within a family relate to each other. By understanding the families you get a mental map that guides you when choosing a pattern for a new design challenge.

Creational Patterns

Creational patterns deal with object creation. They help you control how and when objects appear so that your code avoids brittle direct construction. These patterns let you define flexible ways to build complex objects, hide construction details or delay creation until the moment it is truly needed. The goal is to produce objects in a reliable and adaptable manner while keeping your system open to future change.

💡 Creational patterns often help reduce duplication because construction logic lives in one place rather than spreading throughout a project.

Structural Patterns

Structural patterns focus on how objects fit together. They describe ways to combine components so that the whole system becomes easier to maintain. Some patterns wrap existing objects, some connect unrelated interfaces, and others group components into hierarchies. The main theme is arrangement: these patterns give your system stable, modular structure without locking you into rigid designs.

Behavioral Patterns

Behavioral patterns describe how objects interact. They define communication flows, responsibilities and decision paths within a system. These patterns often help reduce coupling because they separate what happens from who performs the action. As a result your code becomes easier to extend because you can change behaviour without reshaping the entire structure.

⚠️ Do not confuse structure with behaviour. A system can be neatly arranged yet still communicate in awkward or tangled ways. Behavioral patterns help prevent that situation.

Choosing the Right Category

When you face a new design problem, the family groups act like signposts. If the challenge involves constructing objects safely or flexibly, you look toward creational patterns. If you need a clean arrangement of components, structural patterns point the way. If the difficulty lies in communication, decisions or control flow, behavioral patterns carry the answer. The categories help you approach problems with intention so you do not wander through the full catalogue without direction.

Chapter 3: Creational Patterns

Creational patterns control how objects come into existence. They separate the act of making things from the act of using them; this reduces duplication and keeps construction choices flexible. The following sections introduce the classic set with short pseudocode and compact Python demonstrations.

Singleton

Singleton ensures that only one instance of a class exists and that all clients share it. The goal is a single point of access for shared state that truly must be global.

Intent

Provide exactly one object that coordinates or stores shared resources; give all callers a stable access path.

Structure

A class hides its constructor and exposes a get_instance or similar function that returns the single object. Lazy creation is common; the instance appears at first use.

Pseudocode

class Singleton {
  private static instance

  static get_instance() {
    if instance is null then
      instance = new Singleton()
    return instance
  }

  method use() { … }
}

The pseudocode omits thread safety. Real systems often add a lock or rely on language features that guarantee safe initialisation.

Python

class _Singleton:
  def __init__(self):
    self.settings = {}

_singleton = None

def get_instance():
  global _singleton
  if _singleton is None:
    _singleton = _Singleton()
  return _singleton
💡 In Python you can also use a module as a natural singleton; module variables live once per interpreter.

When to Use

Use Singleton for configuration, logging or a resource manager that must be unique and shared.

Tradeoffs

Global access can hide dependencies; testing becomes harder if code reaches into the singleton directly. Prefer explicit dependency passing when practical.

Factory Method

Factory Method delegates construction to subclasses or collaborator objects. Callers ask for a product through a common method; the factory chooses which concrete class to create.

Intent

Define a method that returns a Product; let subclasses decide which concrete product appears.

Structure

An abstract creator declares create_product. Concrete creators override it to build specific products. Clients work with the Product interface and remain unaware of the concrete type.

Pseudocode

interface Product { method run() }

class ConcreteA implements Product { method run() { … } }
class ConcreteB implements Product { method run() { … } }

abstract class Creator {
  abstract method create_product() : Product
  method operate() {
    p = this.create_product()
    p.run()
  }
}

class CreatorA extends Creator { method create_product() { return new ConcreteA() } }
class CreatorB extends Creator { method create_product() { return new ConcreteB() } }

Python

class Product:
  def run(self):
    raise NotImplementedError

class ConcreteA(Product):
  def run(self):
    return "A"

class ConcreteB(Product):
  def run(self):
    return "B"

class Creator:
  def create_product(self) -> Product:
    raise NotImplementedError
  def operate(self):
    return self.create_product().run()

class CreatorA(Creator):
  def create_product(self) -> Product:
    return ConcreteA()

class CreatorB(Creator):
  def create_product(self) -> Product:
    return ConcreteB()
⚠️ If the number of products grows quickly, consider moving selection logic to configuration or a registry to avoid repeated edits.

When to Use

Use Factory Method when a class cannot anticipate which product subclass to create or when subclasses should choose.

Tradeoffs

Indirection adds small complexity; you gain flexibility and testability in exchange for extra classes.

Abstract Factory

Abstract Factory creates families of related products that must work together. Clients obtain a factory for a given family; all products from that factory are compatible.

Intent

Provide an interface for creating related objects without specifying concrete classes; ensure consistent combinations.

Structure

The factory interface declares one method per product type, such as make_button and make_window. Concrete factories build themed sets, such as DarkFactory and LightFactory.

Pseudocode

interface UIFactory {
  method make_button() : Button
  method make_window() : Window
}

class DarkFactory implements UIFactory {
  method make_button() { return new DarkButton() }
  method make_window() { return new DarkWindow() }
}

class LightFactory implements UIFactory {
  method make_button() { return new LightButton() }
  method make_window() { return new LightWindow() }
}

Python

class UIFactory:
  def make_button(self):
    raise NotImplementedError
  def make_window(self):
    raise NotImplementedError

class DarkFactory(UIFactory):
  def make_button(self):
    return {"type": "button", "theme": "dark"}
  def make_window(self):
    return {"type": "window", "theme": "dark"}

class LightFactory(UIFactory):
  def make_button(self):
    return {"type": "button", "theme": "light"}
  def make_window(self):
    return {"type": "window", "theme": "light"}
💡 Abstract factories shine when you must switch themes or platforms at runtime; swap the factory and the whole family changes.

When to Use

Use Abstract Factory when products must be used together and you want to enforce valid combinations.

Tradeoffs

Adding a new product type means changing every concrete factory; adding a new family is simpler because you implement one new factory.

Builder

Builder constructs complex objects step by step. It separates the process of building from the final representation; this lets the same construction steps create different forms.

Intent

Encapsulate construction logic as a sequence of operations; allow different builders to produce varied results from the same process.

Structure

A director coordinates steps such as add_part and set_option. The builder records choices and finally returns a finished product with build or get_result.

Pseudocode

class Builder {
  method reset()
  method add_part(x)
  method set_option(k, v)
  method get_result() : Product
}

class Director {
  method construct(b : Builder) {
    b.reset()
    b.add_part("core")
    b.set_option("mode", "fast")
  }
}

Python

class ReportBuilder:
  def reset(self):
    self._title = None
    self._lines = []
  def add_part(self, line):
    self._lines.append(line)
  def set_option(self, k, v):
    if k == "title":
      self._title = v
  def get_result(self):
    return {"title": self._title, "body": "\n".join(self._lines)}

class Director:
  def construct(self, b: ReportBuilder):
    b.reset()
    b.set_option("title", "Summary")
    b.add_part("Line 1")
    b.add_part("Line 2")
⚠️ If construction has only one or two choices, a simple function with keyword arguments may be clearer than a full builder.

When to Use

Use Builder when creation needs many steps or when different configurations must share a common assembly process.

Tradeoffs

You gain clarity for complex construction; you pay with extra classes and one more layer of calls.

Prototype

Prototype creates new objects by copying existing ones. Instead of building from scratch, clients clone a ready instance and adjust a few fields.

Intent

Start from an example object; duplicate it quickly; customise the copy.

Structure

Each prototype supports a clone operation that returns a new instance with the same internal state. Cloning may be shallow or deep; choose based on whether nested structures must be copied.

Pseudocode

interface Prototype { method clone() : Prototype }

class Document implements Prototype {
  fields: title, parts
  method clone() {
    copy = new Document()
    copy.title = this.title
    copy.parts = this.parts.copy()  // deep copy as needed
    return copy
  }
}

Python

import copy

class Document:
  def __init__(self, title, parts):
    self.title = title
    self.parts = list(parts)
  def clone(self):
    return Document(self.title, copy.deepcopy(self.parts))
💡 In Python consider copy.copy for shallow clones and copy.deepcopy for deep clones; choose based on how nested your state is.

When to Use

Use Prototype when object creation is expensive or when many similar objects share mostly the same setup.

Tradeoffs

Cloning can duplicate unwanted links if you choose shallow copy where deep copy is required; copying large graphs may also be costly at runtime.

Chapter 4: Structural Patterns

Structural patterns focus on how parts of a system connect. They help you arrange components so that your design stays flexible even as it grows. Each pattern introduces a different way to link objects, wrap behaviour or combine pieces into meaningful structures that stay easy to reason about.

Adapter

Adapter lets incompatible interfaces work together by placing a small translation layer between them. It changes the shape of one object so that it matches what another part of the system expects.

Intent

Convert one interface into another so that existing code can use a new class without modification.

Structure

An adapter holds a reference to an adaptee. When clients call methods on the adapter, it forwards those calls to the adaptee using names or formats the adaptee understands. The client sees a clean and expected interface.

Pseudocode

class LegacyPrinter {
  method old_print(text) { … }
}

interface Printer { method print(text) }

class Adapter implements Printer {
  constructor(adaptee)
  method print(text) {
    adaptee.old_print(text)
  }
}

This keeps the legacy code untouched while giving new clients a compatible entry point.

Python

class LegacyPrinter:
  def old_print(self, text):
    return f"Legacy: {text}"

class PrinterAdapter:
  def __init__(self, adaptee):
    self.adaptee = adaptee
  def print(self, text):
    return self.adaptee.old_print(text)
💡 Adapter is ideal when integrating external libraries that cannot be changed.

When to Use

Use Adapter when you must reuse existing components that have incompatible interfaces or when you want to smooth over library inconsistencies.

Tradeoffs

You gain compatibility with minimal intrusion; you pay with an extra layer of indirection.

Bridge

Bridge separates an abstraction from its implementation so that both can vary without forcing constant rewrites. It keeps your code flexible when you anticipate multiple combinations of concepts and underlying mechanisms.

Intent

Split abstraction and implementation into separate class hierarchies that connect through composition.

Structure

The abstraction holds a reference to an implementor. The implementor defines low-level operations. Together they allow you to mix and match behaviours at runtime without creating a large number of subclasses.

Pseudocode

interface Device { method turn_on(); method turn_off() }

class TV implements Device { … }
class Radio implements Device { … }

class RemoteControl {
  constructor(device)
  method on() { device.turn_on() }
  method off() { device.turn_off() }
}

The remote is the abstraction; the devices are implementations. Either side can grow independently.

Python

class Device:
  def turn_on(self):
    raise NotImplementedError
  def turn_off(self):
    raise NotImplementedError

class TV(Device):
  def turn_on(self): return "TV on"
  def turn_off(self): return "TV off"

class RemoteControl:
  def __init__(self, device):
    self.device = device
  def on(self): return self.device.turn_on()
  def off(self): return self.device.turn_off()

When to Use

Use Bridge when a hierarchy has two changing dimensions such as abstraction and platform or shape and rendering engine.

Tradeoffs

Bridge adds structural clarity at scale; for small hierarchies it may feel heavier than needed.

Composite

Composite lets you treat single objects and groups of objects in the same way. This makes tree structures easier to navigate and manipulate because leaves and containers share a common interface.

Intent

Represent part-whole hierarchies so clients can handle individual items and groups uniformly.

Structure

A component interface offers operations shared by both leaves and composites. The composite stores children and forwards operations to them, building large recursive structures with simple rules.

Pseudocode

interface Node { method show() }

class Leaf implements Node {
  method show() { … }
}

class Group implements Node {
  children = []
  method add(n) { children.append(n) }
  method show() {
    for each child in children:
      child.show()
  }
}

This creates unified behaviour across simple and nested elements.

Python

class Leaf:
  def __init__(self, name):
    self.name = name
  def show(self):
    return self.name

class Group:
  def __init__(self):
    self.children = []
  def add(self, node):
    self.children.append(node)
  def show(self):
    return [child.show() for child in self.children]
⚠️ Composite works best when all children share meaningful operations; avoid forcing unrelated components into the structure.

When to Use

Use Composite when working with nested or hierarchical data such as file trees or UI elements.

Tradeoffs

Uniformity simplifies client code; enforcing a common interface may require compromise in component responsibilities.

Decorator

Decorator adds behaviour to objects by wrapping them in layers. Each decorator keeps the original interface while injecting new features around the existing operations.

Intent

Attach optional behaviour dynamically instead of using large inheritance chains.

Structure

Both component and decorator share the same interface. A decorator stores a component and calls it while adding its own pre or post logic.

Pseudocode

interface Writer { method write(x) }

class BasicWriter implements Writer { … }

class LoggingDecorator implements Writer {
  constructor(inner)
  method write(x) {
    log("write called")
    return inner.write(x)
  }
}

Multiple decorators can wrap one another to build a chain of small enhancements.

Python

class Writer:
  def write(self, x):
    raise NotImplementedError

class BasicWriter(Writer):
  def write(self, x):
    return x

class LoggingDecorator(Writer):
  def __init__(self, inner):
    self.inner = inner
  def write(self, x):
    return f"log: {self.inner.write(x)}"

When to Use

Use Decorator when behaviour must be extended step by step without modifying original classes.

Tradeoffs

Decorators compose cleanly; however long chains can obscure control flow.

Facade

Facade provides a simplified gateway to a complex subsystem. It hides the inner maze behind a clean and unified interface.

Intent

Wrap many components behind one straightforward interface that clients can use without learning the entire subsystem.

Structure

The facade holds and coordinates subsystem objects. Clients send high-level requests to the facade; the facade delegates work to the appropriate parts.

Pseudocode

class Engine { method start() }
class Lights { method on() }

class CarFacade {
  constructor(engine, lights)
  method begin_trip() {
    engine.start()
    lights.on()
  }
}

The client sees a simple operation while the facade handles the detailed sequence.

Python

class Engine:
  def start(self): return "engine"

class Lights:
  def on(self): return "lights"

class CarFacade:
  def __init__(self, engine, lights):
    self.engine = engine
    self.lights = lights
  def begin_trip(self):
    return [self.engine.start(), self.lights.on()]
💡 Facade is handy when exposing a module to new users who only need common operations.

When to Use

Use Facade when a subsystem becomes too detailed for direct use or when you want clearer boundaries between layers.

Tradeoffs

Simplification can hide useful features; avoid making the facade the only entry unless that is the explicit goal.

Flyweight

Flyweight reduces memory usage by sharing common internal state across many similar objects. Only the small pieces of state that vary remain unique.

Intent

Share intrinsic data to support large numbers of fine-grained objects efficiently.

Structure

The factory returns shared flyweights for repeated intrinsic values. Extrinsic values are passed to operations at runtime, keeping each flyweight light.

Pseudocode

class Flyweight {
  constructor(shared)
  method render(extrinsic) { … }
}

class FlyweightFactory {
  pool = {}
  method get(shared):
    if shared not in pool:
      pool[shared] = new Flyweight(shared)
    return pool[shared]
}

This keeps the number of created objects small even when the system uses many of them.

Python

class Flyweight:
  def __init__(self, shared):
    self.shared = shared
  def render(self, extrinsic):
    return f"{self.shared}:{extrinsic}"

class FlyweightFactory:
  def __init__(self):
    self.pool = {}
  def get(self, shared):
    if shared not in self.pool:
      self.pool[shared] = Flyweight(shared)
    return self.pool[shared]

When to Use

Use Flyweight when an application stores many small similar objects such as characters in a document or map tiles.

Tradeoffs

State must be split carefully; incorrect separation of intrinsic and extrinsic values creates bugs.

Proxy

Proxy controls access to another object. It behaves like a stand-in that forwards calls while adding checks or extra behaviour.

Intent

Provide a substitute that manages or protects access to a real object.

Structure

The proxy stores a reference to a subject. Each operation may add permission checks, caching or lazy creation before delegating to the real subject.

Pseudocode

interface Service { method request() }

class RealService implements Service { … }

class Proxy implements Service {
  constructor(real)
  method request() {
    if allowed:
      return real.request()
    else:
      return "blocked"
  }
}

This allows the proxy to protect or manage use of the real service.

Python

class RealService:
  def request(self):
    return "real"

class Proxy:
  def __init__(self, real):
    self.real = real
  def request(self):
    return f"proxy:{self.real.request()}"
⚠️ If a proxy becomes too feature-heavy, it may grow into a separate subsystem; keep responsibilities focused.

When to Use

Use Proxy for lazy loading, permission control, caching layers or network stubs that stand in for remote services.

Tradeoffs

Extra indirection can affect performance; thoughtful design keeps overhead small while preserving clarity.

Chapter 5: Behavioral Patterns

Behavioral patterns focus on how objects talk to each other. They help define responsibilities, communication styles, and the flow of work through your system. The goal is to reduce coupling while keeping intent clear and change-friendly.

Chain of Responsibility

Use Chain of Responsibility to pass a request along a linked list of handlers until one of them can deal with it. Each handler decides whether to handle or to forward, which lets you vary handling logic without large if cascades.

Intent

Decouple senders from receivers by giving multiple objects a chance to handle a request. The chain can be fixed or built at runtime.

Structure

Each handler keeps a reference to the next handler and exposes a method such as handle(request). If it cannot process the request, it forwards the request to next.

class Handler:
  def __init__(self, next=None):
    self.next = next
  def handle(self, req):
    if self.next:
      return self.next.handle(req)

class AuthHandler(Handler):
  def handle(self, req):
    if req.get("user"):
      return super().handle(req)
    return "401 Unauthorized"

class LoggingHandler(Handler):
  def handle(self, req):
    print("log", req)
    return super().handle(req)

class Endpoint(Handler):
  def handle(self, req):
    return "200 OK"

chain = AuthHandler(LoggingHandler(Endpoint()))
result = chain.handle({"path": "/api", "user": "robin"})
💡 Build chains from configuration to turn features on or off without changing code.

Considerations

Keep handlers focused. If the chain gets long, latency grows and debugging becomes harder.

Command

Command turns a request into a stand-alone object. You can queue, log, undo, and redo commands. The invoker triggers execute() on each command without knowing details.

Intent

Encapsulate an action and its parameters into an object with operations such as execute() and possibly undo().

Example

class TextDoc:
  def __init__(self): self.text = ""
  def insert(self, s): self.text += s
  def delete_tail(self, n): self.text = self.text[:-n]

class InsertCommand:
  def __init__(self, doc, s): self.doc, self.s = doc, s
  def execute(self): self.doc.insert(self.s)
  def undo(self): self.doc.delete_tail(len(self.s))

doc = TextDoc()
history = []
cmd = InsertCommand(doc, "Hello")
cmd.execute(); history.append(cmd)
history.pop().undo()
⚠️ If you need undo(), capture enough state to reverse the action. Memory growth can be an issue.

Variations

Use a command bus for async dispatch; add a CommandFactory for deserialization from logs.

Interpreter

Interpreter defines a grammar and evaluates sentences in that grammar. It works best for small languages such as filters, rules, or expression trees.

Intent

Represent grammar rules as classes, then interpret expressions composed from these classes. Terminals hold values, nonterminals combine expressions using interpret(context).

Example

class Num:
  def __init__(self, value): self.value = value
  def interpret(self, ctx): return self.value

class Add:
  def __init__(self, left, right): self.left, self.right = left, right
  def interpret(self, ctx): return self.left.interpret(ctx) + self.right.interpret(ctx)

expr = Add(Num(2), Add(Num(3), Num(4)))
result = expr.interpret({})
💡 For larger grammars, use parser generators; hand-built trees get complex once you add precedence, associativity, and {args …} handling.

Considerations

Interpretation can be slow for deep trees. Consider a compiled form such as bytecode or a visit() that emits optimized steps.

Iterator

Iterator provides sequential access to the elements of a collection without exposing its internal representation. The client uses __iter__() and __next__() or similar protocol.

Intent

Traverse containers uniformly; support multiple simultaneous traversals; keep traversal logic out of the container.

Example

class Countdown:
  def __init__(self, start): self.start = start
  def __iter__(self): 
    n = self.start
    while n > 0:
      yield n
      n -= 1

for n in Countdown(3):
  print(n)

Considerations

Prefer lazy iteration for large data; add filtering and mapping by composing iterators.

Mediator

Mediator centralizes complex communication among colleagues. Each colleague talks to the mediator, which coordinates actions and routes messages.

Intent

Reduce many-to-many links among objects by moving coordination into a single Mediator.

Example

class Chat:
  def __init__(self): self.users = []
  def join(self, user): self.users.append(user)
  def send(self, sender, msg):
    for u in self.users:
      if u is not sender:
        u.receive(msg)

class User:
  def __init__(self, name, chat): self.name, self.chat = name, chat; chat.join(self)
  def send(self, msg): self.chat.send(self, f"{self.name}: {msg}")
  def receive(self, msg): print(msg)

room = Chat()
a = User("Ada", room); b = User("Lin", room)
a.send("hi")
⚠️ A fat mediator can become a god object. Split responsibilities once rules accumulate.

Variations

Use publish-subscribe so the mediator becomes a topic bus; colleagues subscribe to events instead of direct callbacks.

Memento

Memento captures and restores an object’s internal state without breaking encapsulation. The originator creates snapshots, the caretaker stores them.

Intent

Provide checkpoint and rollback. Useful for editors, games, and configuration changes.

Example

class Editor:
  def __init__(self): self.text = ""; self.cursor = 0
  def type(self, s): self.text = self.text[:self.cursor] + s + self.text[self.cursor:]; self.cursor += len(s)
  def save(self): return (self.text, self.cursor)
  def restore(self, m): self.text, self.cursor = m

ed = Editor()
ed.type("Hi"); m = ed.save()
ed.type(" there")
ed.restore(m)
💡 Compress or diff large snapshots to control memory usage when you keep many mementos.

Considerations

Decide who owns snapshot lifetimes. Unbounded caretakers can leak memory.

Observer

Observer defines a one-to-many dependency so when one object changes state, dependents are notified. Subjects expose subscribe() and notify().

Intent

Broadcast change without tight coupling. Observers decide what to do with updates.

Example

class Subject:
  def __init__(self): self._obs = []
  def subscribe(self, fn): self._obs.append(fn)
  def set_value(self, v):
    self.value = v
    for fn in self._obs: fn(v)

s = Subject()
s.subscribe(lambda v: print("plot update", v))
s.subscribe(lambda v: print("cache write", v))
s.set_value(42)
⚠️ Beware update storms. Debounce notifications or batch changes to avoid thrashing observers.

Variations

Use event objects with {type: …, data: …} to version your protocol. Add weak references to avoid retention cycles.

State

State lets an object alter its behavior when its internal state changes. The context holds a reference to a state object; switching states swaps logic.

Intent

Represent state-specific behavior using separate classes that share a common interface such as handle().

Example

class Locked:
  def handle(self, door): print("locked"); door.state = Unlocked()

class Unlocked:
  def handle(self, door): print("open"); door.state = Locked()

class Door:
  def __init__(self): self.state = Locked()
  def press(self): self.state.handle(self)

d = Door(); d.press(); d.press()
💡 Add a simple table for transitions if states grow; map (state, event) -> next to keep logic clear.

Considerations

Prefer small state classes. If states share heavy data, move it into the context to avoid duplication.

Strategy

Strategy defines a family of algorithms, encapsulates each one, and makes them interchangeable. The context selects a strategy that implements a common interface such as compute(input).

Intent

Swap algorithms without changing the client. Choose at runtime or via configuration.

Example

class SumStrategy:
  def compute(self, data): return sum(data)

class MaxStrategy:
  def compute(self, data): return max(data)

class Stats:
  def __init__(self, strat): self.strat = strat
  def run(self, data): return self.strat.compute(data)

s = Stats(SumStrategy()); s.run([1,2,3])
⚠️ Too many tiny strategies can be overkill. Prefer plain functions when you do not need polymorphism or state.

Variations

Use a registry keyed by name to look up strategies from config; switch to a composite strategy for pipelines.

Template Method

Template Method defines the skeleton of an algorithm in a base class and lets subclasses override steps. The template calls hooks like step1() and step2() in a fixed order.

Intent

Enforce a stable process while allowing variation at defined points. Keep the template simple and obvious.

Example

class Report:
  def build(self):
    self.header()
    self.body()
    self.footer()
  def header(self): print("Header")
  def body(self): raise NotImplementedError
  def footer(self): print("Footer")

class SalesReport(Report):
  def body(self): print("Sales …")

r = SalesReport(); r.build()
💡 Add default no-op hooks such as before() and after() so subclasses override only what they need.

Considerations

Prefer composition with Strategy if subclasses multiply. Inheritance can lock you into one axis of variation.

Visitor

Visitor separates an algorithm from the object structure it operates on. Each element provides accept(visitor), and the visitor implements typed methods such as visit_File().

Intent

Add operations to a stable set of element classes without editing them. The tradeoff is that adding a new element type requires touching all visitors.

Example

class File:
  def __init__(self, name, size): self.name, self.size = name, size
  def accept(self, v): return v.visit_File(self)

class Folder:
  def __init__(self, name, children): self.name, self.children = name, children
  def accept(self, v): return v.visit_Folder(self)

class SizeVisitor:
  def visit_File(self, f): return f.size
  def visit_Folder(self, d): return sum(c.accept(self) for c in d.children)

tree = Folder("root", [File("a.txt", 10), Folder("bin", [File("b", 5)])])
total = tree.accept(SizeVisitor())
⚠️ Double-dispatch setups can feel heavy. If your language has pattern matching, consider matching on shape instead.

Variations

Use an Acceptor mixin to reduce boilerplate; add a default visit_Default(…) to handle unknown nodes gracefully.

Chapter 6: Modern Variations and Hybrid Patterns

Modern codebases blend classical patterns with pragmatic techniques. The goal is to keep systems flexible, testable, and easy to evolve. These variations focus on wiring, decoupling, and shaping behavior through explicit composition rather than inheritance heavy hierarchies.

Dependency Injection

Dependency Injection supplies required collaborators from the outside, instead of letting an object construct them. This keeps creation separate from use, which improves testability and swapability of implementations.

Intent

Invert control of construction so that classes declare what they need and a composer provides instances at the boundary using __init__(deps) or decorators such as @Inject in languages that support them.

Structure

Classes expose dependencies as constructor parameters or properties. A composition root builds the object graph. You can pass dependencies directly, use a factory, or ask a container to resolve them through resolve(type).

class Emailer:
  def send(self, to, body): print(f"email to {to}: {body}")

class SignupService:
  def __init__(self, emailer): self.emailer = emailer
  def signup(self, user):
    # create user …
    self.emailer.send(user, "Welcome")

emailer = Emailer()
service = SignupService(emailer)
service.signup("robin@example.com")
💡 Keep a single composition root near application start. All wiring lives there, which simplifies tests and minimizes surprises.

Considerations

Prefer constructor injection for required collaborators; reserve setter injection for optional ones. Avoid hiding dependencies behind global singletons; explicit parameters communicate intent better.

Service Locator

Service Locator provides a registry to look up services at runtime using get(name). It centralizes access to shared services such as logging, configuration, or transport clients.

Intent

Offer a simple way to retrieve services without passing them through every call. This can reduce parameter noise when code is already deeply nested.

Example

class Services:
  _map = {}
  @classmethod
  def register(cls, name, svc): cls._map[name] = svc
  @classmethod
  def get(cls, name): return cls._map[name]

class Logger:
  def log(self, msg): print("LOG:", msg)

Services.register("logger", Logger())

def process(item):
  Services.get("logger").log(f"processing {item}")
  # work …

process("order-123")
⚠️ Service Locator can hide dependencies. Excessive use makes code harder to reason about and to test. Prefer Dependency Injection when you can wire dependencies explicitly.

Variations

Use typed keys or generics instead of raw strings for safer lookups. Scope the locator per module to avoid a single global registry that grows without control.

Event Bus

An Event Bus decouples senders and receivers with asynchronous notifications. Publishers emit events through publish(event); subscribers register callbacks through subscribe(type, handler). No participant needs to know who sits on the other side.

Intent

Promote loose coupling and horizontal scaling by turning direct calls into events. This fits analytics, audit trails, cache invalidation, and background workflows.

Example

class EventBus:
  def __init__(self): self._subs = {}
  def subscribe(self, etype, fn): self._subs.setdefault(etype, []).append(fn)
  def publish(self, etype, event):
    for fn in self._subs.get(etype, []):
      fn(event)

bus = EventBus()

def on_user_signed_up(evt): print("welcome flow for", evt["user"])
bus.subscribe("UserSignedUp", on_user_signed_up)

bus.publish("UserSignedUp", {"user": "robin", "meta": {"ip": "…"}})
💡 Model events as immutable records such as {type: "UserSignedUp", id: …, occurred_at: …, data: …}. This helps with replay and audit.

Considerations

Event flow can be hard to trace. Add correlation identifiers to link related events. When delivery matters, use durable queues; in-memory buses are best for simple apps or local plugins.

Composition Over Inheritance

Composition builds behavior by assembling objects that collaborate, rather than by stacking classes in a deep hierarchy. This keeps parts small and swappable, and avoids the fragility that can come from inheritance chains.

Intent

Prefer a has_a relationship over is_a when you want to vary behavior at runtime or only share a small slice of functionality.

Example

class CacheLayer:
  def get(self, k): return None
  def set(self, k, v): pass

class RetryLayer:
  def __init__(self, inner, attempts=3): self.inner, self.attempts = inner, attempts
  def request(self, op):
    for i in range(self.attempts):
      try: return self.inner.request(op)
      except Exception: 
        if i + 1 == self.attempts: raise

class HttpClient:
  def request(self, op): 
    # perform HTTP …
    return "ok"

client = RetryLayer(HttpClient())
# Later swap in a caching decorator without touching callers:
client = RetryLayer(HttpClient())  # then wrap with CacheLayer if it supported request()
⚠️ Composition still needs clear interfaces. Define small protocols such as request(op) or process(item) so layers remain compatible.

Patterns Together

These ideas mix well. Use Dependency Injection to assemble objects; expose a Service Locator sparingly for legacy seams; route side effects through an Event Bus; prefer composition to extend behavior without subclassing. This combination supports testing, change, and clarity.

Chapter 7: Pattern Implementation in Python

Python offers a rich toolbox of language features that reshape how classical patterns appear in real projects. Some patterns fade because Python already solves the problem; some map directly with almost no change; others need adaptation so they feel natural in a language that prizes readability and minimal boilerplate.

Pythonic Idioms That Replace Patterns

Several patterns shrink or disappear entirely thanks to modules, first class functions, dynamic attributes, and the expressive data model. Python’s built ins often act like pattern accelerators because the language already expects code to be stitched together from small cooperators.

Singleton

Instead of building a formal Singleton, Python tends to use modules. Importing a module returns the same instance each time, and its top level names behave like shared state. If you really need a controlled object, factories or explicit wiring usually work better than strict singletons.

# settings.py
config = {"debug": True}

# anywhere else
import settings
print(settings.config["debug"])
💡 Keep module state simple so that import cycles remain easy to avoid and testing stays predictable.

Strategy

Strategy often becomes a collection of plain functions or callables. Passing a function into another function reads cleanly and avoids setting up tiny classes that only wrap one calculation.

def sum_strategy(data): return sum(data)
def max_strategy(data): return max(data)

def run(data, strat): return strat(data)

run([1, 2, 3], sum_strategy)

Command

Python’s first class functions also act as stand alone commands. You can store them, queue them, and call them later. Closures capture state, which means you do not need objects with execute() unless you prefer that shape.

def insert(doc, s):
  def cmd():
    doc.append(s)
  return cmd

doc = []
cmd = insert(doc, "Hello")
cmd()

Patterns That Translate Cleanly

Some patterns map directly because Python’s class model supports encapsulation, delegation, and polymorphism with little ceremony. These patterns keep their usual structure but feel lighter because Python handles details such as initialization, mixins, and method passing gracefully.

Observer

Observer maps well because Python makes callbacks effortless. Storing callable subscribers and invoking them inside notify() is a natural fit.

class Subject:
  def __init__(self): self.subs = []
  def subscribe(self, fn): self.subs.append(fn)
  def set(self, v):
    self.value = v
    for fn in self.subs: fn(v)

s = Subject()
s.subscribe(lambda v: print("update", v))
s.set(10)
⚠️ Hold weak references in long lived subjects when subscribers need to be collected without manual cleanup.

State

State fits Python’s dynamic dispatch well. Each state class defines behavior, and the context swaps the active instance at runtime.

class StateA:
  def run(self, ctx): print("A"); ctx.state = StateB()

class StateB:
  def run(self, ctx): print("B"); ctx.state = StateA()

class Machine:
  def __init__(self): self.state = StateA()
  def step(self): self.state.run(self)

m = Machine()
m.step(); m.step()

Decorator

Decorator aligns with Python’s function decorators. The idea of wrapping behavior with another callable is baked into the syntax.

def log(fn):
  def wrapper(*a, **k):
    print("calling", fn.__name__)
    return fn(*a, **k)
  return wrapper

@log
def greet(name): print("hi", name)

greet("Ada")

Patterns That Need Adaptation

Some patterns require adjustment because Python’s dynamic nature shifts their shape. Instead of porting them verbatim, you reshape them so they feel idiomatic and avoid unnecessary scaffolding.

Abstract Factory

Abstract Factory often collapses into simple factory functions or data driven builders. Because classes are first class objects, returning a class from a function is straightforward.

def widget_factory(theme):
  if theme == "dark": return DarkWidget
  return LightWidget

class DarkWidget: pass
class LightWidget: pass

Widget = widget_factory("dark")
w = Widget()

Visitor

Visitor needs reshaping when Python’s dynamic type handling already solves most dispatch needs. Pattern matching using match can simplify cases where you would normally write a full visitor hierarchy.

def visit(node):
  match node:
    case {"type": "file", "size": s}: return s
    case {"type": "folder", "items": items}: 
      return sum(visit(i) for i in items)
    case _: raise ValueError("unknown node")
💡 When modeling trees, make node shapes predictable so match {key: …} stays readable.

Mediator

Mediator becomes lighter because Python encourages direct callbacks, small helpers, and module level coordination. Instead of a central coordinator class, you often use event functions, registries, or async channels.

class Chat:
  def __init__(self): self.users = []
  def join(self, user): self.users.append(user)
  def send(self, sender, msg):
    for u in self.users:
      if u is not sender:
        u.receive(msg)

class User:
  def __init__(self, name, chat): self.name, self.chat = name, chat; chat.join(self)
  def send(self, msg): self.chat.send(self, msg)
  def receive(self, msg): print(self.name, "<", msg)

Patterns feel at home in Python when they remain small and intentional. Translate the spirit, not the scaffolding, and let Python’s features carry the weight.

Chapter 8: Pattern Selection and Tradeoffs

Knowing a pattern is useful, yet knowing when to apply it is the real craft. Patterns offer structure and clarity, but they also carry cost. This chapter explores the judgment calls that help you decide when a pattern sharpens your design and when it weighs it down.

When to Use a Pattern

Patterns shine when they solve a specific pain point. They help you name ideas, guide change, and keep collaborators aligned. A well placed pattern turns tangled logic into a set of small cooperating parts. The goal is to strengthen readability, testability, and adaptability without adding needless ceremony.

Signs of a Good Fit

You often reach for a pattern when problems repeat or when behavior needs to vary cleanly. Long conditional blocks, duplicated logic, or rigid wiring are all hints that a pattern may bring order. For example, a cluster of if type … statements points toward Strategy or State. A feature that branches across many modules hints at Observer or an event driven shape.

💡 When you can name what you want to express using a known pattern, you gain a shared vocabulary that keeps teammates on the same page.

Context Matters

Patterns help most when codebases grow and multiple contributors shape the system. At that scale, design clarity is not optional. A small script may not need much structure, yet a long lived application benefits from patterns that tame complexity.

When a Pattern Becomes Overengineering

Patterns can also create clutter when used without a real need. Extra levels of indirection, sprawling class hierarchies, and needless abstractions can hide the simple truth of what the code is trying to do. The art lies in knowing when a pattern clarifies and when it obscures.

Warning Signs

If a pattern forces you to write boilerplate that adds no insight, it may be solving a problem you do not have. Tiny classes whose only method delegates to another object, or factories that create trivial objects, signal that the pattern may be propping up design where plain functions would work.

⚠️ A pattern applied prematurely can freeze your design too early. This limits future options rather than opening them.

Costs That Hide in the Background

Every abstraction shapes the mental model of the system. If contributors must climb through many layers to make simple changes, the pattern is getting in the way. Overuse creates systems where small updates ripple through factories, registries, managers, and wrappers that each impose their own contract.

Balancing Flexibility and Simplicity

Good architecture feels balanced. It avoids both rigid mechanics and wild sprawl. Patterns help find that balance, yet they must be chosen with purpose. Simplicity supports clarity, while flexibility supports change. The sweet spot lies where the structure is clear enough to reason about and open enough to evolve.

Choosing the Lightest Tool

Start with the simplest shape that fits your needs. If functions and modules cover the use case, they often beat heavier patterns. Move upward only when you face a real pressure such as repeated behavior, varying rules, or cross cutting concerns.

Evolving the Design

Let patterns emerge naturally as the system teaches you its shape. As requirements shift, a direct approach may mature into Strategy, Observer, or Builder. This evolution keeps the code honest and prevents accidental complexity from creeping in.

💡 Favor clarity first. A simple design that is easy to read usually outperforms a clever design that requires a tour guide.

Chapter 9: Anti Patterns

Anti patterns are recurring solutions that look convenient at first glance, then quietly multiply trouble. This chapter names common traps, shows how to spot them, and offers practical ways to refactor toward cleaner designs. Each section includes symptoms, causes, a brief example, and remedies.

The God Object

A God Object is a class or module that knows too much and does too much. It centralizes state and behavior that should belong to several collaborators. The result is fragile code that resists change and spreads hidden coupling across a codebase.

Symptoms of GodObject

Look for very large files, hundreds of methods, long switch statements, and fields that reference many unrelated concerns. Small changes ripple through the class, unit tests are slow to write, and developers avoid touching it without a buddy present.

Why teams create a GodObject

Early speed and unclear boundaries push teams toward a single hub of logic. Deadlines, lack of domain modeling, and fears about creating too many files all feed the growth of this class.

Example of a GodObject

In this example a single class performs configuration loading, payment processing, email notifications, and logging. The placeholders use … to indicate omitted content.

class SystemManager:
  def __init__(self, cfg_path):
    self.cfg = self._load_cfg(cfg_path)
    self.db = self._connect_db(self.cfg["dsn"])
    self.logger = Logger(self.cfg["log"])
  def _load_cfg(self, path): …
  def _connect_db(self, dsn): …
  def process_payment(self, user_id, amount): …
  def send_receipt(self, user_id): …
  def rebuild_indexes(self): …
  def rotate_logs(self): …

Refactoring strategies for GodObject

Extract cohesive services, create clear boundaries, and inject dependencies. Start with a narrow slice; pull one responsibility into its own class, write tests, then repeat. Consider patterns such as Facade for integration points and Strategy for swappable behavior.

💡 When carving up a large class, follow the rule of one reason to change. If two methods change for different reasons, they belong in different places.

The Blob

The Blob is a data class with fields galore and very little behavior, coupled to a swarm of procedural functions that manipulate it. Logic lives outside the data, which leads to duplication and scatter.

Symptoms of Blob

Wide DTOs with dozens of public attributes, validation code scattered across helper utilities, and a habit of passing the same structure through many layers. The object becomes a suitcase that everyone rummages in.

Why a Blob appears

Teams begin with transport objects to cross boundaries, then never bring the behavior home. Fear of circular dependencies and a thin-domain mindset keep logic outside where it keeps duplicating.

Example of a Blob

Behavior is not attached to data. Everything pokes at the same structure.

class OrderData:
  def __init__(self, id, items, discounts, user, address):
    self.id = id
    self.items = items
    self.discounts = discounts
    self.user = user
    self.address = address

def calc_total(order): …
def apply_discounts(order): …
def validate_order(order): …

Refactoring strategies for Blob

Move behavior to the data that it touches. Introduce rich domain objects with invariants. Replace procedural helpers with methods on domain classes. Use ValueObject types for immutable concepts such as money, quantity, and address.

Smell Shift
Procedural helper Method on domain object
Many public fields Encapsulated properties
Validation scattered Invariants in constructors

Lava Flow

Lava Flow occurs when experimental or obsolete code cools into the codebase and becomes hard to remove. Dead features, half migrations, and commented fossils remain because nobody is sure what depends on them.

Symptoms of LavaFlow

Unreachable branches, TODOs from years ago, duplicated modules with names like new_… and old_…, and fear of deletion. Build scripts reference legacy paths that nobody can safely touch.

Why LavaFlow forms

Prototypes promote into production without cleanup, large rewrites stall, and knowledge leaves with team members. Without tests, deletion feels risky, so code accretes like cooled rock.

Example of LavaFlow

Two competing implementations sit side by side, both wired in subtle ways.

def compute_price(v1_order): …  # legacy
def compute_price_v2(order, rules): …  # intended replacement
# TODO move callers to compute_price_v2 and delete v1 … someday

Refactoring strategies for LavaFlow

Build a safety net, then remove with confidence. Add characterization tests that record current behavior, route new calls through an adapter toward the replacement, and delete unused entry points once coverage exists.

⚠️ Characterization tests confirm current behavior, not ideal behavior. Record what the system actually does before changing it.

Spaghetti Code

Spaghetti Code is a tangle of jumps, implicit state, and cross-cutting calls. Flow is hard to follow; control paths weave and loop through layers until the mental stack overflows.

Symptoms of Spaghetti

Functions call deep into unrelated modules, global state changes mid-execution, and bugs vanish when you add print statements. Fixes in one area break distant features without warning.

Why teams cook Spaghetti

Short-term patches, lack of boundaries, and mixing levels of abstraction produce tangles. A missing architecture allows any piece to talk to any other piece at any time.

Example of Spaghetti

Flow jumps between concerns, with globals and callbacks on the same path.

state = {"user": None, "cart": []}

def add_item(id): …
def save_cart(): …

def checkout():
  if state["user"] is None:
    prompt_login(lambda: checkout())
  else:
    add_item("bonus")
    save_cart()
    notify("done")  # side effect deep in UI

Refactoring strategies for Spaghetti

Choose a primary flow and separate concerns. Introduce modules for orchestration, state, and side effects. Use Facade to centralize coordination, apply Command to model steps, and inject collaborators to break global state.

💡 If a function changes more than one kind of thing, split it so each function performs a single, clear step that can be tested in isolation.

Golden Hammer

The Golden Hammer is the habit of solving every problem with the same familiar tool. A team succeeds with a library or pattern, then sees every future challenge as another nail. The result is forced fits and accidental complexity.

Symptoms of GoldenHammer

One framework appears in places that do not suit it, abstractions are layered where a simple function would do, and performance falls because the chosen tool fights the shape of the problem.

Why GoldenHammer happens

Success stories, cargo-cult advice, and hiring pipelines that reinforce one stack lead teams to habitual choices. Tool familiarity feels like safety, so tradeoffs are not weighed properly.

Example of a GoldenHammer

A message queue is used for synchronous request handling because the team prefers its dashboards. The system becomes slower and harder to debug.

def get_profile(user_id):
  # Puts a request on a queue, then polls for result … for a read path
  enqueue({"type": "FETCH", "id": user_id})
  return wait_for_result(user_id, timeout=2.0)

Refactoring strategies for GoldenHammer

Start with the simplest workable design. Write short decision records that list options and tradeoffs, then choose tools that match the problem. Prefer small patterns like Strategy or Policy objects that allow swapping implementations without forcing one global choice.

Context Prefer
Simple synchronous reads Direct call with retries
High fan-out writes Asynchronous queue
Static rules that evolve Strategy with small policies

Comparing Anti Patterns

Although these traps differ, they share a theme. Each hides a boundary that should be explicit. The God Object hides service boundaries, the Blob hides behavior inside helpers, Lava Flow hides dead code under fear, Spaghetti hides control flow, and the Golden Hammer hides choice under habit. Make boundaries visible and most anti patterns begin to melt.

Chapter 10: Putting Patterns to Work

This chapter gathers the ideas from earlier sections and lets them walk around in a small but complete example. Real systems use patterns in clusters rather than as isolated decorations. Here you will see how patterns cooperate, how refactoring can reveal useful shapes, and how a modest design grows clearer through simple and well-chosen structure.

Small Application Example

This example builds a tiny note service with creation, listing, and formatting. The goal is not to produce a feature rich tool but to demonstrate how classic patterns settle naturally into a small design. The code uses … to mark omitted details. Each piece is short and easily replaced.

Overview of the components

Three elements drive the example. A storage layer provides persistence, a formatter shapes output, and a coordinator handles requests. The boundaries are simple on purpose so the patterns stay clear rather than disappearing inside unnecessary details.

Applying patterns in the example

The FactoryMethod creates storage engines, Strategy selects a formatter, and a light Facade coordinates high level operations. Together they keep responsibilities tidy and each piece stays small enough for quick reading.

class StorageFactory:
  def create(self, kind):
    if kind == "memory":
      return MemoryStorage()
    if kind == "file":
      return FileStorage("notes.db")
    raise ValueError("unknown storage")

class PlainFormatter:
  def format(self, note): return f"* {note}"

class JsonFormatter:
  def format(self, note): …

class NotesApp:  # facade
  def __init__(self, storage, formatter):
    self.storage = storage
    self.formatter = formatter
  def add(self, text): self.storage.save(text)
  def list(self):
    for n in self.storage.load_all():
      yield self.formatter.format(n)

Running the example

The interaction is simple but shows how the outer shell stays clean because inner components carry their share of responsibility. Switching from plain text to JSON formatting takes one line. Swapping storage engines takes another line. The design remains calm even when the supporting classes change shape.

💡 When demonstration examples feel easy to extend, it often means responsibilities were divided in the right places.

Combining Multiple Patterns

Most real solutions mix patterns. A single feature might use Observer for updates, Decorator for optional behavior, and Adapter to unify incompatible interfaces. These combinations follow a simple rule: let each pattern do one job and let their borders touch cleanly.

Patterns that cooperate

Some patterns complement each other particularly well. A Strategy made of small policies works nicely behind a Facade that hides experiment friendly details. A Builder pairs well with Prototype when new configurations must be spun up quickly. These pairings often appear in frameworks because they promote steady growth rather than tangled expansion.

An example combination

This snippet shows a simple exporter that decorates a writer with compression, then uses a strategy to choose output format. The structure lets new formats or decorators join without disturbing the core exporter.

class GzipWriter:
  def __init__(self, inner): self.inner = inner
  def write(self, data): self.inner.write(compress(data))

class CsvStrategy:
  def encode(self, rows): …

class JsonStrategy:
  def encode(self, rows): …

class Exporter:
  def __init__(self, writer, strategy):
    self.writer = writer
    self.strategy = strategy
  def export(self, rows):
    encoded = self.strategy.encode(rows)
    self.writer.write(encoded)

Avoiding pattern overload

Combining patterns works best when each pattern has a clear job. Overuse leads to extra layers where none are needed. A clean test suite often reveals this because excessive scaffolding makes tests slow to write, slow to read, and slow to trust.

⚠️ When patterns grow faster than features, the design becomes ceremony rather than support. Keep the code useful first and shaped second.

Refactoring Toward Patterns

Patterns are easier to apply when they emerge from the code rather than being forced on it. In practice most pattern use begins with a rough but working structure, then small refactorings guide the design toward familiar shapes.

Recognizing opportunities

Repeated conditionals point toward Strategy. Long creation sequences point toward Builder. Ad hoc wrappers point toward Adapter. When code mutters the same idea in different places, that is usually a signal that a known pattern can concentrate the idea into one location.

A small refactor

This example begins with a block of repeated branching. The refactor extracts policy objects that follow a common interface, which turns the branching into a single call. The … marks removed detail.

def price(item, region):
  if region == "EU": return item.base + vat(item)
  if region == "US": return item.base + sales_tax(item)
  if region == "UK": return item.base + uk_tax(item)
  …

# refactor toward strategy

class EuTax: 
  def apply(self, item): …

class UsTax:
  def apply(self, item): …

class UkTax:
  def apply(self, item): …

def price(item, policy): return item.base + policy.apply(item)

Letting patterns settle naturally

The best designs evolve through steady reshaping. Rather than stamping a pattern onto freshly written code, let small refactorings pull the design into place. This keeps the code expressive without feeling overstructured. Patterns should feel like helpful scaffolding, not forced architecture.

Part 2: Beyond the Patterns

Chapter 11: The History and Evolution of Patterns

Design patterns did not appear from nowhere; they grew from earlier thinking about recurring forms that solve recurring problems. This chapter traces the path from architectural ideas to software catalogs, then follows the spread of patterns through languages and communities. The goal is context. When you know where patterns came from, you can apply them with clearer intent and lighter hands.

Origins

The roots of software patterns reach back to ideas in architecture where recurring design solutions were named, described, and composed to create places that feel coherent. The central insight was that a named solution is easier to discuss and easier to reuse. Software borrowed this approach and started to label proven design moves so teams could share them quickly without rederiving the same solutions every project.

Named Solutions

Once a solution carries a name, it becomes a tool in the shared vocabulary of a team. Names let developers point at structure and intent rather than drowning in raw code. A named pattern also captures tradeoffs. It tells you what you gain and what you give up, which helps when a system changes under your feet.

From Buildings to Code

Translating the idea of a pattern from physical space to software required a focus on relationships and roles. In code we model collaborators, protocols, and flows rather than bricks. The essential move is the same. You capture a proven arrangement and explain how to fit it to a new situation.

💡 Treat a pattern description as a template for thinking. Start with the core forces, then adjust the structure to your exact context.

The Gang of Four Moment

The first widely adopted catalog in software arrived as a compact list of core patterns with consistent names, a stable format, and straightforward diagrams. This gave developers a common map. It also encouraged discussion that moved beyond personal style toward shared structure.

Stabilising the Vocabulary

The catalog fixed names for patterns that already existed in scattered notes and codebases. With names came better communication, faster onboarding, and a way to reason about architecture at a higher level than individual methods. Teams could say “use Observer here” and be understood in seconds.

Format That Traveled

Each entry followed a regular outline: intent, motivation, structure, participants, collaborations, and consequences. The predictability mattered. Readers learned how to skim for what they needed and how to compare patterns fairly. The format itself became a pattern for describing patterns.

⚠️ A fixed catalog is a starting point, not a finish line. Treat it as a baseline and adapt details to your language, runtime, and constraints.

Patterns in Smalltalk

Early object systems such as Smalltalk offered fertile ground for exploring reusable design moves. The language model invited composition, late binding, and message passing; all three align well with pattern thinking. Many examples that later appeared in mainstream catalogs were first exercised in Smalltalk environments and teaching materials.

Message Passing And Roles

Patterns flourish when objects collaborate through simple messages and clear roles. Smalltalk highlighted this by keeping syntax minimal and making the messaging model explicit. You see the conversation directly, which makes it easier to reason about shapes such as Strategy and Observer.

A Tiny Sketch

This miniature shows the spirit rather than strict syntax. The focus is the conversation between a context and a strategy object; details inside braces are not central here and are elided with ….

class Context {
  setStrategy(s) { this.s = s }
  run(x) { return this.s.compute(x) }
}

class StrategyA {
  compute(x) { return x + 1 }
}

class StrategyB {
  compute(x) { return x * 2 }
}
💡 When a language encourages late binding and small objects, many patterns feel like natural conversations rather than heavy frameworks.

Why Patterns Exploded

Patterns spread quickly because they solved three social problems in software: they improved communication, they accelerated mentorship, and they reduced architectural wheel reinvention. The result was a shared shorthand for structure that crossed language and platform boundaries.

Communication At Speed

Names compress experience. Saying “wrap this with Decorator” communicates structure, intent, and likely tradeoffs in one breath. This saves hours of explanation and helps teams align on designs before they write code.

Mentorship In Print

A pattern write-up acts like a senior developer whispering advice to a reader. It captures accumulated lessons, including pitfalls and misuses. This makes patterns excellent teaching devices for developers who are new to large systems.

Pragmatic Reuse

Patterns are not libraries. They are design moves. You do not install them; you apply them. This made them travel well, since readers could adapt them inside any codebase without waiting for external dependencies.

How Languages Changed Patterns

Languages influence which patterns are needed and how heavy they feel. As features such as functions as values, modules, pattern matching, and generics became common, some classical patterns shrank or disappeared in practice. Others remained but changed shape to fit idioms of their hosts.

Idioms That Replace Patterns

In many modern languages a few built in features replace formal scaffolding. A first class function often replaces a small Strategy type; a module with simple state can replace a strict Singleton. The goal is the same outcome with less ceremony.

// Strategy as a function
function run(data, strat) { return strat(data) }
function sumStrategy(xs) { return xs.reduce((a, b) => a + b, 0) }
function maxStrategy(xs) { return Math.max(...xs) }
// usage: run([1,2,3], sumStrategy)
⚠️ Replacing a pattern with an idiom still requires clear boundaries. Keep names crisp and interfaces small, or the design will drift over time.

When Patterns Grow Lighter

Even when a pattern remains useful, language features can trim it. Interfaces, traits, lambdas, and data classes reduce boilerplate. A Factory Method can become a tiny function that returns a configured object; a Decorator can be a closure that wraps a callable and adds behavior around it.

Feature Influence Overview

This compact table lists common language features and the typical effect on classic patterns. It is not exhaustive; it is a quick map that helps you decide whether to keep a pattern formal or to translate it into a lighter idiom.

Language feature Effect on patterns Typical outcome
First class functions Reduces need for small interface types Strategy and Command become plain functions
Modules or packages Shared state without global objects Singleton replaced by a module with configuration
Pattern matching Direct shape based dispatch Visitor becomes a match over variants
Traits or interfaces Light polymorphism Decorator and Proxy stay but with less boilerplate
Generics Type safe reuse Factory Method and builders become concise and typed

Keeping The Spirit

Patterns are more durable than any one syntax. The spirit is to separate concerns, clarify collaborations, and make change safer. Use the lightest expression your language allows. If a module, closure, or data class expresses the idea cleanly, prefer it. If a formal shape communicates intent better to your team, keep it.

Chapter 12: How Patterns Interact

Patterns rarely live alone in real systems. They form small ecosystems where one pattern supplies structure, another supplies variation, and a third supplies coordination. This chapter explores several of these pairings. Each section focuses on how two patterns strengthen each other, why the pairing appears so often, and how to shape the interaction so it stays clear and maintainable.

Command and Memento

Command turns actions into objects, while Memento captures state so those actions can be reversed. Together they create undo and redo systems that behave predictably. The pairing works because actions and state snapshots complement each other; one describes what happened and the other preserves what must be restored.

Coordinated Roles

A Command stores just enough information to perform or reverse an operation. A Memento stores the internal state of the receiver. When used together, the command triggers changes and the memento anchors the state before and after those changes. This keeps the undo logic separate from business logic.

Compact Example

This demonstration uses simple placeholders inside braces to keep the focus on flow rather than implementation details. The command knows what to call; the memento holds the values needed to roll back the operation.

class Editor {
  constructor() { this.text = "" }
  type(s) { this.text += s }
  save() { return { text: this.text } }  // memento
  restore(m) { this.text = m.text }
}

class TypeCommand {
  constructor(ed, s) { this.ed = ed; this.s = s }
  execute() {
    this.before = this.ed.save()
    this.ed.type(this.s)
  }
  undo() { this.ed.restore(this.before) }
}
💡 Store only the minimum snapshot needed for rollback. Large snapshots slow down undo stacks and increase memory use.

Composite and Visitor

Composite represents hierarchical structures and lets clients treat groups and leaves uniformly. Visitor adds new operations to those structures without editing the existing classes. The interaction works because Composite exposes the shape of the tree and Visitor supplies pluggable behavior for walking it.

Why They Pair Well

Composite offers clean traversal. Visitor offers clean dispatch based on node type. When they meet, the traversal becomes predictable and the operations stay modular. You can add analytics, printing, exporting, or validation by swapping visitors without touching the tree structure.

Compact Example

This sketch shows nodes that accept a visitor. Internals that are not relevant are represented with … so that the mechanics of the interaction stay visible.

class Leaf {
  constructor(v) { this.v = v }
  accept(vis) { return vis.visitLeaf(this) }
}

class Group {
  constructor(children) { this.children = children }
  accept(vis) { return vis.visitGroup(this) }
}

class SumVisitor {
  visitLeaf(n) { return n.v }
  visitGroup(g) {
    return g.children.reduce((a, c) => a + c.accept(this), 0)
  }
}
⚠️ Visitor becomes heavy when node types change frequently. Each new node requires updates to every visitor.

Observer and Mediator

Observer broadcasts updates to subscribers, while Mediator centralises coordination among participants. Together they create systems with clear communication boundaries. Observer handles event flow; Mediator handles decision flow. This pairing appears wherever many components must stay informed without developing direct dependencies.

Balancing Broadcast and Control

Observer ensures that changes propagate. Mediator ensures that reactions remain orderly. Used together, they prevent the uncontrolled fan out that pure Observer setups sometimes suffer. The mediator becomes a traffic officer that listens to events and decides what should happen next.

Compact Example

This example shows a subject that emits events through simple callbacks. A mediator receives the event and coordinates the next steps. The inner details are trimmed where not essential.

class Subject {
  constructor() { this.subs = [] }
  subscribe(f) { this.subs.push(f) }
  setValue(v) {
    this.value = v
    this.subs.forEach(f => f(v))
  }
}

class Mediator {
  constructor() { this.count = 0 }
  onUpdate(v) {
    this.count += 1
    // coordinate …
  }
}

const s = new Subject()
const m = new Mediator()
s.subscribe(v => m.onUpdate(v))
💡 Use the mediator to keep event storms under control. It can throttle, batch, or filter notifications before they trigger real work.

Factory Method and Strategy

Factory Method selects which object to create, while Strategy selects how an object behaves. Combining them creates flexible systems where construction and behaviour vary independently. This pairing appears in configuration driven systems, plugin systems, and applications that support multiple execution modes.

Independent Axes Of Variation

The factory controls object type. The strategy controls behaviour. When you separate the two, you avoid nested conditionals that mix object selection with operational logic. The result is easier testing and a clearer mental model of the system.

Compact Example

This demonstration shows a factory that chooses between processors and a strategy that selects the algorithm each processor uses. The shapes are small so the flow remains clear, and placeholder content is marked with ….

class FastStrategy {
  run(xs) { return xs.reduce((a, b) => a + b, 0) }
}

class SafeStrategy {
  run(xs) { return xs.filter(x => x > 0).reduce((a, b) => a + b, 0) }
}

class Processor {
  constructor(strat) { this.strat = strat }
  process(xs) { return this.strat.run(xs) }
}

function makeProcessor(kind) {
  if (kind === "fast") return new Processor(new FastStrategy())
  return new Processor(new SafeStrategy())
}

// usage: makeProcessor("fast").process([1,2,3])
⚠️ Factories can become overloaded with configuration. Keep them small and let strategies capture variation so the selection logic remains simple.

Chapter 13: Pattern Alternatives and Lightweight Shapes

Classical patterns offer reliable structure, yet many languages include features that let you express the same intent with far less scaffolding. These alternatives are not replacements for the ideas behind patterns; they are more compact shapes that deliver the same effect when the surrounding language already does some of the heavy lifting. This chapter introduces several lightweight approaches and shows where they fit naturally.

Functions Instead of Strategy

Strategy defines a family of algorithms and lets a caller select one at runtime. In languages that support first class functions, the same effect can be achieved by passing a function directly. This reduces boilerplate and keeps the flow clear by lifting algorithm selection to the call site.

Reducing Ceremony

Wrapping a tiny algorithm inside a class often adds unnecessary structure. A function expresses intent immediately. It also becomes easier to test because you do not need to instantiate anything or mock collaborator fields.

Compact Example

This example uses simple functions to compute either the sum or the maximum of a list. The calling code receives a function and applies it as needed; the parts not essential to the concept are omitted with ….

function sum(xs) { return xs.reduce((a, b) => a + b, 0) }
function max(xs) { return Math.max(...xs) }

function run(xs, alg) { return alg(xs) }

// usage: run([1, 2, 3], sum)
💡 Keep passed functions pure where possible. Pure functions simplify reasoning and make testing effortless.

Modules Instead of Singleton

Singleton ensures a single global instance. Many languages already guarantee that a module or package is loaded only once, which makes modules a natural alternative. A module can hold configuration, state, or shared services without introducing explicit global object management.

Built In Lifetime Management

Modules provide their own loading rules. When you import a module, you receive the same instance every time. This gives you the core benefit of Singleton without the ceremony of private constructors or guarded instantiation logic.

Compact Example

This demonstration shows a module that stores configuration. When any part of the program imports the module, it sees the same state. Items inside the module are omitted with … where not important to the flow.

// config.js
export const settings = { debug: true }

// somewhere else
import { settings } from "./config.js"
console.log(settings.debug)
⚠️ Avoid storing large mutable structures in modules unless you manage their lifetime carefully. Shared state spreads through a system quickly.

Pattern Matching Instead of Visitor

Visitor separates operations from the data structures they walk. Modern pattern matching achieves a similar effect by dispatching based on shape directly, often with less code. This approach works well when node types are few and stable, and when clarity matters more than extending behaviour through multiple visitor classes.

Direct Dispatch

Pattern matching lets you express variant based logic in a compact form. Each case handles a known structure. This avoids the double dispatch required by Visitor and keeps all related logic in one place.

Compact Example

Here a structure contains files and folders. The matching expression handles each shape explicitly. The example trims internal details, using … where the specifics are unimportant.

function size(node) {
  switch (node.type) {
    case "file":
      return node.size
    case "folder":
      return node.items.reduce((a, c) => a + size(c), 0)
    default:
      throw new Error("unknown")
  }
}
💡 Pattern matching shines when the set of variants is stable. If you add new node types often, a full Visitor may offer clearer structure.

Decorators Instead of Template Method

TemplateMethod defines a fixed algorithm and lets subclasses override steps. In languages that support decorator functions or wrapper objects, you can inject behaviour by composition rather than inheritance. This keeps the main algorithm visible and allows small changes without subclassing.

Composition Over Extension

Decorators wrap a function or method and add behaviour before or after the core call. This provides fine grained control without altering the underlying type hierarchy. It is also easier to mix multiple behaviours because decorators can be layered.

Compact Example

This example wraps a simple function with a logging decorator. Details not central to the idea are marked with … for clarity.

function log(fn) {
  return function(...args) {
    console.log("calling", fn.name)
    return fn(...args)
  }
}

function greet(name) { return "hi " + name }

// usage: const wrapped = log(greet); wrapped("Ada")
⚠️ Too many decorators in a chain can obscure control flow. Keep layers small and focused.

Data Classes Instead of Abstract Factory

AbstractFactory creates families of related objects. In many languages data classes or record types can express simple product families in a more lightweight way. When products share only basic fields and behaviour is minimal, constructing them directly is cleaner than adding factory hierarchies.

Lightweight Construction

Data classes provide default constructors, equality checks, and readable structure. This reduces the need for factories when your primary requirement is to generate well formed data objects. If behaviour increases later, you can introduce more structure naturally.

Compact Example

This sketch shows a theme selection that returns different data shapes without formal factory interfaces. Content not critical to the concept is represented with ….

class Button {
  constructor(label, theme) {
    this.label = label
    this.theme = theme
  }
}

function makeButton(theme) {
  return new Button("OK", theme)
}

// usage: makeButton("dark")
💡 If object combinations become more complex later, you can reintroduce a small factory to keep creation rules consistent.

Chapter 14: Testing and Patterns

Design patterns often behave like quiet stagehands in the background of a test suite. They can lift scenes into clearer view or tug ropes so hard that everything wobbles. This chapter explores how patterns shape the structure of tests, how they help or hinder isolation, and how to keep your test code from turning into a cluttered attic of tangled mocks.

Patterns That Improve Testability

Some patterns open windows in your design so that tests can see inside without leaning on awkward scaffolding. These patterns support separation, allow substitution, and provide controlled seams where test objects slide neatly into place. Clear boundaries help a test suite move with grace rather than feeling like a bulky robot trying to tiptoe.

Strategy for Swap Friendly Logic

The Strategy pattern invites you to swap behaviours at runtime. This creates an ideal spot for injecting a stub or fake during a test. When behaviour is externalised like a removable puzzle piece, you can examine a class without dragging the real behaviour along for the ride.

Adapter for Predictable Interfaces

An Adapter gives you a stable surface to grip during tests. Even when the underlying system has quirks, the adapter wraps those quirks in a consistent shape. A test can then focus on the contract presented by the adapter rather than the unpredictable details hidden behind it.

💡 A good adapter lets you write tests that read like small stories rather than long manuals filled with vendor specific oddities.

Patterns That Hinder Testing

Some patterns build walls instead of windows. These designs trap logic in lifetimes that are hard to poke, or they encourage sprawling behaviour that tests struggle to isolate. The result feels like trying to test a clock while all the gears spin at once.

Singleton and Hidden Dependencies

The Singleton pattern promotes a global instance that sticks around like glue. This makes tests run into shared state, which spreads unpredictability. Resetting the world between tests becomes a chore, and subtle interactions creep in like dust between gears.

Facade With Overgrown Responsibilities

A Facade that tries to do everything creates a testing challenge because its behaviour becomes wide and shallow. A test must dance around too many branches at once. This often leads to bloated mocks, which become difficult to maintain.

Dependency Injection for Isolation

Dependency injection lets you slip testing pieces into the gaps between layers. Instead of creating objects deep inside a class, you pass them in from the outside so tests can feed in their own controlled versions. This helps each class become a small island that can be inspected without disturbing the surrounding sea.

Constructor Injection for Stable Tests

Constructor injection provides a clear path for objects that a class relies on. Tests gain the ability to wire predictable fakes in place, which keeps behaviour steady. This approach helps keep surprises out of your test runs.

Setter Injection for Flexible Scenarios

Setter injection gives you a way to slip in dependencies after object creation. This approach can feel more flexible, although you must be careful to avoid leaving objects in half finished states. Tests benefit from the ease of swapping collaborators when trying varied scenarios.

⚠️ Setter based injection only works cleanly if the class guarantees some sensible default or validates that required dependencies are in place before use.

Using Spies With Observer

When events ripple through an Observer structure, spies give you a clear record of what happened. A spy listens like a quiet clerk taking notes. Rather than pretending to perform real behaviour, it collects calls, arguments and invocation order so your test can confirm that notifications flowed correctly.

Tracking Notifications

A spy observer records each event it receives. Later the test can inspect these records to ensure the sequence of calls matches expectations. This helps verify that the subject broadcasts exactly what it should without reordering or skipping steps.

Testing Unsubscribe Behaviour

Observers can come and go like swallows passing over a lake. A spy helps you check that unsubscribed observers truly stop receiving messages. This keeps your system from sending unnecessary signals into empty space.

Testing Template Method Sequences

The Template Method pattern defines a skeleton with customisable steps. Tests must verify that this skeleton is followed correctly. You can use small subclasses or spies inserted into specific hooks to track calls as they unfold. This sequential focus lets you confirm that the template is honoured.

Checking Required Steps

Some steps in a template are required for correctness. A test ensures these steps run in the intended order. By keeping each hook’s behaviour small and clear, the test can examine output without distraction.

Verifying Optional Hooks

Optional hooks allow flexibility. Tests must confirm that they are skipped when unused and invoked when implemented. This keeps behaviour consistent for different subclasses that plug into the same template.

Anti Patterns That Damage Test Suites

Anti patterns can sink a test suite with surprising weight. They mix concerns, hide state and wrap logic inside shapes that tests cannot grip. A healthy design avoids such pitfalls to keep the test environment predictable.

Hidden Work in Getters

Getters that sneak extra behaviour into a simple read operation surprise both code and tests. When a test expects a straightforward value but discovers computation, it becomes harder to maintain. Predictable interfaces help keep testing focused.

💡 A getter should act like a pane of glass rather than a puzzle box. Transparency makes tests easier to reason about.

Long Methods With Entangled Branches

Long methods force tests to dig through complicated paths. Each branch becomes a separate scenario that needs careful setup. Splitting behaviour into smaller units helps tests become lighter and more direct.

Chapter 15: Refactoring Toward Patterns

Refactoring toward patterns is not about forcing shapes onto code. It is about revealing the design that was already trying to speak. In this chapter we take realistic code smells and reshape them into clear structures using Strategy, Observer, Facade and dependency injection DI. Each section shows how to move step by step, keeping tests green while the design grows simpler.

Evolving Conditionals Into Strategy

Large conditional blocks are like tall hedges that hide small pathways. The Strategy pattern trims those hedges so each path stands on its own. We replace nested conditionals with interchangeable behaviours that share a clean interface.

Recognise the Smell

You often see a tangle of if and elif statements around the same decision. Each branch tweaks similar values and returns related results. This repetition signals that a single concept is wearing many costumes.

Extract the Policy Interface

Define a small contract that captures what varies. The client should depend on a calculate or apply shaped method rather than many branches. The name of the method frames intent and guides the refactor.

Move Branch Logic Into Strategies

Create one class per former branch. Keep each class tiny. If the behaviours share helpers, pull those up into a common utility. Tests help confirm that each variant produces the same outputs as the original branch.

# Before
def shipping_cost(order, region):
  if region == "domestic":
    return order.weight * 1.0
  elif region == "eu":
    return order.weight * 1.3 + 5
  elif region == "intl":
    return order.weight * 1.8 + 12
  return 0
# After: Strategy
class ShippingStrategy:
  def cost(self, order): raise NotImplementedError

class Domestic(ShippingStrategy):
  def cost(self, order): return order.weight * 1.0

class EU(ShippingStrategy):
  def cost(self, order): return order.weight * 1.3 + 5

class International(ShippingStrategy):
  def cost(self, order): return order.weight * 1.8 + 12

class Shipper:
  def __init__(self, strategy: ShippingStrategy):
    self.strategy = strategy
  def cost(self, order): return self.strategy.cost(order)
💡 Keep the interface tiny. A single method on the Strategy is often enough; more methods suggest multiple strategies or a forgotten helper.

Wire With Factories or DI

Construct the right Strategy at the edges of the system. Use a small factory or supply the strategy via DI. The core stays branch free while composition chooses behaviour.

Growing a Notifier Into Observer

Ad hoc callbacks start simple then turn into vines. The Observer pattern trains those vines onto a trellis. We introduce a subject that manages subscribers, and observers that react to events without tight coupling.

Identify Implicit Subscribers

Look for code that calls a hard coded list of handlers or toggles flags for other components. These are observers in disguise. They want a formal subscribe and notify flow.

Introduce Subject and Subscription API

Create a subject that exposes subscribe, unsubscribe and notify. Move direct calls to go through the subject. Keep payloads small and well named so tests read cleanly.

# Before
def save(user):
  db.write(user)
  analytics.track("user_saved", {"id": user.id})
  cache.invalidate(f"user:{user.id}")
# After: Observer
class UserSavedSubject:
  def __init__(self): self._observers = []
  def subscribe(self, obs): self._observers.append(obs)
  def unsubscribe(self, obs): self._observers.remove(obs)
  def notify(self, event): 
    for o in list(self._observers): o.update(event)

class AnalyticsObserver:
  def update(self, event): analytics.track("user_saved", {"id": event["id"]})

class CacheObserver:
  def update(self, event): cache.invalidate(f"user:{event['id']}")

def save(user, subject: UserSavedSubject):
  db.write(user)
  subject.notify({"id": user.id})
⚠️ Keep events stable. Changing event shapes breaks observers silently. Version the payload or add fields with defaults to protect consumers.

Test With Spies

Attach a spy observer that records calls and arguments. Verify order and count. This confirms that the subject only broadcasts the intended sequence and that unsubscribed observers no longer receive updates.

Breaking Down a God Object

The God Object hoards Knowledge, Orchestration and Storage. Untangling it is like moving from a cluttered garage to labelled shelves. We slice by responsibility, tease apart data and behaviour, then reconnect pieces through clear interfaces.

Map Responsibilities

List everything the object knows and does. Group by capability such as validation, persistence, calculation and coordination. This map becomes your cut plan.

Extract Cohesive Types

For each group, create a small type with a single focus. Move methods and the fields they truly need. Leave behind only what still belongs. Aim for crisp names that match the capability, not the old class.

# Before: one giant class UserService {...} handles …
# After: split into UserRepository, PasswordPolicy, ProfileComposer, UserService

Rewire Through DI and Facades

Let the high level service depend on interfaces for its new collaborators. Supply them via DI. If callers previously touched the giant surface, place a small Facade in front that routes to the new parts while clients slowly migrate.

💡 Move behaviour in thin slices with tests after each slice. Many small steps keep the code base calm and predictable.

Blending Facade, Strategy, and DI

Complex subsystems benefit from a welcoming entry point plus pluggable behaviours underneath. A Facade presents a tidy surface, Strategy provides variation, and DI composes the pieces without hidden construction.

Define the Facade Contract

Capture the most common tasks behind a simple API. The facade hides wiring and advanced options while still allowing access to the underlying parts for expert paths.

Insert Strategies Behind the Facade

Where behaviour varies, depend on a Strategy interface. The facade delegates to the selected strategy. This avoids new conditionals as features grow and keeps tests focused on single behaviours.

Compose With DI at the Edges

Construct concrete strategies and collaborators at composition time. Inject them into the facade. Tests can supply fakes and spies to verify orchestration without invoking heavy dependencies.

# Sketch
class ImageFacade:
  def __init__(self, loader, saver, scaler_strategy):
    self.loader = loader
    self.saver = saver
    self.scaler = scaler_strategy
  def resize(self, path, out, size):
    img = self.loader.load(path)
    img2 = self.scaler.scale(img, size)
    self.saver.save(img2, out)
⚠️ Keep the Facade small. If it starts sprouting rarely used methods, split it by use case and expose multiple focused facades.

Chapter 16: Pattern Case Studies

Patterns shine when dropped into real systems. In this chapter we explore how common software domains lean on different design patterns and why certain shapes appear again and again. Each case study shows how patterns support clarity, simplify change and keep complexity from creeping through the cracks.

User Interface Frameworks

User interface frameworks deal with constant motion. Buttons fire events, screens shift, and users nudge the system into countless tiny state transitions. Patterns help those movements stay readable. Most major UI toolkits borrow ideas from Observer, Mediator, Command and Composite to keep interactions flowing smoothly.

Observer for Interactive Signals

A button press or slider change emits a signal. The Observer pattern turns that signal into a stream of events for listeners. Frameworks use this to keep input handling independent from rendering or logic. A clear event path helps keep screens responsive.

Composite for Nested Widgets

UI elements nest inside larger containers. The Composite pattern gives each widget a shared interface so layouts can treat children uniformly. This makes it easier to build trees of widgets such as panels that hold labels, text fields and controls.

💡 Many toolkits store layout rules in small objects rather than hard coded checks. This echoes the Strategy pattern and keeps layout engines flexible.

REST API Design

REST APIs often hide complex behaviour behind clean endpoints. Patterns help convert messy business logic into predictable flows. Facade, Strategy and Decorator appear frequently as APIs adapt to scaling and versioning while staying readable.

Facade for Endpoint Boundaries

An endpoint acts like a small facade that takes input, validates it and routes it to a domain service. This isolates protocol details and keeps HTTP concerns from leaking into core logic. A stable boundary also makes APIs easier to evolve.

Decorator for Middleware

Middleware functions wrap behaviour in layers. Each layer adds cross cutting concerns such as logging or authentication. The Decorator pattern appears here because each wrapper keeps the same interface while adding a new responsibility.

⚠️ Excessive middleware nesting slows clarity. If a chain feels tangled, merge layers or use a single composed policy to regain simplicity.

Game Engines

Game engines juggle rendering pipelines, physics calculations and user actions at high speed. Patterns keep this energetic environment coherent. Flyweight trims memory, Command sequences actions, and Observer synchronises state changes without knotting systems together.

Flyweight for Repeated Assets

Games reuse textures, meshes and sounds constantly. The Flyweight pattern stores shared data once and attaches small state objects for per instance details. This reduces memory use and keeps loading times reasonable.

Command for Input Actions

Input systems often map key presses to small Command objects that perform actions. This keeps control schemes configurable and simplifies undo or replay features. It also supports recording sequences to test gameplay logic.

Document Editors

Editors weave together text, formatting, undo history and plugins. Patterns provide the backbone that keeps all these concerns steady. Command, Memento, Composite and Visitor appear often because they support structured content and editing flows.

Command and Memento for Undo

The undo stack relies on two key ideas. Command represents each action while Memento captures the state needed to reverse it. This pairing creates predictable multi step undos and keeps history stable even as documents grow.

Composite for Document Structure

Documents often form a tree of chapters, paragraphs and spans. A Composite keeps this hierarchy manageable by giving each node a uniform interface. Rendering and analysis can then walk the tree without specialised branching.

💡 Some editors use Visitor to run spellchecking and formatting analysis across the tree without cluttering node classes with extra responsibilities.

Plugin Systems

Plugin architectures breathe flexibility into software. They allow external modules to extend behaviour without altering the core. Patterns such as Factory Method, Adapter and Strategy help keep this system consistent and safe as plugins evolve.

Factory Method for Plugin Discovery

Plugins often register themselves or expose metadata through a small factory. The core asks a known method for an instance, keeping construction predictable. This supports dynamic loading without messy conditional chains.

Adapter for Compatibility Layers

Plugins may come from different ecosystems. An Adapter converts their interfaces into the canonical shape expected by the host. This protects the core from divergent APIs and lets plugin authors focus on their feature rather than integration details.

⚠️ Avoid leaking plugin details into unrelated parts of the system. The whole point of a plugin boundary is separation. Keeping adapters narrow preserves that separation.

Chapter 17: Pattern Selection Heuristics

Choosing a pattern is less like picking a tool from a tidy box and more like picking the right lens for a camera; the same scene can look crisp or muddy depending on the lens. The following heuristics help you decide when a pattern fits, when to reshape the problem, and when to step away from patterns entirely. Use these as guides; prefer clarity over ceremony.

When to Choose Strategy

Strategy is a fit when behavior varies by policy, rule, or environment and you want to swap that behavior without changing the host object. If you can write “do the same thing, but with a different algorithm” and keep the same inputs and outputs, you are in Strategy country. It is also useful when testing needs to stub out complex logic with a simpler stand-in; plug a fake strategy in and keep the host untouched.

Heuristics for Strategy

Choose Strategy when you see conditional clusters that only differ in computation, not in data shape. If clients should not know which algorithm runs, supply them with a stable interface such as calculate(…) or render(…). If the number of variants is open-ended, prefer a registry or factory for strategies instead of sprawling if trees.

Anti-signals

Avoid Strategy if variants require incompatible state models or wildly different method signatures; you will end up with adapter glue around each strategy which suggests the problem is not a single axis of variation. If only two strategies exist and will never grow, a simple function parameter or a callable may be clearer.

💡 Small, pure strategies are easy to test. Keep shared state out of strategies; pass all inputs to run(…) so each variant remains independent.

Tiny example

This shows swapping a pricing algorithm without touching the order pipeline.

class PricingStrategy:
  def price(self, order): return 0.0

class StandardPricing(PricingStrategy):
  def price(self, order): return sum(i.qty * i.unit for i in order.items)

class PromoPricing(PricingStrategy):
  def price(self, order): return 0.9 * sum(i.qty * i.unit for i in order.items)

class Checkout:
  def __init__(self, pricing): self.pricing = pricing
  def total(self, order): return self.pricing.price(order)

Adapter Versus Rewriting

Adapter helps when you must integrate an incompatible API without changing it. Rewriting helps when the foreign API is small, unstable, or low quality. The choice pivots on expected lifetime, ownership, and coupling. If the vendor code will outlive your app, adapt it. If the code is thin and you own it, rewriting can simplify your domain and remove long-term glue.

Cost–benefit quick table

This table frames common trade-offs. Treat it as a compass, not a contract.

Context Lean Adapter Favor Rewrite
Third-party API is stable and supported ✔ Interface shield for your domain ✖ Costly and risky
Third-party API is volatile or poorly designed ✖ Adapters multiply and fossilize oddities ✔ Clean, domain-first interface
Performance bottleneck in marshaling ✖ Extra layers add overhead ✔ Rewrite to native types
Time to deliver is very limited ✔ Thin shim buys time ✖ Rewrite may slip schedule

Adapter sketch

Here a legacy payment client is wrapped to match your gateway interface. The adapter isolates odd field names like {"cc_no": …}.

class LegacyPayClient:
  def send(self, payload): return {"ok": True, "id": "abc123"}

class PaymentGateway:
  def charge(self, card, amount): raise NotImplementedError

class LegacyAdapter(PaymentGateway):
  def __init__(self, client): self.client = client
  def charge(self, card, amount):
    data = {"cc_no": card.number, "amt": amount.cents, "cur": amount.currency}
    resp = self.client.send(data)
    return resp["ok"], resp.get("id")
⚠️ Avoid cascading adapters. If you need FooAdapter for BarAdapter for Baz, you are accumulating structural debt. Consider a targeted rewrite or a façade that presents a single clean surface.

Decorator Versus Inheritance

Use Decorator to add behavior to specific instances without touching their class. Prefer it when features are optional, orthogonal, and can stack in different orders such as logging, caching, or throttling. Use inheritance when the variation is intrinsic to the type and all instances of the subtype must share it.

Heuristics for Decorator

If you can read “base behavior stays the same and extra behavior wraps before or after it,” choose Decorator. When combinations matter, such as Cache then Retry versus Retry then Cache, instance-level stacking beats subclass explosions like LoggedCachingRetryingService.

Inheritance still wins here

Inheritance is simpler when the variation changes the core contract or representation. Examples include Square as a special case of Rectangle that enforces equal sides, or UnsignedInteger that forbids negatives at the type level. If a feature must exist for all instances, make it part of the class rather than a wrapper.

class Repo:
  def get(self, key): return load_from_db(key)

class LoggingRepo(Repo):
  def __init__(self, inner): self.inner = inner
  def get(self, key):
    print(f"GET {key}")
    return self.inner.get(key)
💡 Keep decorators transparent. Forward every method you do not change and avoid changing return shapes. If you must transform results, consider a façade with an explicit contract.

When Composite Fits

Composite unifies treatment of parts and wholes. Use it when you have recursive hierarchies such as UI trees, file systems, or nested expressions and you want clients to call the same methods on both leaves and groups. The key signal is a tree with behavior that aggregates naturally, for example render(), evaluate(), or size().

Heuristics for Composite

If you find yourself asking “is this a leaf?” before every operation, reach for Composite. If your API can be uniform, define it on a shared interface and let groups forward calls to children. If ordering or constraints matter, let the composite enforce them so clients do not have to juggle sequence rules.

class Node:
  def render(self): raise NotImplementedError

class Text(Node):
  def __init__(self, s): self.s = s
  def render(self): return self.s

class Group(Node):
  def __init__(self): self.children = []
  def add(self, n): self.children.append(n)
  def render(self): return "".join(c.render() for c in self.children)
⚠️ Guard against cycles. A composite should be a true tree. Add checks when calling add(…) so a node cannot be added to one of its descendants.

When Observer Causes Trouble

Observer looks simple yet often misbehaves in production. The most common problems are update storms, unpredictable ordering, memory leaks from forgotten subscriptions, and hidden coupling. If state changes ripple through many observers, small tweaks can trigger large and surprising effects.

Risk signals

Be cautious if observers mutate the subject or each other, if ordering matters to correctness, or if subscriptions lack a lifetime policy. Synchronous notifications can cause deep call chains and stack churn. Asynchronous notifications can reorder events and surface race conditions.

Mitigations

Prefer immutable payloads for notifications so observers cannot fight over shared state. Use unsubscribe() or context managers to tie lifetime to scope. If ordering matters, define it explicitly in the subject and keep it stable. For high-volume signals, batch updates or debounce.

class EventBus:
  def __init__(self): self._subs = {}
  def on(self, topic, fn): self._subs.setdefault(topic, []).append(fn); return lambda: self._subs[topic].remove(fn)
  def emit(self, topic, data): 
    for fn in list(self._subs.get(topic, [])): fn(data)  # copy to avoid mutation issues
💡 Consider a queue plus a single dispatcher for hot paths. Queueing flattens call depth, enables batching, and gives you a single place to log or meter events.

Chapter 18: A Small Pattern Driven Project

This chapter walks through a compact yet realistic project that uses a handful of patterns with purpose rather than ceremony. We will sketch a feature slice end to end and show where each pattern pays its rent. The aim is clarity; every pattern must earn its place.

Project Overview

We will build a tiny digital bookstore slice that supports adding items to a cart, choosing a pricing policy, placing an order, and notifying interested parties. The scope includes a domain core, a minimal persistence seam, and a simple service boundary. We will not cover authentication or deployment; focus stays on design choices that shape maintainability.

Functional goals

Users add books to a cart, view totals, apply a promotion, and confirm the order. On confirmation the system charges a payment gateway and emits notifications to subscribers. Failures should not corrupt the cart or order state.

Non-functional goals

Keep business rules testable without infrastructure, allow pricing rules to vary, and isolate the external payment provider so it can be swapped without touching core code.

💡 Keep the slice thin. A small, vertical cut across the UI, service, domain, and infrastructure reveals pattern seams without drowning in scaffolding.

Core Components

The design centers on a handful of focused types with narrow contracts. Names are illustrative rather than canonical. Keep each component small and stable.

Cart and Order

Cart collects line items and exposes totals. Order captures a snapshot of the cart at confirmation time and holds immutable details needed for charging and fulfillment.

PricingStrategy

Different pricing policies implement a shared interface, for example price(…). Strategies swap freely without touching the cart or order code.

PaymentGateway and PaymentAdapter

PaymentGateway defines the contract we want. PaymentAdapter wraps a third-party client and translates between our types and its payloads such as {"cc_no": …} and {"amt": …}.

OrderService and DomainEvents

OrderService orchestrates checkout. DomainEvents provides a tiny observer style bus so listeners can react to order confirmation, for example to send email or to update analytics.

Patterns in the Design

Each pattern maps to a concrete need, not a fashion choice. The table summarizes the role and the tradeoffs to watch.

Pattern Placed In Why It Fits Watch Out For
Strategy PricingStrategy Swap pricing rules without touching cart or order Overkill if only one rule will ever exist
Adapter PaymentAdapter Shield the domain from a vendor API Do not leak vendor types into signatures
Decorator GatewayWithRetry Add retry or logging around a gateway instance Order of wrappers can change behavior
Observer DomainEvents Notify subscribers on order confirmed Unbounded fan-out can cause storms
Facade OrderService Single entry point for checkout workflow Keep it thin; avoid god-class drift

Why not Singleton or Service Locator

Global access hides dependencies and harms testability. Prefer constructor injection on OrderService and explicit wiring in composition roots for clarity.

Minimal Implementation Sketch

The following sketch is compact by design. It shows how the parts fit together without dictating a framework or database. Replace with your infrastructure of choice.

from dataclasses import dataclass

@dataclass(frozen=True)
class Money:
  cents: int
  currency: str = "USD"
  def __add__(self, other): return Money(self.cents + other.cents, self.currency)

@dataclass
class LineItem:
  sku: str
  qty: int
  unit_cents: int

class Cart:
  def __init__(self): self.items = []
  def add(self, sku, qty, unit_cents): self.items.append(LineItem(sku, qty, unit_cents))
  def subtotal(self): return Money(sum(i.qty * i.unit_cents for i in self.items))

class PricingStrategy:
  def price(self, cart): return cart.subtotal()

class TenOffOverHundred(PricingStrategy):
  def price(self, cart):
    sub = cart.subtotal()
    return Money(sub.cents - 1000, sub.currency) if sub.cents >= 10000 else sub

class PaymentGateway:
  def charge(self, card, amount): raise NotImplementedError

class VendorClient:
  def send(self, payload): return {"ok": True, "id": "tx_…"}

class PaymentAdapter(PaymentGateway):
  def __init__(self, client): self.client = client
  def charge(self, card, amount):
    payload = {"cc_no": card["number"], "amt": amount.cents, "cur": amount.currency}
    r = self.client.send(payload)
    return r["ok"], r.get("id")

class GatewayWithRetry(PaymentGateway):
  def __init__(self, inner, attempts=3): self.inner, self.attempts = inner, attempts
  def charge(self, card, amount):
    last = (False, None)
    for _ in range(self.attempts):
      ok, tx = self.inner.charge(card, amount)
      if ok: return True, tx
      last = (ok, tx)
    return last

class DomainEvents:
  _subs = {}
  @classmethod
  def on(cls, topic, fn): cls._subs.setdefault(topic, []).append(fn)
  @classmethod
  def emit(cls, topic, data):
    for fn in list(cls._subs.get(topic, [])): fn(data)

@dataclass(frozen=True)
class Order:
  id: str
  total: Money
  lines: tuple

class OrderService:
  def __init__(self, pricing: PricingStrategy, gateway: PaymentGateway):
    self.pricing, self.gateway = pricing, gateway
  def checkout(self, cart, card):
    total = self.pricing.price(cart)
    ok, tx = self.gateway.charge(card, total)
    if not ok: raise RuntimeError("payment failed")
    order = Order(id=tx, total=total, lines=tuple(cart.items))
    DomainEvents.emit("order.confirmed", {"id": order.id, "total": order.total.cents})
    return order

# wiring & sample flow
cart = Cart(); cart.add("BOOK-001", 2, 3500); cart.add("BOOK-002", 1, 1200)
pricing = TenOffOverHundred()
gateway = GatewayWithRetry(PaymentAdapter(VendorClient()))
DomainEvents.on("order.confirmed", lambda e: print(f"confirmed {e['id']} for {e['total']}"))
order = OrderService(pricing, gateway).checkout(cart, {"number": "4111 1111 1111 1111"})
⚠️ Bind event handlers at the edge and keep payloads immutable. Mutable shared state across observers invites race conditions and spooky action at a distance.

Reflection on Tradeoffs

Strategy keeps business rules swappable and testable; this is worth it when promotions multiply. Adapter prevents vendor types from leaking into the domain; this reduces migration pain later. Decorator lets reliability concerns like retry or logging wrap a gateway instance without subclass sprawl. Observer decouples side effects such as email from the happy path; this helps throughput yet demands care with ordering and volume control. If the project shrinks or stabilizes, you can inline strategies or drop the bus for direct calls. Patterns serve the story; they do not write it.

Chapter 19: Summary, Glossary, and Pattern Map

This chapter gathers the main threads of the book into a single place. Think of it as a final lap around the track where each pattern, principle, and habit shows its shape one more time. The goal is clarity: a clean recap, a tight glossary, and a simple map you can carry into your future projects.

Summary of Key Ideas

Patterns are vocabulary. They help developers talk about structure, tradeoffs, and intent without drowning in details. The power does not come from memorizing diagrams but from recognizing recurring forces in real code such as variation in behavior, tension between convenience and flexibility, and boundaries between stable and shifting parts. Patterns help you design with purpose rather than accident.

Patterns as lenses

A pattern highlights one path through a design problem. Strategy separates the algorithm from the host. Adapter bridges mismatched APIs. Decorator adds optional or layered behavior. Observer signals change across collaborators. Composite unifies treatment of parts and wholes. Each solves a different kind of discomfort that appears in growing systems.

Tradeoffs matter

No pattern is free. Observer can create fan-out storms. Decorator can collapse under too many layers. Adapter can fossilize legacy shapes. Good design comes from understanding these costs and balancing them with the benefits. Patterns guide thinking; they do not replace it.

💡 When unsure whether a pattern fits, write the simplest version of the feature and read the code aloud. If it sounds like a rule you repeat often, you may have found a place for a pattern.

Glossary of Terms

This glossary aims for clarity over formality. Each entry keeps to the core idea so you can recall the meaning quickly when scanning code or discussing design choices.

Key entries

⚠️ Glossaries help, but always read terms in context. A pattern name alone is never enough to reveal intent without the surrounding story.

Pattern Map

The pattern map below groups families by the kind of problem they solve. Treat it as a trail guide. When a project grows or changes shape, revisit the map and see which forces are active.

By design pressure

Pressure Patterns
Varying behavior Strategy, State
Incompatible interfaces Adapter, Facade
Optional or layered behavior Decorator
Hierarchies and trees Composite, Visitor
Coordination among many objects Observer, Mediator
Object creation rules Factory Method, Abstract Factory, Builder, Prototype

How to use the map

Start with the pressure you feel in the code. Identify what hurts: too many conditionals, tangled dependencies, or data shaped wrong for a library. Follow the map to candidate patterns and test them in small experiments. If the code becomes simpler, lean in. If it becomes heavier, roll back.

Next Steps

You now have a working vocabulary for design conversations and a set of tools for shaping growing systems. Real mastery comes from practice: refactoring small projects, reading codebases that model different domains, and watching how patterns evolve as requirements shift. Keep patterns in your pocket, not on a pedestal, and let them help you build clear, adaptable, and human-friendly software.



© 2025 Robin Nixon. All rights reserved

No content may be re-used, sold, given away, or used for training AI without express permission

Questions? Feedback? Get in touch