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.
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.
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.
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.
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
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()
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"}
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")
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))
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)
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]
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()]
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()}"
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"})
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()
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({})
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")
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)
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)
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()
(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])
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()
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())
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")
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")
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": "…"}})
{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()
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"])
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)
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")
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 }
}
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)
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) }
}
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)
}
}
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))
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])
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)
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)
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")
}
}
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")
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")
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.
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.
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.
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)
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})
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.
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)
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.
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.
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.
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.
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.
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")
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)
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)
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
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.
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"})
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.
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
- Abstraction: A simplified view of something complex, usually represented by an interface or shared contract.
- Adapter: A wrapper that makes one interface look like another so existing code can use it.
- Composite: A structure where parts and groups share the same interface, usually forming a recursive tree.
- Decorator: A wrapper that adds behavior around another object without changing its class.
- Encapsulation: Keeping internal details hidden so only a stable surface is exposed.
- Facade: A simple entry point that hides a cluster of underlying services.
- Interface: A contract describing what methods exist and what they mean.
- Memento: A snapshot of an object’s state used for undo or state restoration.
- Observer: A subscription model where listeners react to changes emitted by a subject.
- Prototype: A pattern that clones a preconfigured instance for quick object creation.
- Refactoring: Improving structure without changing behavior.
- Strategy: A family of algorithms that share a contract and can be swapped at runtime.
- Template Method: A skeleton method with hooks for subclasses to fill in.
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