Design patterns are established solutions to recurring problems in software design. They provide a common vocabulary for developers to communicate solutions and help avoid reinventing the wheel. Knowing key design patterns can greatly improve your ability to design scalable and maintainable systems.
In this article, we’ll explore several important design patterns every developer should know, categorized into three main types: creational, structural, and behavioral patterns, along with code examples for each.
1. Creational Design Patterns
Creational patterns deal with object creation mechanisms, optimizing the process of creating objects to suit various situations. These patterns provide solutions to common problems related to instantiating objects and controlling object creation.
a) Singleton Pattern
The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. It is commonly used in scenarios where a single instance of a class is needed to coordinate actions across a system.
// Singleton Pattern Example in JavaScript
class Singleton {
constructor() {
if (Singleton.instance) {
return Singleton.instance;
}
this.data = "Singleton Data";
Singleton.instance = this;
}
getData() {
return this.data;
}
}
// Usage
const instance1 = new Singleton();
const instance2 = new Singleton();
console.log(instance1 === instance2); // true
Example Use Case: A logging service where only one log file is written by the application.
b) Factory Method Pattern
The Factory Method pattern provides a way to create objects without specifying the exact class of the object that will be created. This pattern allows a class to delegate the responsibility of object creation to subclasses, promoting flexibility and scalability.
// Factory Method Example in JavaScript
class Vehicle {
constructor() {
this.type = "Generic Vehicle";
}
drive() {
return `Driving a ${this.type}`;
}
}
class Car extends Vehicle {
constructor() {
super();
this.type = "Car";
}
}
class Truck extends Vehicle {
constructor() {
super();
this.type = "Truck";
}
}
class VehicleFactory {
static createVehicle(type) {
switch (type) {
case "car":
return new Car();
case "truck":
return new Truck();
default:
return new Vehicle();
}
}
}
// Usage
const car = VehicleFactory.createVehicle("car");
console.log(car.drive()); // "Driving a Car"
Example Use Case: A framework for different database management systems (e.g., MySQL, PostgreSQL) where each database type can be instantiated using a factory method.
c) Builder Pattern
The Builder pattern is used when constructing complex objects step by step. It separates the construction of an object from its representation, allowing you to create different representations of the same type of object.
// Builder Pattern Example in JavaScript
class House {
constructor() {
this.rooms = 0;
this.garage = false;
this.swimmingPool = false;
}
}
class HouseBuilder {
constructor() {
this.house = new House();
}
addRooms(num) {
this.house.rooms = num;
return this;
}
addGarage() {
this.house.garage = true;
return this;
}
addSwimmingPool() {
this.house.swimmingPool = true;
return this;
}
build() {
return this.house;
}
}
// Usage
const myHouse = new HouseBuilder().addRooms(4).addGarage().addSwimmingPool().build();
console.log(myHouse); // { rooms: 4, garage: true, swimmingPool: true }
Example Use Case: Building complex objects like documents, where different parts of the document (header, footer, body) are created separately.
2. Structural Design Patterns
Structural patterns focus on how objects and classes are composed to form larger structures while keeping these structures flexible and efficient.
a) Adapter Pattern
The Adapter pattern allows two incompatible interfaces to work together. It acts as a bridge between two objects by converting one interface into another that the client expects.
// Adapter Pattern Example in JavaScript
class OldSystem {
oldMethod() {
return "Old System Output";
}
}
class NewSystem {
newMethod() {
return "New System Output";
}
}
class Adapter {
constructor() {
this.oldSystem = new OldSystem();
}
newMethod() {
return this.oldSystem.oldMethod();
}
}
// Usage
const adaptedSystem = new Adapter();
console.log(adaptedSystem.newMethod()); // "Old System Output"
Example Use Case: Integrating an old system with new code by adapting legacy interfaces to work with modern ones.
b) Decorator Pattern
The Decorator pattern allows behavior to be added to an individual object dynamically, without affecting the behavior of other objects from the same class. This is particularly useful for adhering to the Open/Closed principle.
// Decorator Pattern Example in JavaScript
class Coffee {
cost() {
return 5;
}
}
class MilkDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() {
return this.coffee.cost() + 2;
}
}
class SugarDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() {
return this.coffee.cost() + 1;
}
}
// Usage
let coffee = new Coffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
console.log(coffee.cost()); // 8
Example Use Case: Adding functionality like encryption or logging to an existing class without altering its structure.
c) Facade Pattern
The Facade pattern provides a simplified interface to a complex subsystem. It hides the complexity of the subsystem and provides an easy-to-use interface to clients.
// Facade Pattern Example in JavaScript
class PaymentProcessor {
processPayment(amount) {
console.log(`Processing payment of $${amount}`);
}
}
class InventorySystem {
checkInventory(product) {
console.log(`Checking inventory for ${product}`);
}
}
class ShippingSystem {
arrangeShipping(product) {
console.log(`Arranging shipping for ${product}`);
}
}
class OrderFacade {
constructor() {
this.paymentProcessor = new PaymentProcessor();
this.inventorySystem = new InventorySystem();
this.shippingSystem = new ShippingSystem();
}
placeOrder(product, amount) {
this.inventorySystem.checkInventory(product);
this.paymentProcessor.processPayment(amount);
this.shippingSystem.arrangeShipping(product);
}
}
// Usage
const order = new OrderFacade();
order.placeOrder("Laptop", 1000);
Example Use Case: A complex library for multimedia processing (e.g., video encoding/decoding) can expose a simple interface for users who only need basic operations.
3. Behavioral Design Patterns
Behavioral patterns are concerned with the interaction and responsibility of objects. They help define communication patterns between objects and manage complex control flows.
a) Observer Pattern
The Observer pattern establishes a one-to-many relationship between objects, where one object (the subject) notifies a set of observer objects about changes in its state. This pattern is commonly used in event-driven systems.
// Observer Pattern Example in JavaScript
class Subject {
constructor() {
this.observers = [];
}
subscribe(observer) {
this.observers.push(observer);
}
unsubscribe(observer) {
this.observers = this.observers.filter(obs => obs !== observer);
}
notify() {
this.observers.forEach(observer => observer.update());
}
}
class Observer {
update() {
console.log("Observer updated!");
}
}
// Usage
const subject = new Subject();
const observer1 = new Observer();
const observer2 = new Observer();
subject.subscribe(observer1);
subject.subscribe(observer2);
subject.notify(); // Observer updated! (twice)
Example Use Case: A notification system where multiple components need to be informed when a specific event occurs, such as in a stock trading application where changes in stock prices need to notify multiple clients.
b) Strategy Pattern
The Strategy pattern allows you to define a family of algorithms, encapsulate each one, and make them interchangeable. This pattern is useful when you have multiple ways to perform an operation, but the choice of which to use should be dynamic.
// Strategy Pattern Example in JavaScript
class StrategyA {
execute() {
console.log("Executing Strategy A");
}
}
class StrategyB {
execute() {
console.log("Executing Strategy B");
}
}
class Context {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
executeStrategy() {
this.strategy.execute();
}
}
// Usage
const context = new Context(new StrategyA());
context.executeStrategy(); // "Executing Strategy A"
context.setStrategy(new StrategyB());
context.executeStrategy(); // "Executing Strategy B"
Example Use Case: A payment system where the user can choose different payment methods (credit card, PayPal, etc.) at runtime.
Conclusion
Understanding and using design patterns is essential for writing maintainable and scalable software. The Singleton, Factory Method, Adapter, Observer, and Strategy patterns are just a few examples of how design patterns can solve common design problems. By incorporating these patterns into your development process, you can build systems that are flexible, extensible, and easy to maintain over time.
