Design Patterns
Design Patterns¶
Topic: Programming Lesson: 7 of 16 Prerequisites: Understanding of OOP principles (encapsulation, inheritance, polymorphism), basic programming experience Objective: Master common design patterns (Gang of Four and beyond), recognize when to apply them, and understand their trade-offs
Introduction¶
Design patterns are reusable solutions to commonly occurring problems in software design. They provide a shared vocabulary for developers and encapsulate best practices refined over decades. This lesson covers the classic Gang of Four (GoF) patterns, when to use them, and when they might be overkill.
What Are Design Patterns?¶
Design Pattern = Name + Problem + Solution + Consequences
- Name: A shared vocabulary (e.g., "use the Factory pattern here")
- Problem: When to apply the pattern
- Solution: General design that solves the problem
- Consequences: Trade-offs, costs, and benefits
History: Gang of Four (1994)¶
The book Design Patterns: Elements of Reusable Object-Oriented Software by Gamma, Helm, Johnson, and Vlissides introduced 23 patterns, organized into three categories:
- Creational: Object creation mechanisms
- Structural: Composition of classes and objects
- Behavioral: Communication between objects
When to Use Patterns¶
Good reasons: - Problem matches pattern's intent - Pattern simplifies design - Team understands the pattern
Bad reasons (anti-patterns): - "We need more design patterns" (pattern fever) - Using patterns for resume padding - Overengineering simple problems - Forcing patterns where they don't fit
Remember: Patterns are tools, not goals. Simple code > unnecessarily patterned code.
Creational Patterns¶
These patterns deal with object creation, abstracting the instantiation process.
Singleton¶
Intent: Ensure a class has only one instance and provide a global access point.
When to use: - Managing shared resources (database connection pool, logger) - Coordinating system-wide actions
Java (thread-safe):
public class DatabaseConnection {
private static volatile DatabaseConnection instance;
private Connection connection;
// Private constructor prevents external instantiation
private DatabaseConnection() {
// Initialize connection
connection = DriverManager.getConnection("jdbc:...");
}
// Double-checked locking for thread safety
public static DatabaseConnection getInstance() {
if (instance == null) {
synchronized (DatabaseConnection.class) {
if (instance == null) {
instance = new DatabaseConnection();
}
}
}
return instance;
}
public Connection getConnection() {
return connection;
}
}
// Usage
DatabaseConnection db = DatabaseConnection.getInstance();
Python:
class Logger:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.initialized = False
return cls._instance
def __init__(self):
if not self.initialized:
self.log_file = open('app.log', 'a')
self.initialized = True
def log(self, message):
self.log_file.write(f"{message}\n")
self.log_file.flush()
# Usage
logger1 = Logger()
logger2 = Logger()
assert logger1 is logger2 # Same instance
C++ (Meyer's Singleton):
class Config {
public:
static Config& getInstance() {
static Config instance; // Thread-safe in C++11+
return instance;
}
// Delete copy constructor and assignment
Config(const Config&) = delete;
Config& operator=(const Config&) = delete;
void setValue(const std::string& key, const std::string& value) {
settings[key] = value;
}
std::string getValue(const std::string& key) {
return settings[key];
}
private:
Config() {} // Private constructor
std::map<std::string, std::string> settings;
};
// Usage
Config::getInstance().setValue("timeout", "30");
Why it's often an anti-pattern: - Global state makes testing difficult - Tight coupling - Violates Single Responsibility Principle - Difficult to mock or replace
Better alternative: Dependency injection
// Instead of Singleton
public class UserService {
private final Database database;
public UserService(Database database) { // Injected dependency
this.database = database;
}
}
// Easy to test with mock
Database mockDb = new MockDatabase();
UserService service = new UserService(mockDb);
Factory Method¶
Intent: Define an interface for creating objects, but let subclasses decide which class to instantiate.
JavaScript:
// Product interface
class Button {
render() {
throw new Error('Must implement render()');
}
}
class WindowsButton extends Button {
render() {
return '<button class="windows">Click me</button>';
}
}
class MacButton extends Button {
render() {
return '<button class="mac">Click me</button>';
}
}
// Creator
class Dialog {
render() {
const button = this.createButton(); // Factory method
return `<div>${button.render()}</div>`;
}
createButton() {
throw new Error('Must implement createButton()');
}
}
class WindowsDialog extends Dialog {
createButton() {
return new WindowsButton();
}
}
class MacDialog extends Dialog {
createButton() {
return new MacButton();
}
}
// Usage
const os = detectOS();
let dialog;
if (os === 'Windows') {
dialog = new WindowsDialog();
} else if (os === 'Mac') {
dialog = new MacDialog();
}
console.log(dialog.render());
Python (Simple Factory vs Factory Method):
from abc import ABC, abstractmethod
# Simple Factory (not GoF pattern, but useful)
class ShapeFactory:
@staticmethod
def create_shape(shape_type):
if shape_type == 'circle':
return Circle()
elif shape_type == 'rectangle':
return Rectangle()
else:
raise ValueError(f"Unknown shape: {shape_type}")
# Factory Method (GoF pattern)
class Document(ABC):
@abstractmethod
def create_page(self):
pass
def print_document(self):
page = self.create_page() # Factory method
print(f"Printing: {page.render()}")
class PDFDocument(Document):
def create_page(self):
return PDFPage()
class WordDocument(Document):
def create_page(self):
return WordPage()
class Page(ABC):
@abstractmethod
def render(self):
pass
class PDFPage(Page):
def render(self):
return "PDF page content"
class WordPage(Page):
def render(self):
return "Word page content"
Builder¶
Intent: Separate construction of a complex object from its representation, allowing step-by-step construction.
Java (Fluent Interface):
public class HttpRequest {
private String method;
private String url;
private Map<String, String> headers;
private String body;
private HttpRequest() {
headers = new HashMap<>();
}
public static class Builder {
private HttpRequest request;
public Builder() {
request = new HttpRequest();
}
public Builder method(String method) {
request.method = method;
return this;
}
public Builder url(String url) {
request.url = url;
return this;
}
public Builder header(String key, String value) {
request.headers.put(key, value);
return this;
}
public Builder body(String body) {
request.body = body;
return this;
}
public HttpRequest build() {
// Validation
if (request.method == null || request.url == null) {
throw new IllegalStateException("method and url are required");
}
return request;
}
}
// Getters...
}
// Usage
HttpRequest request = new HttpRequest.Builder()
.method("POST")
.url("https://api.example.com/users")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer token123")
.body("{\"name\": \"Alice\"}")
.build();
C++ (SQL Query Builder):
class SQLQuery {
private:
std::string table;
std::vector<std::string> columns;
std::string whereClause;
std::string orderBy;
int limitValue = -1;
public:
class Builder {
private:
SQLQuery query;
public:
Builder& select(const std::vector<std::string>& cols) {
query.columns = cols;
return *this;
}
Builder& from(const std::string& tbl) {
query.table = tbl;
return *this;
}
Builder& where(const std::string& condition) {
query.whereClause = condition;
return *this;
}
Builder& orderBy(const std::string& column) {
query.orderBy = column;
return *this;
}
Builder& limit(int n) {
query.limitValue = n;
return *this;
}
SQLQuery build() {
if (query.table.empty()) {
throw std::runtime_error("table is required");
}
return query;
}
};
std::string toSQL() const {
std::string sql = "SELECT ";
if (columns.empty()) {
sql += "*";
} else {
for (size_t i = 0; i < columns.size(); i++) {
if (i > 0) sql += ", ";
sql += columns[i];
}
}
sql += " FROM " + table;
if (!whereClause.empty()) {
sql += " WHERE " + whereClause;
}
if (!orderBy.empty()) {
sql += " ORDER BY " + orderBy;
}
if (limitValue > 0) {
sql += " LIMIT " + std::to_string(limitValue);
}
return sql;
}
};
// Usage
SQLQuery query = SQLQuery::Builder()
.select({"id", "name", "email"})
.from("users")
.where("age > 18")
.orderBy("name")
.limit(10)
.build();
std::cout << query.toSQL() << "\n";
Prototype¶
Intent: Create new objects by copying existing objects (prototypes).
JavaScript (prototypal inheritance):
const carPrototype = {
drive() {
console.log(`Driving a ${this.make} ${this.model}`);
},
clone() {
return Object.create(Object.getPrototypeOf(this),
Object.getOwnPropertyDescriptors(this));
}
};
function createCar(make, model, year) {
const car = Object.create(carPrototype);
car.make = make;
car.model = model;
car.year = year;
return car;
}
const tesla = createCar('Tesla', 'Model 3', 2024);
const teslaClone = tesla.clone();
teslaClone.year = 2025;
tesla.drive(); // Driving a Tesla Model 3
teslaClone.drive(); // Driving a Tesla Model 3
Structural Patterns¶
These patterns deal with object composition, forming larger structures while keeping them flexible.
Adapter¶
Intent: Convert the interface of a class into another interface clients expect.
Python:
# Legacy system
class OldPaymentProcessor:
def process_payment(self, amount):
print(f"Old system processing ${amount}")
# New interface
class PaymentProcessor:
def pay(self, amount):
raise NotImplementedError
# Adapter
class PaymentAdapter(PaymentProcessor):
def __init__(self, old_processor):
self.old_processor = old_processor
def pay(self, amount):
# Adapt new interface to old interface
self.old_processor.process_payment(amount)
# Client code expects new interface
def checkout(payment_processor: PaymentProcessor, amount):
payment_processor.pay(amount)
# Usage
old_system = OldPaymentProcessor()
adapter = PaymentAdapter(old_system)
checkout(adapter, 99.99) # Works with new interface
Java (Object Adapter):
// Target interface
interface MediaPlayer {
void play(String filename);
}
// Adaptee (incompatible interface)
class AdvancedMediaPlayer {
public void playVlc(String filename) {
System.out.println("Playing VLC file: " + filename);
}
public void playMp4(String filename) {
System.out.println("Playing MP4 file: " + filename);
}
}
// Adapter
class MediaAdapter implements MediaPlayer {
private AdvancedMediaPlayer advancedPlayer;
public MediaAdapter() {
advancedPlayer = new AdvancedMediaPlayer();
}
@Override
public void play(String filename) {
if (filename.endsWith(".vlc")) {
advancedPlayer.playVlc(filename);
} else if (filename.endsWith(".mp4")) {
advancedPlayer.playMp4(filename);
}
}
}
// Usage
MediaPlayer player = new MediaAdapter();
player.play("video.mp4");
Decorator¶
Intent: Attach additional responsibilities to an object dynamically, providing a flexible alternative to subclassing.
Java (I/O Streams):
// Classic example: Java I/O
InputStream fileStream = new FileInputStream("data.txt");
InputStream bufferedStream = new BufferedInputStream(fileStream);
InputStream compressedStream = new GZIPInputStream(bufferedStream);
// Each decorator adds functionality
Python (Function Decorators):
import time
from functools import wraps
# Timing decorator
def timing(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} took {end - start:.4f}s")
return result
return wrapper
# Logging decorator
def logging(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with {args}, {kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapper
# Stack decorators
@timing
@logging
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
fibonacci(10)
C++ (Coffee Shop Example):
// Component
class Coffee {
public:
virtual ~Coffee() = default;
virtual std::string getDescription() const = 0;
virtual double cost() const = 0;
};
// Concrete Component
class SimpleCoffee : public Coffee {
public:
std::string getDescription() const override {
return "Simple coffee";
}
double cost() const override {
return 2.0;
}
};
// Decorator
class CoffeeDecorator : public Coffee {
protected:
Coffee* coffee;
public:
CoffeeDecorator(Coffee* c) : coffee(c) {}
virtual ~CoffeeDecorator() { delete coffee; }
};
// Concrete Decorators
class Milk : public CoffeeDecorator {
public:
Milk(Coffee* c) : CoffeeDecorator(c) {}
std::string getDescription() const override {
return coffee->getDescription() + ", milk";
}
double cost() const override {
return coffee->cost() + 0.5;
}
};
class Sugar : public CoffeeDecorator {
public:
Sugar(Coffee* c) : CoffeeDecorator(c) {}
std::string getDescription() const override {
return coffee->getDescription() + ", sugar";
}
double cost() const override {
return coffee->cost() + 0.2;
}
};
// Usage
Coffee* myCoffee = new SimpleCoffee();
myCoffee = new Milk(myCoffee);
myCoffee = new Sugar(myCoffee);
std::cout << myCoffee->getDescription() << " costs $" << myCoffee->cost() << "\n";
// Output: Simple coffee, milk, sugar costs $2.7
delete myCoffee;
Facade¶
Intent: Provide a unified, simplified interface to a complex subsystem.
JavaScript:
// Complex subsystem
class CPU {
freeze() { console.log('CPU: Freezing...'); }
jump(position) { console.log(`CPU: Jumping to ${position}`); }
execute() { console.log('CPU: Executing...'); }
}
class Memory {
load(position, data) {
console.log(`Memory: Loading ${data} at ${position}`);
}
}
class HardDrive {
read(sector, size) {
console.log(`HDD: Reading ${size} bytes from sector ${sector}`);
return 'boot data';
}
}
// Facade
class ComputerFacade {
constructor() {
this.cpu = new CPU();
this.memory = new Memory();
this.hdd = new HardDrive();
}
start() {
this.cpu.freeze();
const bootData = this.hdd.read(0, 1024);
this.memory.load(0, bootData);
this.cpu.jump(0);
this.cpu.execute();
}
}
// Client code (simple!)
const computer = new ComputerFacade();
computer.start(); // Hides complex subsystem
Proxy¶
Intent: Provide a surrogate or placeholder for another object to control access.
Types: - Virtual Proxy: Lazy initialization (create expensive object only when needed) - Protection Proxy: Access control - Remote Proxy: Represent object in different address space - Cache Proxy: Cache results
Python (Virtual Proxy with Caching):
class Image:
def __init__(self, filename):
self.filename = filename
self._load()
def _load(self):
print(f"Loading image from {self.filename}")
# Expensive operation
time.sleep(2)
self.data = f"Image data from {self.filename}"
def display(self):
print(f"Displaying {self.data}")
# Proxy with lazy loading and caching
class ImageProxy:
def __init__(self, filename):
self.filename = filename
self._image = None # Not loaded yet
def display(self):
if self._image is None:
self._image = Image(self.filename) # Lazy load
self._image.display()
# Usage
image = ImageProxy("photo.jpg")
# Image not loaded yet
image.display() # Loads now (2 second delay)
image.display() # Uses cached image (instant)
Java (Protection Proxy):
interface Document {
void display();
void edit(String content);
}
class RealDocument implements Document {
private String content;
public void display() {
System.out.println("Displaying: " + content);
}
public void edit(String newContent) {
content = newContent;
System.out.println("Document edited");
}
}
class ProtectedDocument implements Document {
private RealDocument document;
private String userRole;
public ProtectedDocument(String role) {
document = new RealDocument();
userRole = role;
}
public void display() {
document.display(); // Anyone can view
}
public void edit(String content) {
if (userRole.equals("ADMIN")) {
document.edit(content);
} else {
System.out.println("Access denied: insufficient permissions");
}
}
}
// Usage
Document doc = new ProtectedDocument("USER");
doc.display(); // OK
doc.edit("new text"); // Denied
Composite¶
Intent: Compose objects into tree structures to represent part-whole hierarchies, treating individual and composite objects uniformly.
C++:
#include <vector>
#include <memory>
// Component
class Graphic {
public:
virtual ~Graphic() = default;
virtual void draw() const = 0;
virtual void add(std::shared_ptr<Graphic> g) {
throw std::runtime_error("Cannot add to leaf");
}
};
// Leaf
class Circle : public Graphic {
public:
void draw() const override {
std::cout << "Drawing circle\n";
}
};
class Rectangle : public Graphic {
public:
void draw() const override {
std::cout << "Drawing rectangle\n";
}
};
// Composite
class CompositeGraphic : public Graphic {
private:
std::vector<std::shared_ptr<Graphic>> children;
public:
void add(std::shared_ptr<Graphic> g) override {
children.push_back(g);
}
void draw() const override {
for (const auto& child : children) {
child->draw();
}
}
};
// Usage
auto circle1 = std::make_shared<Circle>();
auto circle2 = std::make_shared<Circle>();
auto rect = std::make_shared<Rectangle>();
auto group1 = std::make_shared<CompositeGraphic>();
group1->add(circle1);
group1->add(rect);
auto group2 = std::make_shared<CompositeGraphic>();
group2->add(circle2);
group2->add(group1); // Nested composite
group2->draw(); // Draws entire tree
Behavioral Patterns¶
These patterns focus on communication between objects.
Observer¶
Intent: Define a one-to-many dependency so that when one object changes state, all dependents are notified.
JavaScript (Event System):
class EventEmitter {
constructor() {
this.listeners = {};
}
on(event, callback) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(callback);
}
off(event, callback) {
if (this.listeners[event]) {
this.listeners[event] = this.listeners[event]
.filter(cb => cb !== callback);
}
}
emit(event, data) {
if (this.listeners[event]) {
this.listeners[event].forEach(callback => callback(data));
}
}
}
// Subject
class Stock extends EventEmitter {
constructor(symbol) {
super();
this.symbol = symbol;
this.price = 0;
}
setPrice(price) {
this.price = price;
this.emit('priceChange', { symbol: this.symbol, price });
}
}
// Observers
class Investor {
constructor(name) {
this.name = name;
}
update(data) {
console.log(`${this.name} notified: ${data.symbol} is now $${data.price}`);
}
}
// Usage
const apple = new Stock('AAPL');
const investor1 = new Investor('Alice');
const investor2 = new Investor('Bob');
apple.on('priceChange', data => investor1.update(data));
apple.on('priceChange', data => investor2.update(data));
apple.setPrice(150); // Both investors notified
apple.setPrice(155); // Both investors notified
Python (Property Observer):
class Observable:
def __init__(self):
self._observers = []
def attach(self, observer):
self._observers.append(observer)
def detach(self, observer):
self._observers.remove(observer)
def notify(self, data):
for observer in self._observers:
observer.update(data)
class Subject(Observable):
def __init__(self):
super().__init__()
self._state = None
@property
def state(self):
return self._state
@state.setter
def state(self, value):
self._state = value
self.notify(value) # Notify observers on change
class ConcreteObserver:
def __init__(self, name):
self.name = name
def update(self, data):
print(f"{self.name} received update: {data}")
# Usage
subject = Subject()
obs1 = ConcreteObserver("Observer 1")
obs2 = ConcreteObserver("Observer 2")
subject.attach(obs1)
subject.attach(obs2)
subject.state = "new state" # Both observers notified
Strategy¶
Intent: Define a family of algorithms, encapsulate each one, and make them interchangeable.
Java:
// Strategy interface
interface PaymentStrategy {
void pay(int amount);
}
// Concrete strategies
class CreditCardPayment implements PaymentStrategy {
private String cardNumber;
public CreditCardPayment(String cardNumber) {
this.cardNumber = cardNumber;
}
@Override
public void pay(int amount) {
System.out.println("Paid $" + amount + " with credit card " + cardNumber);
}
}
class PayPalPayment implements PaymentStrategy {
private String email;
public PayPalPayment(String email) {
this.email = email;
}
@Override
public void pay(int amount) {
System.out.println("Paid $" + amount + " via PayPal account " + email);
}
}
class CryptoPayment implements PaymentStrategy {
private String walletAddress;
public CryptoPayment(String wallet) {
this.walletAddress = wallet;
}
@Override
public void pay(int amount) {
System.out.println("Paid $" + amount + " to crypto wallet " + walletAddress);
}
}
// Context
class ShoppingCart {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public void checkout(int amount) {
paymentStrategy.pay(amount);
}
}
// Usage
ShoppingCart cart = new ShoppingCart();
cart.setPaymentStrategy(new CreditCardPayment("1234-5678"));
cart.checkout(100);
cart.setPaymentStrategy(new PayPalPayment("user@example.com"));
cart.checkout(50);
cart.setPaymentStrategy(new CryptoPayment("0x1234..."));
cart.checkout(200);
Command¶
Intent: Encapsulate a request as an object, allowing parameterization, queuing, logging, and undo operations.
C++ (Text Editor with Undo/Redo):
#include <stack>
#include <memory>
// Command interface
class Command {
public:
virtual ~Command() = default;
virtual void execute() = 0;
virtual void undo() = 0;
};
// Receiver
class TextEditor {
private:
std::string text;
public:
void insertText(const std::string& str, size_t pos) {
text.insert(pos, str);
}
void deleteText(size_t pos, size_t length) {
text.erase(pos, length);
}
std::string getText() const {
return text;
}
};
// Concrete Commands
class InsertCommand : public Command {
private:
TextEditor* editor;
std::string textToInsert;
size_t position;
public:
InsertCommand(TextEditor* ed, const std::string& text, size_t pos)
: editor(ed), textToInsert(text), position(pos) {}
void execute() override {
editor->insertText(textToInsert, position);
}
void undo() override {
editor->deleteText(position, textToInsert.length());
}
};
class DeleteCommand : public Command {
private:
TextEditor* editor;
std::string deletedText;
size_t position;
size_t length;
public:
DeleteCommand(TextEditor* ed, size_t pos, size_t len)
: editor(ed), position(pos), length(len) {
deletedText = editor->getText().substr(pos, len);
}
void execute() override {
editor->deleteText(position, length);
}
void undo() override {
editor->insertText(deletedText, position);
}
};
// Invoker
class CommandManager {
private:
std::stack<std::shared_ptr<Command>> undoStack;
std::stack<std::shared_ptr<Command>> redoStack;
public:
void executeCommand(std::shared_ptr<Command> cmd) {
cmd->execute();
undoStack.push(cmd);
// Clear redo stack on new command
while (!redoStack.empty()) {
redoStack.pop();
}
}
void undo() {
if (!undoStack.empty()) {
auto cmd = undoStack.top();
undoStack.pop();
cmd->undo();
redoStack.push(cmd);
}
}
void redo() {
if (!redoStack.empty()) {
auto cmd = redoStack.top();
redoStack.pop();
cmd->execute();
undoStack.push(cmd);
}
}
};
// Usage
TextEditor editor;
CommandManager manager;
auto insert1 = std::make_shared<InsertCommand>(&editor, "Hello", 0);
manager.executeCommand(insert1);
std::cout << editor.getText() << "\n"; // Hello
auto insert2 = std::make_shared<InsertCommand>(&editor, " World", 5);
manager.executeCommand(insert2);
std::cout << editor.getText() << "\n"; // Hello World
manager.undo();
std::cout << editor.getText() << "\n"; // Hello
manager.redo();
std::cout << editor.getText() << "\n"; // Hello World
Iterator¶
Intent: Provide a way to access elements of a collection sequentially without exposing the underlying representation.
Python (Custom Iterator):
class LinkedListNode:
def __init__(self, value):
self.value = value
self.next = None
class LinkedList:
def __init__(self):
self.head = None
def append(self, value):
if not self.head:
self.head = LinkedListNode(value)
else:
current = self.head
while current.next:
current = current.next
current.next = LinkedListNode(value)
def __iter__(self):
return LinkedListIterator(self.head)
class LinkedListIterator:
def __init__(self, head):
self.current = head
def __iter__(self):
return self
def __next__(self):
if self.current is None:
raise StopIteration
value = self.current.value
self.current = self.current.next
return value
# Usage
linked_list = LinkedList()
linked_list.append(1)
linked_list.append(2)
linked_list.append(3)
for value in linked_list:
print(value) # 1, 2, 3
State¶
Intent: Allow an object to alter its behavior when its internal state changes.
JavaScript (TCP Connection):
// State interface
class ConnectionState {
open(connection) { throw new Error('Not implemented'); }
close(connection) { throw new Error('Not implemented'); }
read(connection) { throw new Error('Not implemented'); }
write(connection, data) { throw new Error('Not implemented'); }
}
// Concrete States
class ClosedState extends ConnectionState {
open(connection) {
console.log('Opening connection...');
connection.setState(new OpenState());
}
close(connection) {
console.log('Already closed');
}
read(connection) {
console.log('Cannot read: connection closed');
}
write(connection, data) {
console.log('Cannot write: connection closed');
}
}
class OpenState extends ConnectionState {
open(connection) {
console.log('Already open');
}
close(connection) {
console.log('Closing connection...');
connection.setState(new ClosedState());
}
read(connection) {
console.log('Reading data...');
return 'data';
}
write(connection, data) {
console.log(`Writing: ${data}`);
}
}
// Context
class TCPConnection {
constructor() {
this.state = new ClosedState();
}
setState(state) {
this.state = state;
}
open() { this.state.open(this); }
close() { this.state.close(this); }
read() { return this.state.read(this); }
write(data) { this.state.write(this, data); }
}
// Usage
const conn = new TCPConnection();
conn.read(); // Cannot read: connection closed
conn.open(); // Opening connection...
conn.write('Hello'); // Writing: Hello
conn.close(); // Closing connection...
Template Method¶
Intent: Define the skeleton of an algorithm in a method, deferring some steps to subclasses.
Python:
from abc import ABC, abstractmethod
class DataParser(ABC):
# Template method
def parse(self, filename):
data = self.read_file(filename)
parsed = self.parse_data(data)
validated = self.validate(parsed)
self.use_data(validated)
def read_file(self, filename):
with open(filename, 'r') as f:
return f.read()
@abstractmethod
def parse_data(self, data):
pass
def validate(self, data):
# Default validation (can be overridden)
return data
@abstractmethod
def use_data(self, data):
pass
class CSVParser(DataParser):
def parse_data(self, data):
lines = data.strip().split('\n')
return [line.split(',') for line in lines]
def use_data(self, data):
print(f"Processing {len(data)} CSV rows")
class JSONParser(DataParser):
def parse_data(self, data):
import json
return json.loads(data)
def validate(self, data):
if not isinstance(data, list):
raise ValueError("Expected list")
return data
def use_data(self, data):
print(f"Processing {len(data)} JSON objects")
# Usage
csv_parser = CSVParser()
csv_parser.parse('data.csv')
json_parser = JSONParser()
json_parser.parse('data.json')
Anti-Patterns¶
Common anti-patterns to avoid:
- God Object: One class that does everything
- Spaghetti Code: Tangled control flow
- Golden Hammer: Using one pattern for every problem
- Lava Flow: Dead code kept "just in case"
- Copy-Paste Programming: Duplication instead of abstraction
Modern Perspective¶
Many GoF patterns are less necessary in modern languages:
- Strategy/Command: First-class functions eliminate need for classes
- Iterator: Built into most languages (Python generators, Java Streams)
- Singleton: Dependency injection preferred
- Visitor: Pattern matching in functional languages
Example (Strategy without classes in Python):
# Old (class-based)
class Strategy:
def execute(self):
pass
# New (function-based)
def strategy_a(data):
return data * 2
def strategy_b(data):
return data ** 2
strategies = {'a': strategy_a, 'b': strategy_b}
result = strategies['a'](10)
Summary¶
| Pattern | Category | Purpose | When to Use |
|---|---|---|---|
| Singleton | Creational | One instance | Shared resources (use DI instead) |
| Factory Method | Creational | Defer instantiation | Object creation varies |
| Builder | Creational | Complex construction | Many optional parameters |
| Adapter | Structural | Interface conversion | Integrate incompatible code |
| Decorator | Structural | Add behavior dynamically | Flexible alternatives to subclassing |
| Facade | Structural | Simplify interface | Hide complex subsystems |
| Proxy | Structural | Control access | Lazy loading, access control |
| Observer | Behavioral | One-to-many notifications | Event systems |
| Strategy | Behavioral | Swappable algorithms | Algorithm varies at runtime |
| Command | Behavioral | Encapsulate requests | Undo/redo, queuing |
Exercises¶
Exercise 1: Identify Patterns¶
Identify which design pattern(s) are used in each scenario:
- Java's
InputStreamhierarchy (BufferedInputStream, GZIPInputStream) - Spring Framework's dependency injection
- GUI event listeners (button.onClick)
- Python's
@propertydecorator - SQL query builders
Exercise 2: Implement Observer¶
Implement a weather station that notifies multiple displays when temperature changes. Include:
- WeatherStation (subject)
- Multiple display types (current conditions, statistics, forecast)
- Ability to add/remove displays dynamically
Exercise 3: Refactor with Strategy¶
Refactor this code to use the Strategy pattern:
class Order {
public double calculateShipping(String method, double weight) {
if (method.equals("standard")) {
return weight * 0.5;
} else if (method.equals("express")) {
return weight * 1.5;
} else if (method.equals("overnight")) {
return weight * 3.0;
}
return 0;
}
}
Exercise 4: Build a Command System¶
Create a smart home automation system using the Command pattern: - Support commands: turn on/off lights, adjust thermostat, lock/unlock doors - Implement macro commands (e.g., "leaving home" turns off lights, locks doors, adjusts thermostat) - Support undo for all commands
Exercise 5: When NOT to Use Patterns¶
For each scenario, explain why using a design pattern would be overkill:
- A script that reads a file and prints its contents
- A simple calculator with add/subtract/multiply/divide
- A todo list app with 3 screens
- A config file with 5 settings
Previous: 06_Functional_Programming.md Next: 08_Clean_Code.md