Introduction to Design Patterns - Commonly Used Design Patterns

17 min read | 2024.12.27

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