10 Design Patterns Explained in 10 Minutes

Software Design Patterns

What Are Software Design Patterns?

Software design patterns provide reusable solutions to common software design problems. They help standardize approaches, making code easier to understand, maintain, and extend. The influential book Design Patterns by the “Gang of Four” categorizes design patterns into three types:

  • Creational Patterns: Handle object creation mechanisms.
  • Structural Patterns: Define how objects and components relate.
  • Behavioral Patterns: Govern object communication and workflows.

Using design patterns effectively can improve code quality, but excessive or incorrect use may introduce unnecessary complexity.

Key Software Design Patterns

Singleton Pattern

The Singleton pattern ensures that only one instance of a class exists and provides a global point of access.

Use Cases: Database connections, Logging services, Global configurations

Example (JavaScript):

class Singleton {
  constructor() {
    if (!Singleton.instance) {
      Singleton.instance = this;
    }
    return Singleton.instance;
  }
}
const instance1 = new Singleton();
const instance2 = new Singleton();
console.log(instance1 === instance2); // true

Prototype Pattern

The Prototype pattern allows new objects to be created by cloning an existing object rather than instantiating a new class.

Use Cases: Performance optimization, Object cloning

Example (JavaScript):

const carPrototype = {
  start: function () {
    console.log("Engine started!");
  }
};
const myCar = Object.create(carPrototype);
myCar.start(); // Engine started!

Builder Pattern

The Builder pattern simplifies object creation when multiple configuration options exist.

Use Cases: Constructing complex objects, UI component creation

Example (JavaScript):

class CarBuilder {
  constructor() {
    this.car = {};
  }
  setColor(color) {
    this.car.color = color;
    return this;
  }
  setWheels(wheels) {
    this.car.wheels = wheels;
    return this;
  }
  build() {
    return this.car;
  }
}
const myCar = new CarBuilder().setColor("red").setWheels(4).build();
console.log(myCar);

Factory Pattern

The Factory pattern encapsulates object creation logic, making code more modular and easier to extend.

Use Cases: Dependency injection, Platform-specific object creation

Example (JavaScript):

class CarFactory {
  static createCar(type) {
    const carTypes = {
      sedan: { type: "sedan", doors: 4 },
      coupe: { type: "coupe", doors: 2 },
    };
    return carTypes[type] || null;
  }
}
const myCar = CarFactory.createCar("sedan");
console.log(myCar);

Facade Pattern

The Facade pattern provides a simplified interface to a complex system.

Use Cases: Simplifying APIs, Reducing dependencies

Example (JavaScript):

class Computer {
  start() { console.log("Computer starting..."); }
  shutdown() { console.log("Computer shutting down..."); }
}
class ComputerFacade {
  constructor() {
    this.computer = new Computer();
  }
  turnOn() {
    this.computer.start();
  }
  turnOff() {
    this.computer.shutdown();
  }
}
const myComputer = new ComputerFacade();
myComputer.turnOn();

Proxy Pattern

The Proxy pattern acts as an intermediary to control access to an object.

Use Cases: Lazy loading, Security proxies

Example (JavaScript):

class RealImage {
  constructor(filename) {
    this.filename = filename;
  }
  display() {
    console.log("Displaying " + this.filename);
  }
}
class ProxyImage {
  constructor(filename) {
    this.realImage = null;
    this.filename = filename;
  }
  display() {
    if (!this.realImage) {
      this.realImage = new RealImage(this.filename);
    }
    this.realImage.display();
  }
}
const image = new ProxyImage("test.jpg");
image.display();

Iterator Pattern

The Iterator pattern provides a way to access elements of a collection sequentially without exposing its internal structure.

Use Cases: Collection traversal, Data processing

Example (JavaScript):

class Iterator {
  constructor(items) {
    this.items = items;
    this.index = 0;
  }
  next() {
    return this.index < this.items.length ? { value: this.items[this.index++], done: false } : { done: true };
  }
}
const iterator = new Iterator(["a", "b", "c"]);
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());

Observer Pattern

The Observer pattern enables a one-to-many dependency between objects, ensuring changes to one object are reflected in its dependents.

Use Cases: Event handling, Reactive programming

Example (JavaScript):

class Subject {
  constructor() {
    this.observers = [];
  }
  subscribe(observer) {
    this.observers.push(observer);
  }
  notify(data) {
    this.observers.forEach(observer => observer.update(data));
  }
}
class Observer {
  update(data) {
    console.log("Received update: " + data);
  }
}
const subject = new Subject();
const observer1 = new Observer();
subject.subscribe(observer1);
subject.notify("Hello World");

Mediator Pattern

The Mediator pattern centralizes communication between objects to reduce dependencies.

Use Cases: Chat applications, Workflow coordination

Example (JavaScript):

class Mediator {
  constructor() {
    this.participants = [];
  }
  register(participant) {
    this.participants.push(participant);
  }
  send(message, sender) {
    this.participants.forEach(participant => {
      if (participant !== sender) {
        participant.receive(message);
      }
    });
  }
}
class Participant {
  constructor(name, mediator) {
    this.name = name;
    this.mediator = mediator;
    mediator.register(this);
  }
  send(message) {
    this.mediator.send(message, this);
  }
  receive(message) {
    console.log(this.name + " received: " + message);
  }
}
const mediator = new Mediator();
const p1 = new Participant("Alice", mediator);
p1.send("Hello");