Design Patterns
What are Design Patterns?
Design patterns are reusable solutions to common problems in software design. This skill covers both classic GoF (Gang of Four) patterns and other widely-used design patterns.
GoF Design Patterns
The GoF patterns are divided into three categories:
Creational Patterns (5)
Patterns that deal with object creation mechanisms:
- •Abstract Factory - Provides an interface for creating families of related objects
- •Builder - Separates object construction from its representation
- •Factory Method - Defines an interface for creating objects, letting subclasses decide which class to instantiate
- •Prototype - Creates new objects by copying existing ones
- •Singleton - Ensures a class has only one instance
Structural Patterns (7)
Patterns that deal with object composition and relationships:
- •Adapter - Converts one interface to another
- •Bridge - Separates abstraction from implementation
- •Composite - Composes objects into tree structures
- •Decorator - Adds responsibilities to objects dynamically
- •Facade - Provides a simplified interface to a complex subsystem
- •Flyweight - Shares objects to support large numbers efficiently
- •Proxy - Provides a surrogate or placeholder for another object
Behavioral Patterns (11)
Patterns that deal with communication between objects:
- •Chain of Responsibility - Passes requests along a chain of handlers
- •Command - Encapsulates a request as an object
- •Interpreter - Defines a grammar and interpreter for a language
- •Iterator - Provides sequential access to elements
- •Mediator - Defines simplified communication between classes
- •Memento - Captures and restores an object's internal state
- •Observer - Defines a one-to-many dependency between objects
- •State - Allows an object to alter its behavior when state changes
- •Strategy - Defines a family of interchangeable algorithms
- •Template Method - Defines the skeleton of an algorithm
- •Visitor - Separates algorithms from the objects they operate on
Quick Reference
| Pattern | Purpose | When to Use |
|---|---|---|
| Strategy | Define family of algorithms | Multiple ways to do something |
| Decorator | Add responsibilities dynamically | Extend functionality without subclassing |
| Factory Method | Create objects without specifying exact class | Defer instantiation to subclasses |
| Observer | Notify multiple objects of state changes | One-to-many dependencies |
| Singleton | Ensure single instance | Shared resource or configuration |
| Adapter | Make incompatible interfaces work together | Integrate with legacy code |
| Template Method | Define algorithm skeleton | Common algorithm with varying steps |
Other Design Patterns
Beyond GoF patterns, this skill also covers:
Enterprise Patterns
Patterns commonly used in enterprise application architecture:
- •Repository Pattern - Mediates between domain and data mapping layers
- •Unit of Work - Maintains a list of objects affected by a business transaction
- •Data Mapper - Maps data between objects and database
- •Service Layer - Defines application's boundary and encapsulates business logic
Architectural Patterns
High-level patterns for structuring applications:
- •MVC (Model-View-Controller) - Separates application into three interconnected components
- •MVVM (Model-View-ViewModel) - Facilitates separation of UI development from business logic
- •Clean Architecture - Creates maintainable systems independent of frameworks and UI
- •Hexagonal Architecture (Ports and Adapters) - Isolates core logic from external concerns
Concurrency Patterns
Patterns for handling concurrent operations:
- •Producer-Consumer - Separates data production from consumption
- •Read-Write Lock - Allows concurrent reads but exclusive writes
- •Thread Pool - Manages a pool of worker threads
Detailed Pattern Documentation
For in-depth explanations with code examples, refer to:
Key Principles
Design patterns support these fundamental principles:
- •Program to an interface, not an implementation
- •Favor composition over inheritance
- •Encapsulate what varies
- •Strive for loosely coupled designs
- •Classes should be open for extension but closed for modification
Instructions
When analyzing code or design problems:
- •Identify the core problem or requirement in the provided code
- •Determine if a design pattern applies by checking:
- •Is there a recurring design problem?
- •Would a pattern provide clear benefits (flexibility, maintainability)?
- •Is the complexity justified by the problem?
- •Consider patterns from all categories (GoF, Enterprise, Architectural, Concurrency)
- •Explain which pattern(s) could help and why
- •Provide implementation guidance with code examples
- •Discuss trade-offs and alternatives
- •Ensure the pattern doesn't add unnecessary complexity
- •Highlight the benefits and potential drawbacks
When to Use Patterns
✅ Use patterns when:
- •You recognize a recurring design problem
- •The pattern provides clear benefits (flexibility, maintainability, etc.)
- •The team understands the pattern
- •The complexity is justified by the problem
❌ Avoid patterns when:
- •The problem is simple and doesn't need the complexity
- •You're "pattern hunting" without a real need
- •The pattern makes the code harder to understand
- •It's premature optimization
Examples
Example 1: Strategy Pattern for Payment Processing
Problem:
class PaymentService {
public void processPayment(double amount, String method) {
if (method.equals("credit_card")) {
// Credit card logic
} else if (method.equals("paypal")) {
// PayPal logic
} else if (method.equals("crypto")) {
// Crypto logic
}
}
}
Issue: Adding new payment methods requires modifying existing code. Multiple if-else statements make code hard to maintain.
Recommended Pattern: Strategy Pattern
Solution:
interface PaymentStrategy {
void pay(double amount);
}
class CreditCardPayment implements PaymentStrategy {
public void pay(double amount) {
// Credit card logic
}
}
class PayPalPayment implements PaymentStrategy {
public void pay(double amount) {
// PayPal logic
}
}
class PaymentService {
private PaymentStrategy strategy;
public void setStrategy(PaymentStrategy strategy) {
this.strategy = strategy;
}
public void processPayment(double amount) {
strategy.pay(amount);
}
}
Benefits: Easy to add new payment methods, adheres to Open/Closed Principle, each strategy is independently testable.
Example 2: Decorator Pattern for Coffee Shop
Problem: Need to add various options (milk, sugar, whipped cream) to coffee, and pricing should reflect all additions.
Recommended Pattern: Decorator Pattern
Solution:
interface Coffee {
double getCost();
String getDescription();
}
class SimpleCoffee implements Coffee {
public double getCost() { return 2.0; }
public String getDescription() { return "Simple coffee"; }
}
abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee;
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
}
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) { super(coffee); }
public double getCost() { return coffee.getCost() + 0.5; }
public String getDescription() { return coffee.getDescription() + ", milk"; }
}
class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) { super(coffee); }
public double getCost() { return coffee.getCost() + 0.2; }
public String getDescription() { return coffee.getDescription() + ", sugar"; }
}
// Usage
Coffee coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
// Cost: 2.7, Description: "Simple coffee, milk, sugar"
Benefits: Add functionality dynamically at runtime, avoids explosion of subclasses, follows Single Responsibility Principle.
Example 3: Observer Pattern for Stock Market
Problem: Multiple displays need to update when stock prices change.
Recommended Pattern: Observer Pattern
Solution:
interface Observer {
update(stock: string, price: number): void;
}
class Stock {
private observers: Observer[] = [];
private prices: Map<string, number> = new Map();
attach(observer: Observer): void {
this.observers.push(observer);
}
setPrice(stock: string, price: number): void {
this.prices.set(stock, price);
this.notifyObservers(stock, price);
}
private notifyObservers(stock: string, price: number): void {
this.observers.forEach(observer => observer.update(stock, price));
}
}
class StockDisplay implements Observer {
update(stock: string, price: number): void {
console.log(`Display: ${stock} is now $${price}`);
}
}
class StockAlert implements Observer {
update(stock: string, price: number): void {
if (price > 100) {
console.log(`Alert: ${stock} exceeded $100!`);
}
}
}
// Usage
const stock = new Stock();
stock.attach(new StockDisplay());
stock.attach(new StockAlert());
stock.setPrice("AAPL", 150); // Both observers notified
Benefits: Loose coupling between subject and observers, supports broadcast communication, easy to add new observers.
Common Pitfalls
- •Overuse - Don't force patterns where they don't fit
- •Premature abstraction - Wait until you have a real need
- •Wrong pattern - Make sure the pattern matches the problem
- •Complexity creep - Patterns should simplify, not complicate
- •Ignoring context - Consider your specific requirements and constraints