Design Patterns
Part of Software Engineering
Design Patterns
Design Patterns are reusable solutions to common software design problems. They represent best practices evolved over time by experienced software developers.
Why Design Patterns Matter
Think of design patterns as proven architectural blueprints for software. Just as architects use patterns for buildings (like "open plan" or "split level"), developers use patterns for software structure. They provide shared vocabulary, prevent reinventing the wheel, and create maintainable, scalable code.
Pattern Categories
Creational
Deal with object creation mechanisms
Common Patterns
Primary Purpose
Control object creation, provide flexibility in what gets created
When to Use
When object creation is complex or needs to be controlled
Structural
Deal with object composition and relationships
Common Patterns
Primary Purpose
Form larger structures from individual objects
When to Use
When you need to compose objects or manage relationships
Behavioral
Deal with object interaction and responsibility distribution
Common Patterns
Primary Purpose
Define communication between objects and assign responsibilities
When to Use
When objects need to communicate or algorithms need to vary
Creational Patterns
Control object creation mechanisms, trying to create objects in a manner suitable to the situation.
Singleton
Ensures a class has only one instance and provides global access
Intent
Control object creation, limiting to single instance
Structure
Private constructor, static instance, static access method
Common Use Cases
Example Implementation
class Database {
private static instance: Database;
private connection: Connection;
private constructor() {
this.connection = new Connection();
}
public static getInstance(): Database {
if (!Database.instance) {
Database.instance = new Database();
}
return Database.instance;
}
public query(sql: string): Result {
return this.connection.execute(sql);
}
}Pros
- Controlled access
- Reduced memory usage
- Global state management
Cons
- Difficult to test
- Hidden dependencies
- Violates Single Responsibility
Structural Patterns
Deal with object composition and relationships, forming larger structures from individual objects.
Adapter
Allows incompatible interfaces to work together
Intent
Convert interface of class into another interface clients expect
Structure
Target interface, Adaptee, Adapter
Common Use Cases
Example Implementation
// Old system
class OldPrinter {
printText(text: string) {
console.log(`Old printer: ${text}`);
}
}
// New interface
interface Printer {
print(content: string): void;
}
// Adapter
class PrinterAdapter implements Printer {
private oldPrinter: OldPrinter;
constructor(oldPrinter: OldPrinter) {
this.oldPrinter = oldPrinter;
}
print(content: string): void {
this.oldPrinter.printText(content);
}
}Pros
- Reuse legacy code
- Single Responsibility
- Flexibility
Cons
- Additional complexity
- Performance overhead
- Overuse warning
Behavioral Patterns
Deal with object interaction and responsibility distribution among objects.
Observer
Defines one-to-many dependency between objects
Intent
Define one-to-many dependency between objects
Structure
Subject, Observer interface, Concrete observers
Common Use Cases
Example Implementation
interface Observer {
update(temperature: number, humidity: number): void;
}
interface Subject {
registerObserver(o: Observer): void;
removeObserver(o: Observer): void;
notifyObservers(): void;
}
class WeatherStation implements Subject {
private observers: Observer[] = [];
private temperature: number = 0;
private humidity: number = 0;
registerObserver(o: Observer) {
this.observers.push(o);
}
removeObserver(o: Observer) {
const index = this.observers.indexOf(o);
if (index > -1) this.observers.splice(index, 1);
}
notifyObservers() {
this.observers.forEach(observer =>
observer.update(this.temperature, this.humidity)
);
}
setMeasurements(temp: number, humidity: number) {
this.temperature = temp;
this.humidity = humidity;
this.notifyObservers();
}
}Pros
- Loose coupling
- Broadcast communication
- Dynamic relationships
Cons
- Memory leaks
- Unexpected updates
- Performance
Real-world Applications
Database Connection Pool
Manages limited database connections across application
Key Benefits
class ConnectionPool {
private static instance: ConnectionPool;
private connections: Connection[] = [];
private MAX_CONNECTIONS = 10;
private constructor() {
this.initializePool();
}
public static getInstance(): ConnectionPool {
if (!ConnectionPool.instance) {
ConnectionPool.instance = new ConnectionPool();
}
return ConnectionPool.instance;
}
public getConnection(): Connection {
return this.connections.pop() || new Connection();
}
}Stock Market Notifications
Notifies multiple investors when stock prices change
Key Benefits
class StockMarket {
private investors: Investor[] = [];
private stockPrice: number = 100;
subscribe(investor: Investor) {
this.investors.push(investor);
}
setPrice(price: number) {
this.stockPrice = price;
this.investors.forEach(investor =>
investor.update(this.stockPrice)
);
}
}E-commerce Shipping
Different shipping strategies (Standard, Express, Overnight)
Key Benefits
interface ShippingStrategy {
calculate(weight: number): number;
}
class StandardShipping implements ShippingStrategy {
calculate(weight: number): number {
return weight * 1.5;
}
}
class Order {
private shipping: ShippingStrategy;
setShipping(strategy: ShippingStrategy) {
this.shipping = strategy;
}
calculateShipping(weight: number): number {
return this.shipping.calculate(weight);
}
}Web Request Middleware
Adds functionality to HTTP requests (logging, auth, compression)
Key Benefits
interface Handler {
handle(request: Request): Response;
}
class LoggingDecorator implements Handler {
constructor(private next: Handler) {}
handle(request: Request): Response {
console.log(`Request: ${request.url}`);
return this.next.handle(request);
}
}Common Anti-patterns
These are common bad practices that design patterns help you avoid.
God Object
One class does too many things, knows too much
Symptoms
Fix
Apply Single Responsibility Principle, split into smaller classes
Example
// BAD: God object
class Application {
// Handles everything: UI, business logic, database, logging, etc.
}
// GOOD: Separate responsibilities
class UserService { }
class DatabaseService { }
class Logger { }Spaghetti Code
Unstructured, tangled code with no clear separation
Symptoms
Fix
Apply design patterns, refactor with clear structure
Example
// BAD: Spaghetti code
if (condition1) {
if (condition2) {
// ... 10 more nested levels
}
}
// GOOD: Clean structure
function processCondition1() {
if (!condition1) return;
processCondition2();
}Golden Hammer
Using same solution (pattern) for every problem
Symptoms
Fix
Choose patterns based on problem, not habit
Example
// BAD: Using Singleton unnecessarily
class Logger {
private static instance: Logger;
// ... but we only need one logger instance?
}
// GOOD: Simple static class might suffice
class Logger {
static log(message: string) { console.log(message); }
}Circular Dependency
Two or more classes depend on each other
Symptoms
Fix
Use Dependency Injection, introduce interfaces, refactor
Example
// BAD: Circular dependency
class A { constructor(private b: B) {} }
class B { constructor(private a: A) {} }
// GOOD: Break the cycle
interface IA { /* methods */ }
class A implements IA { constructor(private b: B) {} }
class B { constructor(private a: IA) {} }Pattern Selection Guide
| Problem | Consider These Patterns | Quick Decision |
|---|---|---|
| Need to ensure only one instance | Singleton | Use for: DB connections, config, logging |
| Creating complex objects | Builder, Factory Method, Abstract Factory | Builder for step-by-step, Factory for families |
| Adding behavior dynamically | Decorator | Better than inheritance for runtime changes |
| Simplifying complex system | Facade | When clients need simple interface to complex code |
| Algorithm selection at runtime | Strategy | Replace conditional logic with objects |
| One-to-many notifications | Observer | For event-driven, pub/sub systems |
| Need undo/redo functionality | Command | Encapsulate requests as objects |
| Object behavior depends on state | State | Replace state conditionals with objects |
Golden Rule
Don't force patterns. Use them when they solve actual problems in your code. Patterns should emerge from refactoring, not be applied prematurely.
Best Practices
Start Simple
Begin with simple code. Apply patterns during refactoring when you see duplication, complexity, or inflexibility. Don't over-engineer from the start.
Know SOLID Principles
Patterns work best with SOLID principles. Understand Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion.
Choose Wisely
Not every problem needs a pattern. Sometimes a simple function or class is enough. Use patterns to solve specific problems, not as a goal in themselves.
Refactor Gradually
Apply patterns incrementally. Refactor small parts at a time, ensuring tests pass. This reduces risk and makes changes manageable.
Test Your Pattern Knowledge
Design Patterns Quiz
Question 1 of 6Which pattern ensures a class has only one instance?
Further Resources
Essential Books
- Design Patterns: Elements of Reusable OO SoftwareGang of Four
- Head First Design PatternsBeginner Friendly
- Patterns of Enterprise Application ArchitectureMartin Fowler
- Clean ArchitectureRobert C. Martin
Online Resources
- Refactoring.GuruFree
- SourceMaking PatternsFree
- Dofactory .NET PatternsFree
- Pattern Language (C2 Wiki)Community
Practice Problems
Beginner
- • Logger with Singleton
- • Payment system with Strategy
- • File system with Composite
- • Coffee shop with Decorator
Intermediate
- • E-commerce with Factory
- • Text editor with Command
- • Weather station with Observer
- • Game character with State
Advanced
- • ORM with Proxy/Decorator
- • Workflow engine with Template
- • Microservices with Facade
- • Plugin system with Strategy