JavaScript design patterns are reusable solutions to common problems that occur in software development. These patterns are like templates that can be applied to specific coding scenarios to make your code more organized, efficient, and maintainable. They are a key part of writing clean, scalable, and high-quality JavaScript code.
Using design patterns in your JavaScript projects can significantly improve your code by:
Let's explore some of the most popular JavaScript design patterns with practical examples:
The Singleton Pattern ensures that a class has only one instance and provides a global point of access to that instance.
const Singleton = (function () {
let instance;
function createInstance() {
const object = new Object("I am the instance");
return object;
}
return {
getInstance: function () {
if (!instance) {
instance = createInstance();
}
return instance;
},
};
})();
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // true
Use Case: Best suited for scenarios where only one instance of a class is needed, such as a configuration manager.
The Module Pattern helps in organizing your code into separate, reusable, and encapsulated pieces, making your code more modular.
const MyModule = (function () {
const privateVar = "I'm private";
function privateMethod() {
console.log(privateVar);
}
return {
publicMethod: function () {
privateMethod();
},
};
})();
MyModule.publicMethod(); // Output: I'm private
Use Case: Useful for managing code within namespaces and avoiding global scope pollution.
The Observer Pattern is perfect for creating a subscription mechanism to allow one object (subject) to notify other objects (observers) of state changes.
Example:
class Subject {
constructor() {
this.observers = [];
}
subscribe(observer) {
this.observers.push(observer);
}
unsubscribe(observer) {
this.observers = this.observers.filter(sub => sub !== observer);
}
notify(data) {
this.observers.forEach(observer => observer.update(data));
}
}
class Observer {
update(data) {
console.log("Observer received data:", data);
}
}
const subject = new Subject();
const observer1 = new Observer();
subject.subscribe(observer1);
subject.notify("Hello, World!"); // Output: Observer received data: Hello, World!
Use Case: Ideal for implementing event handling, like notifications or real-time updates.
The Factory Pattern provides a way to create objects without specifying the exact class of the object that will be created.
Example:
class Car {
constructor() {
this.type = "Car";
}
}
class Bike {
constructor() {
this.type = "Bike";
}
}
class VehicleFactory {
createVehicle(vehicleType) {
switch (vehicleType) {
case "car":
return new Car();
case "bike":
return new Bike();
default:
return null;
}
}
}
const factory = new VehicleFactory();
const myCar = factory.createVehicle("car");
console.log(myCar.type); // Car
Use Case: Useful for creating different instances of objects based on dynamic conditions.
The Strategy Pattern allows you to define different algorithms for a task and switch between them at runtime.
Example:
class PaymentContext {
setStrategy(strategy) {
this.strategy = strategy;
}
executeStrategy(amount) {
return this.strategy.pay(amount);
}
}
class PayPalStrategy {
pay(amount) {
console.log(`Paid $${amount} using PayPal`);
}
}
class CreditCardStrategy {
pay(amount) {
console.log(`Paid $${amount} using Credit Card`);
}
}
const payment = new PaymentContext();
payment.setStrategy(new PayPalStrategy());
payment.executeStrategy(100); // Paid $100 using PayPal
Use Case: Best used when you have multiple ways to perform a task, like payment methods.