Design Patterns
Purpose
Apply structural patterns as solutions to problems you actually have. A pattern applied prophylactically is just indirection with a formal name.
When to Use
- A problem recurs and the current structure requires a change in many places each time.
- Choosing between inheritance, composition, and configuration.
- Reviewing code that is heavily abstracted and hard to follow.
- Designing an extension point for third parties.
Capabilities
- Creational: factory, builder, dependency injection.
- Structural: adapter, facade, decorator, proxy.
- Behavioral: strategy, observer, command, state machine.
- Concurrency: producer-consumer, worker pool, circuit breaker.
- Recognizing pattern misuse: the singleton-as-global, the abstract factory of one.
Inputs
- The problem being solved and how often it recurs.
- The axis of expected change — what varies, and what stays fixed.
- Who extends the code: you, your team, or external consumers.
Outputs
- A structure with a named rationale.
- The specific change it makes cheap, stated explicitly.
Workflow
- Name the axis of variation. A pattern buys flexibility along exactly one axis, at the cost of indirection along all of them. If you cannot name the axis, do not apply a pattern.
- Count the instances. One implementation needs no strategy interface. Two might. Three definitely does.
- Choose the lightest structure that works. A function passed as a parameter is a strategy. It does not need a class hierarchy.
- Write it concretely first. Then extract the pattern when the second case arrives — the shape of the abstraction will be obvious rather than guessed.
- Justify the indirection. If a reader must open three files to answer "what happens when I call this", the pattern is costing more than it returns.
Best Practices
- Composition over inheritance, nearly always. Inheritance couples you to the parent's internals forever.
- Strategy, adapter, and decorator earn their keep constantly. Abstract factory and visitor rarely do outside frameworks.
- A singleton is a global variable with better manners. Use dependency injection instead — it makes the dependency visible and the code testable.
- A state machine with explicit states and transitions beats a tangle of boolean flags every time.
- Circuit breakers, retries, and timeouts are patterns too, and they are more likely to save you than any of the Gang of Four.
- Two similar pieces of code are not necessarily duplication. Deduplicating things that merely look alike couples things that change for different reasons.
Examples
Explicit state machine instead of boolean soup:
# Before: state is implied by three booleans, and half the combinations are invalid.
if order.is_paid and not order.is_shipped and not order.is_cancelled:
ship(order)
# After: states are enumerated, transitions are the only way to move.
class OrderState(Enum):
PENDING = auto()
PAID = auto()
SHIPPED = auto()
CANCELLED = auto()
TRANSITIONS: dict[OrderState, set[OrderState]] = {
OrderState.PENDING: {OrderState.PAID, OrderState.CANCELLED},
OrderState.PAID: {OrderState.SHIPPED, OrderState.CANCELLED},
OrderState.SHIPPED: set(),
OrderState.CANCELLED: set(),
}
def transition(order: Order, to: OrderState) -> None:
if to not in TRANSITIONS[order.state]:
raise InvalidTransition(f"{order.state.name} -> {to.name}")
order.state = to
Invalid states are now unrepresentable, and the legal transitions are readable in one place.
Notes
- The dependency-inversion principle is about direction, not about interfaces. An interface with exactly one implementation, created only to "follow the principle", inverts nothing.
- Facades are the most under-used pattern in large codebases: a single, small entry point to a subsystem does more for comprehensibility than any amount of internal cleanliness.
1---2name: design-patterns3description: Use when choosing how to structure code for a recurring problem. Covers the patterns that earn their keep, the ones that usually do not, and how to recognize when a pattern is being applied for its own sake.4---56# Design Patterns78## Purpose910Apply structural patterns as solutions to problems you actually have. A pattern applied prophylactically is just indirection with a formal name.1112## When to Use1314- A problem recurs and the current structure requires a change in many places each time.15- Choosing between inheritance, composition, and configuration.16- Reviewing code that is heavily abstracted and hard to follow.17- Designing an extension point for third parties.1819## Capabilities2021- Creational: factory, builder, dependency injection.22- Structural: adapter, facade, decorator, proxy.23- Behavioral: strategy, observer, command, state machine.24- Concurrency: producer-consumer, worker pool, circuit breaker.25- Recognizing pattern misuse: the singleton-as-global, the abstract factory of one.2627## Inputs2829- The problem being solved and how often it recurs.30- The axis of expected change — what varies, and what stays fixed.31- Who extends the code: you, your team, or external consumers.3233## Outputs3435- A structure with a named rationale.36- The specific change it makes cheap, stated explicitly.3738## Workflow39401. **Name the axis of variation.** A pattern buys flexibility along exactly one axis, at the cost of indirection along all of them. If you cannot name the axis, do not apply a pattern.412. **Count the instances.** One implementation needs no strategy interface. Two might. Three definitely does.423. **Choose the lightest structure that works.** A function passed as a parameter is a strategy. It does not need a class hierarchy.434. **Write it concretely first.** Then extract the pattern when the second case arrives — the shape of the abstraction will be obvious rather than guessed.445. **Justify the indirection.** If a reader must open three files to answer "what happens when I call this", the pattern is costing more than it returns.4546## Best Practices4748- Composition over inheritance, nearly always. Inheritance couples you to the parent's internals forever.49- Strategy, adapter, and decorator earn their keep constantly. Abstract factory and visitor rarely do outside frameworks.50- A singleton is a global variable with better manners. Use dependency injection instead — it makes the dependency visible and the code testable.51- A state machine with explicit states and transitions beats a tangle of boolean flags every time.52- Circuit breakers, retries, and timeouts are patterns too, and they are more likely to save you than any of the Gang of Four.53- Two similar pieces of code are not necessarily duplication. Deduplicating things that merely look alike couples things that change for different reasons.5455## Examples5657**Explicit state machine instead of boolean soup:**5859```python60# Before: state is implied by three booleans, and half the combinations are invalid.61if order.is_paid and not order.is_shipped and not order.is_cancelled:62 ship(order)6364# After: states are enumerated, transitions are the only way to move.65class OrderState(Enum):66 PENDING = auto()67 PAID = auto()68 SHIPPED = auto()69 CANCELLED = auto()7071TRANSITIONS: dict[OrderState, set[OrderState]] = {72 OrderState.PENDING: {OrderState.PAID, OrderState.CANCELLED},73 OrderState.PAID: {OrderState.SHIPPED, OrderState.CANCELLED},74 OrderState.SHIPPED: set(),75 OrderState.CANCELLED: set(),76}7778def transition(order: Order, to: OrderState) -> None:79 if to not in TRANSITIONS[order.state]:80 raise InvalidTransition(f"{order.state.name} -> {to.name}")81 order.state = to82```8384Invalid states are now unrepresentable, and the legal transitions are readable in one place.8586## Notes8788- The dependency-inversion principle is about direction, not about interfaces. An interface with exactly one implementation, created only to "follow the principle", inverts nothing.89- Facades are the most under-used pattern in large codebases: a single, small entry point to a subsystem does more for comprehensibility than any amount of internal cleanliness.