Helps understand and apply the 23 classic GoF design patterns to solve common software design problems
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.
The GoF patterns are divided into three categories:
Patterns that deal with object creation mechanisms:
Patterns that deal with object composition and relationships:
Patterns that deal with communication between objects:
| 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 |
Beyond GoF patterns, this skill also covers:
Patterns commonly used in enterprise application architecture:
High-level patterns for structuring applications:
Patterns for handling concurrent operations:
For in-depth explanations with code examples, refer to:
Design patterns support these fundamental principles:
When analyzing code or design problems:
✅ Use patterns when:
❌ Avoid patterns when:
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.
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.
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.