Chapter 16: Design Patterns
Design patterns are typical solutions to common problems in software design. They provide reusable templates and best practices that can be applied to solve various design challenges. This chapter introduces some common design patterns used in JavaScript.
Common Design Patterns
1. Singleton Pattern
The Singleton pattern ensures that a class has only one instance and provides a global point of access to that instance. This is useful when exactly one object is needed to coordinate actions across the system.
Example
class Singleton {
constructor() {
if (Singleton.instance) {
return Singleton.instance;
}
Singleton.instance = this;
}
getInstance() {
return Singleton.instance;
}
}
const instance1 = new Singleton();
const instance2 = new Singleton();
console.log(instance1 === instance2); // Output: true
In this example, the Singleton class only allows one instance to be created. Subsequent attempts to create new instances return the same existing instance.
2. Observer Pattern
The Observer pattern is a behavioral design pattern where an object (known as the subject) maintains a list of dependents (observers) that are notified of any changes to the subject’s state. This is commonly used in event-driven systems.
Example
class Subject {
constructor() {
this.observers = [];
}
addObserver(observer) {
this.observers.push(observer);
}
notifyObservers(message) {
this.observers.forEach((observer) => observer.update(message));
}
}
class Observer {
update(message) {
console.log(`Observer received message: ${message}`);
}
}
const subject = new Subject();
const observer1 = new Observer();
const observer2 = new Observer();
subject.addObserver(observer1);
subject.addObserver(observer2);
subject.notifyObservers("Hello Observers!");
// Output:
// Observer received message: Hello Observers!
// Observer received message: Hello Observers!
In this example, the Subject class maintains a list of observers. When the subject's state changes, it notifies all registered observers.
3. Module Pattern
The Module pattern provides a way to encapsulate private data and expose only the public API. It helps in organizing code and managing dependencies by creating modular and self-contained units.
Example
const Module = (function () {
let privateData = "I am private";
function privateMethod() {
console.log("I am a private method");
}
return {
publicMethod: function () {
console.log("I am a public method");
privateMethod();
},
getPrivateData: function () {
return privateData;
},
};
})();
Module.publicMethod(); // Output: I am a public method \n I am a private method
console.log(Module.getPrivateData()); // Output: I am private
In this example, the Module is an immediately invoked function expression (IIFE) that encapsulates private data and methods while exposing public methods through the returned object.
4. Factory Pattern
The Factory pattern provides a method for creating objects in a super class but allows subclasses to alter the type of objects that will be created.
Example
class Dog {
speak() {
console.log("Woof!");
}
}
class Cat {
speak() {
console.log("Meow!");
}
}
class AnimalFactory {
createAnimal(type) {
if (type === "dog") {
return new Dog();
} else if (type === "cat") {
return new Cat();
} else {
throw new Error("Unknown animal type");
}
}
}
const factory = new AnimalFactory();
const dog = factory.createAnimal("dog");
const cat = factory.createAnimal("cat");
dog.speak(); // Output: Woof!
cat.speak(); // Output: Meow!
In this example, AnimalFactory is used to create objects of type Dog or Cat based on the provided type.
5. Decorator Pattern
The Decorator pattern allows adding new behavior to an object dynamically without altering its structure. It is typically used to extend functionalities of classes.
Example
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;
}
}
const coffee = new Coffee();
const milkCoffee = new MilkDecorator(coffee);
const milkAndSugarCoffee = new SugarDecorator(milkCoffee);
console.log(coffee.cost()); // Output: 5
console.log(milkCoffee.cost()); // Output: 7
console.log(milkAndSugarCoffee.cost()); // Output: 8
In this example, MilkDecorator and SugarDecorator are used to dynamically add additional cost to the Coffee object.
6. Prototype Pattern
The Prototype pattern creates new objects by copying an existing object, which allows for efficient object creation. It is useful when object creation is expensive and you want to avoid repeated instantiation.
Example
const carPrototype = {
drive() {
console.log("Driving a car");
},
clone() {
const newCar = Object.create(this);
return newCar;
},
};
const myCar = carPrototype.clone();
myCar.drive(); // Output: Driving a car
In this example, carPrototype serves as a prototype object that can be cloned to create new car objects.
Summary
In this chapter, we covered several common design patterns in JavaScript:
- Singleton Pattern: Ensures a single instance of a class.
- Observer Pattern: Allows objects to listen and react to events or changes.
- Module Pattern: Encapsulates private data and exposes public methods.
- Factory Pattern: Creates objects without specifying the exact class of the object.
- Decorator Pattern: Adds new behavior to objects dynamically.
- Prototype Pattern: Creates new objects by copying an existing prototype.
Understanding and applying these design patterns will help you create more robust, maintainable, and scalable JavaScript applications.