Deep Dive into Design Patterns: Implementing Design Patterns in JavaScript
Codingmart Technologies
We help companies of all sizes from Startups to Unicorns to Enterprises; to pioneer the next generation technologies.
Hey there, coding enthusiasts!
In last week's newsletter, we introduced the concept of Design Patterns and discussed their importance in writing efficient, scalable, and maintainable code. Now that we have a high-level understanding of what design patterns are and why they matter, it’s time to dive deeper into some of the most important patterns. In this newsletter, we'll take a closer look at individual design patterns, their specific use cases, and how to implement them in JavaScript with real-world examples.
By the end of this content, you'll not only understand the theory behind these patterns but also gain practical knowledge of how to apply them in your own projects. Let’s get started!
1. Singleton Pattern
The Singleton Pattern ensures that a class has only one instance and provides a global point of access to it. This is useful in situations where you need to control access to some shared resource, such as a database connection or a global configuration object.
Example in JavaScript:
class Singleton {
constructor() {
if (Singleton.instance) {
return Singleton.instance;
}
this.data = "Singleton Data";
Singleton.instance = this;
return this;
}
getData() {
return this.data;
}
}
const singleton1 = new Singleton();
console.log(singleton1.getData()); // Output: Singleton Data
const singleton2 = new Singleton();
console.log(singleton2.getData()); // Output: Singleton Data
console.log(singleton1 === singleton2); // Output: true
Explanation: In this example, the Singleton class restricts the instantiation to a single instance. Every time you try to create a new instance, it returns the same object, ensuring that only one instance of the class is ever created.
2. Factory Pattern
The Factory Pattern is a creational pattern that provides a way to create objects without specifying the exact class of the object that will be created. This pattern is particularly useful when the exact type of the object isn't known until runtime.
Example in JavaScript:
class Car {
constructor() {
this.type = "Car";
}
drive() {
console.log("Driving a car...");
}
}
class Truck {
constructor() {
this.type = "Truck";
}
drive() {
console.log("Driving a truck...");
}
}
class VehicleFactory {
createVehicle(vehicleType) {
switch(vehicleType) {
case "car":
return new Car();
case "truck":
return new Truck();
default:
throw new Error("Vehicle type not recognized");
}
}
}
const factory = new VehicleFactory();
const myCar = factory.createVehicle("car");
myCar.drive(); // Output: Driving a car...
const myTruck = factory.createVehicle("truck");
myTruck.drive(); // Output: Driving a truck...
Explanation: The VehicleFactory class creates objects based on the input type. This way, you can create different types of vehicles (Car, Truck) without having to specify the exact class.
3. Observer Pattern
The Observer Pattern defines a one-to-many relationship between objects, so when one object changes state, all its dependents are notified and updated automatically. This pattern is useful for implementing distributed event handling systems.
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(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();
const observer2 = new Observer();
subject.subscribe(observer1);
subject.subscribe(observer2);
subject.notify("Hello Observers!");
// Output: Observer received data: Hello Observers!
// Output: Observer received data: Hello Observers!
subject.unsubscribe(observer1);
subject.notify("Another notification!");
// Output: Observer received data: Another notification!
Explanation: In this example, the Subject class maintains a list of observers and notifies them of any changes. The Observer class implements an update method that receives data from the Subject. This pattern is often used in event handling systems where multiple objects need to react to changes in another object.
4. Strategy Pattern
The Strategy Pattern allows you to define a family of algorithms, encapsulate each one, and make them interchangeable. This pattern is particularly useful when you have multiple ways of doing something, and you want to switch between them at runtime.
Example in JavaScript:
class CreditCardPayment {
pay(amount) {
console.log(`Paid ${amount} using Credit Card`);
}
}
class PayPalPayment {
pay(amount) {
console.log(`Paid ${amount} using PayPal`);
}
}
class PaymentContext {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
executeStrategy(amount) {
this.strategy.pay(amount);
}
}
const creditCard = new CreditCardPayment();
const payPal = new PayPalPayment();
const paymentContext = new PaymentContext(creditCard);
paymentContext.executeStrategy(100);
// Output: Paid 100 using Credit Card
paymentContext.setStrategy(payPal);
paymentContext.executeStrategy(200);
// Output: Paid 200 using PayPal
Explanation: The PaymentContext class is configured with a payment strategy and uses it to process payments. The strategy can be changed dynamically, allowing the payment method to be switched at runtime.
Final Thoughts
Incorporate these patterns into your projects, and you'll find that they not only solve immediate problems but also pave the way for more sustainable and adaptable software development.