What are Design Patterns
Design patterns are reusable solutions to commonly occurring problems in software design. They were systematized in the 1994 book by the GoF (Gang of Four).
Why learn patterns: Avoid reinventing the wheel and establish a common vocabulary among developers.
Creational Patterns
Singleton
Ensures a class has only one instance.
// Singleton in JavaScript
class Database {
static #instance = null;
constructor() {
if (Database.#instance) {
return Database.#instance;
}
this.connection = this.connect();
Database.#instance = this;
}
connect() {
console.log('Database connected');
return { /* connection */ };
}
static getInstance() {
if (!Database.#instance) {
Database.#instance = new Database();
}
return Database.#instance;
}
}
// Usage
const db1 = Database.getInstance();
const db2 = Database.getInstance();
console.log(db1 === db2); // true
Use cases: Database connections, logging functionality, configuration management
Factory
Encapsulates object creation.
// Factory for creating notifications
class NotificationFactory {
static create(type, message) {
switch (type) {
case 'email':
return new EmailNotification(message);
case 'sms':
return new SMSNotification(message);
case 'push':
return new PushNotification(message);
default:
throw new Error(`Unknown notification type: ${type}`);
}
}
}
// Usage
const notification = NotificationFactory.create('email', 'Hello!');
notification.send();
Use cases: Conditional object creation, separating dependencies
Builder
Constructs complex objects step by step.
class QueryBuilder {
constructor() {
this.query = { select: '*', from: '', where: [], orderBy: '' };
}
select(fields) {
this.query.select = fields;
return this;
}
from(table) {
this.query.from = table;
return this;
}
where(condition) {
this.query.where.push(condition);
return this;
}
orderBy(field) {
this.query.orderBy = field;
return this;
}
build() {
let sql = `SELECT ${this.query.select} FROM ${this.query.from}`;
if (this.query.where.length > 0) {
sql += ` WHERE ${this.query.where.join(' AND ')}`;
}
if (this.query.orderBy) {
sql += ` ORDER BY ${this.query.orderBy}`;
}
return sql;
}
}
// Usage
const query = new QueryBuilder()
.select('name, email')
.from('users')
.where('status = "active"')
.where('age > 18')
.orderBy('created_at DESC')
.build();
Use cases: SQL query building, HTTP request building, complex configuration objects
Structural Patterns
Adapter
Converts incompatible interfaces.
// Old API
class OldPaymentSystem {
processPayment(amount) {
console.log(`Old system: Processing ${amount}`);
return { success: true };
}
}
// New API interface
class PaymentAdapter {
constructor(oldSystem) {
this.oldSystem = oldSystem;
}
pay(request) {
// Convert new interface to old system
const result = this.oldSystem.processPayment(request.amount);
return {
transactionId: `txn_${Date.now()}`,
status: result.success ? 'completed' : 'failed'
};
}
}
// Usage
const adapter = new PaymentAdapter(new OldPaymentSystem());
adapter.pay({ amount: 1000, currency: 'JPY' });
Use cases: Legacy system integration, abstracting third-party libraries
Decorator
Dynamically adds functionality to objects.
// Basic coffee
class Coffee {
cost() { return 300; }
description() { return 'Coffee'; }
}
// Decorators
class MilkDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() { return this.coffee.cost() + 50; }
description() { return `${this.coffee.description()} + Milk`; }
}
class SugarDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() { return this.coffee.cost() + 20; }
description() { return `${this.coffee.description()} + Sugar`; }
}
// Usage
let coffee = new Coffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
console.log(coffee.description()); // Coffee + Milk + Sugar
console.log(coffee.cost()); // 370
Use cases: Middleware, adding logging functionality, adding caching
Facade
Provides a simple interface to a complex subsystem.
// Complex subsystem
class VideoDecoder { decode(file) { /* ... */ } }
class AudioDecoder { decode(file) { /* ... */ } }
class SubtitleParser { parse(file) { /* ... */ } }
class VideoPlayer { play(video, audio, subtitle) { /* ... */ } }
// Facade
class MediaPlayerFacade {
constructor() {
this.videoDecoder = new VideoDecoder();
this.audioDecoder = new AudioDecoder();
this.subtitleParser = new SubtitleParser();
this.player = new VideoPlayer();
}
playVideo(filename) {
const video = this.videoDecoder.decode(filename);
const audio = this.audioDecoder.decode(filename);
const subtitle = this.subtitleParser.parse(filename);
this.player.play(video, audio, subtitle);
}
}
// Usage (Simple!)
const player = new MediaPlayerFacade();
player.playVideo('movie.mp4');
Use cases: Library wrappers, simplifying complex processes
Behavioral Patterns
Observer
Notifies multiple objects of state changes.
class EventEmitter {
constructor() {
this.listeners = {};
}
on(event, callback) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(callback);
}
emit(event, data) {
if (this.listeners[event]) {
this.listeners[event].forEach(callback => callback(data));
}
}
}
// Usage
const store = new EventEmitter();
store.on('userLoggedIn', (user) => {
console.log(`Welcome, ${user.name}!`);
});
store.on('userLoggedIn', (user) => {
analytics.track('login', { userId: user.id });
});
store.emit('userLoggedIn', { id: 1, name: 'Alice' });
Use cases: Event systems, state management, reactive programming
Strategy
Makes algorithms interchangeable.
// Payment strategies
const paymentStrategies = {
creditCard: (amount) => {
console.log(`Credit card payment: ${amount}`);
return { method: 'creditCard', fee: amount * 0.03 };
},
bankTransfer: (amount) => {
console.log(`Bank transfer: ${amount}`);
return { method: 'bankTransfer', fee: 0 };
},
paypal: (amount) => {
console.log(`PayPal payment: ${amount}`);
return { method: 'paypal', fee: amount * 0.04 };
}
};
class PaymentProcessor {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
pay(amount) {
return this.strategy(amount);
}
}
// Usage
const processor = new PaymentProcessor(paymentStrategies.creditCard);
processor.pay(1000);
processor.setStrategy(paymentStrategies.bankTransfer);
processor.pay(1000);
Use cases: Sorting algorithms, switching authentication methods, pricing calculations
Command
Encapsulates operations as objects.
// Command interface
class Command {
execute() { throw new Error('Not implemented'); }
undo() { throw new Error('Not implemented'); }
}
// Concrete command
class AddTextCommand extends Command {
constructor(editor, text) {
super();
this.editor = editor;
this.text = text;
}
execute() {
this.editor.content += this.text;
}
undo() {
this.editor.content = this.editor.content.slice(0, -this.text.length);
}
}
// Executor
class CommandExecutor {
constructor() {
this.history = [];
}
execute(command) {
command.execute();
this.history.push(command);
}
undo() {
const command = this.history.pop();
if (command) {
command.undo();
}
}
}
// Usage
const editor = { content: '' };
const executor = new CommandExecutor();
executor.execute(new AddTextCommand(editor, 'Hello '));
executor.execute(new AddTextCommand(editor, 'World'));
console.log(editor.content); // 'Hello World'
executor.undo();
console.log(editor.content); // 'Hello '
Use cases: Undo/Redo functionality, transactions, task queues
Summary
Design patterns function as a common language for software design and provide proven solutions to common problems. However, the goal is not to apply patterns for their own sake, but to select patterns appropriate to the problem at hand.
← Back to list