Design patterns are a vocabulary, not recipes. They give us names for recurring solutions, allowing us to discuss architecture without prescribing exact code. Start with the problem, then decide whether a pattern simplifies more than it complicates. If the added abstraction outweighs the clarity it brings, the pattern is not worth it.
Three Broad Categories
- Creation: When object construction is complex or variable.
In such cases the pattern shields clients from the details of how objects are instantiated. - Structure: When relationships between objects need organization.
These patterns help you compose objects into larger structures without entangling their responsibilities. - Behavior: When communication or algorithms need to vary.
They let you alter how objects interact or process data without hard‑coding choices.
Creation Patterns
Factory Method
Use it when a creator should select a concrete implementation without exposing the construction details.
The creator returns an abstract product, leaving the concrete class hidden from the client, which eases swapping implementations later.
This pattern allows for flexibility in choosing the object to create without the consumer needing to know the specifics of its construction.
Because the client depends only on the abstract type, you can introduce new subclasses without touching existing code.
Abstract Factory
Use it for families of related objects that must be compatible, such as a matching set of UI controls.
By returning abstract families, the factory guarantees that all produced components share the same look‑and‑feel or protocol.
This ensures that all components work well together, maintaining consistency across the system.
If you later switch from a Windows‑style kit to a macOS‑style kit, you replace only the concrete factory.
Builder
Use it when an object has many optional parameters or construction steps.
The builder collects parameters step by step, producing a fully configured object only when you call its build method.
This allows for step‑by‑step construction of complex objects, making it easier to handle different configurations without duplicating code.
It also makes the client code readable, as each configuration call is self‑describing.
Singleton
Use it only when one instance is a genuine invariant.
If the system truly needs a single point of coordination, such as a logging manager, singleton guarantees that all code shares the same instance.
Dependency injection is often easier to test and reason about, making it a preferable choice when the singleton pattern is not strictly necessary.
Injecting a mock implementation in tests sidesteps the global state that singletons introduce.
Structural Patterns
Adapter
Use it when an existing interface does not match the interface your code expects.
The adapter implements the expected interface while delegating calls to the adaptee, translating method signatures or data formats as needed.
This allows you to connect incompatible interfaces and bridge the gap between them, making it possible to use existing classes without modifying them.
Thus you can integrate third‑party libraries without rewriting them.
Decorator
Use it to add behavior around an object without changing its class.
Each decorator wraps the original object, forwarding calls it does not augment, which preserves the object's core contract.
This enables the extension of an object's capabilities at runtime, allowing for dynamic behavior modification without altering the object's core structure.
You can stack multiple decorators to compose features like logging, compression, or authentication.
Facade
Use it to expose a focused interface to a complex subsystem.
The facade gathers a handful of operations and presents them as a single method call, shielding clients from subsystem intricacies.
This simplifies the use of complex subsystems by providing a single, simplified interface that hides the underlying complexity.
When the subsystem evolves, only the facade needs updating, leaving client code untouched.
Composite
Use it when individual objects and groups should support the same operations, as with files and folders.
Both leaf nodes and composite nodes implement the same component interface, so client code can treat a single file or an entire directory uniformly.
This pattern allows for treating individual objects and compositions of objects uniformly, making it easier to manage hierarchical structures.
It also enables recursive algorithms that traverse the hierarchy without type checks.
Behavioral Patterns
Observer
Use it when one subject should notify multiple listeners without hard‑coding each listener.
The subject maintains a list of observers and pushes updates to each, decoupling the notifier from the concrete observers.
This pattern ensures that changes in state can be communicated to interested parties without tight coupling, promoting loose coupling and flexibility.
Observers can be added or removed at runtime, allowing the system to evolve dynamically.
Command
Use it when actions need history, queues, macros, auditing, or undo.
Each command encapsulates all information needed to execute, undo, or redo an operation, turning requests into first‑class objects.
This allows for encapsulating requests as objects, enabling you to parameterize clients with queues, requests, and operations.
Consequently you can log commands, replay them, or batch them for later execution.
Strategy
Use it when an algorithm should be selected or replaced at runtime.
The context holds a reference to a strategy interface; swapping the concrete strategy changes behavior without altering the context's code.
This pattern enables the algorithm used by a class to be selected at runtime, making it easier to switch between different behaviors without changing the class structure.
It also encourages testing each algorithm in isolation.
Mediator
Use it when direct communication between many objects has become tangled.
The mediator centralizes the interaction logic, so objects send messages to the mediator rather than to each other.
This pattern introduces a mediator object that encapsulates how a set of objects interact, making communication between them more manageable and reducing the complexity of direct interactions.
When you add a new participant, you only need to adjust the mediator, not every existing class.
Chain of Responsibility
Use it when a request should pass through handlers until one handles it.
Each handler decides either to process the request or to forward it to the next handler, creating a flexible processing pipeline.
This pattern allows for passing a request along a chain of potential handlers until one of them is able to handle the request, making the request handling process more flexible and decoupled.
Handlers can be reordered or added without changing the client that initiates the request.
A Compact Decision Matrix
| Problem | Candidate pattern |
|---|---|
| Construction varies | Factory Method |
| Compatible object families | Abstract Factory |
| Many construction options | Builder |
| Interface mismatch | Adapter |
| Add behavior dynamically | Decorator |
| Simplify a subsystem | Facade |
| Tree of interchangeable objects | Composite |
| One-to-many notifications | Observer |
| Undoable or queued actions | Command |
| Selectable algorithm | Strategy |
| Tangled peer communication | Mediator |
| First capable handler | Chain of Responsibility |
Avoid Pattern-Driven Design
Patterns add names, indirection, and maintenance cost.
They also introduce extra layers that developers must understand, which can slow onboarding.
A pattern is justified when the problem recurs, the team understands the trade‑offs, and the resulting design is simpler to change or test.
In such cases the abstraction pays off by reducing duplicated logic.
Introducing a pattern solely to use a familiar label is not a good practice.
It often leads to over‑engineered code that obscures the real intent.
The Bottom Line
Choose the smallest design that keeps responsibilities clear.
If a simple class or function solves the task, resist the urge to wrap it in a pattern.
A pattern should emerge naturally from the constraints of the problem, not be imposed on a codebase for its own sake.
When the constraints shift, the design should adapt without having to refactor a heavyweight pattern.
This ensures that the solution is both effective and maintainable.
Maintainability follows from clarity, not from the number of pattern names you can quote.

Comments (0)
Please sign in to leave a comment