| name | gof-design-patterns |
| description | Helps understand and apply the 23 classic GoF design patterns to solve common software design problems |
GoF Design Patterns
What are GoF Design Patterns?
Design patterns are reusable solutions to common problems in software design. 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 |
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?
- 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