Understanding the Factory Design Pattern with Node.js
Dinesh A
Posted on June 3, 2024
Design patterns are a crucial part of software engineering, helping to create robust, scalable, and maintainable code. There are various design patterns, the Factory Design Pattern is particularly useful for managing object creation.
Factory design pattern helps create objects without having to specify the exact class of the object that will be created. This is particularly useful in scenarios where the exact type of object may not be known until runtime.
Use cases of Factory Design Pattern
Whenever we create objects that have similar behavior or are closely related, we can use a factory design pattern.
- In Swiggy, when we are creating orders, orders can be of type dine-in, delivery, or take-out. We have to decide at runtime what type of order should be created.
- In Ola, when we are booking a ride, rides can be of type solo rides, share rides, or luxury rides. We have to decide at runtime what type of ride should be created. In both examples, the creation process is encapsulated using the factory design pattern.
Understanding with an example
Let's take an example of a notification system, there are different types of notifications including Email, SMS, and Push notifications. The type of notification required will be decided during runtime.
In Factory Pattern, the code will be split into 4 major parts.
- Interface (Notification interface)
- Concrete Classes (EmailNotification, SMSNotification, PushNotification)
- Factory (Notification Factory)
- Client
The interface acts as a blueprint for concrete classes. Each Concrete class extends the interface and provides its implementation of methods declared in the interface. Factory determines which concrete class to instantiate based on input parameters. The client utilizes the Factory to create instances of objects and Interacts with the created objects to perform actions.
Code
Notification Interface
First, we'll define a common interface for all types of notifications. In Nodejs, we'll use a base class with an abstract method to achieve this.
class Notification {
send(message) {
throw new Error('Method "send()" must be implemented.');
}
}
module.exports = Notification;
Concrete Classes
Now, we'll create concrete classes for Email, SMS, and Push notifications that extend the base Notification class.
const Notification = require('./Notification');
class EmailNotification extends Notification {
send(message) {
console.log(`Sending email with message: ${message}`);
// Email sending logic here
}
}
class SMSNotification extends Notification {
send(message) {
console.log(`Sending SMS with message: ${message}`);
// SMS sending logic here
}
}
class PushNotification extends Notification {
send(message) {
console.log(`Sending push notification with message: ${message}`);
// Push notification sending logic here
}
}
module.exports = { EmailNotification, SMSNotification, PushNotification };
Notification Factory
Now, we'll create a factory class that will return the appropriate notification object based on the input.
const { EmailNotification, SMSNotification, PushNotification } = require('./notifications/Notifications');
class NotificationFactory {
static createNotification(type) {
switch (type) {
case 'email':
return new EmailNotification();
case 'sms':
return new SMSNotification();
case 'push':
return new PushNotification();
default:
throw new Error('Unknown notification type');
}
}
}
module.exports = NotificationFactory;
Client
Finally, we'll use the factory to create notification objects in our client code.
const NotificationFactory = require('./notifications/NotificationFactory');
const notificationType = process.argv[2]; // 'email', 'sms', or 'push'
const message = 'Hello, this is a test message!';
const notification = NotificationFactory.createNotification(notificationType);
notification.send(message);
What benefit did we get by following this factory design pattern ?
- Encapsulation: It encapsulates the instantiation logic, making the code cleaner and more modular.
- Decoupling: It decouples the client code from the concrete classes, promoting flexibility and maintainability.
- Single Responsibility Principle: It adheres to the Single Responsibility Principle by delegating the creation logic to the factory class.
- Open/Closed Principle: It adheres to the Open/Closed Principle, allowing new types to be added without modifying the existing client code.
Conclusions
The Factory Design Pattern is one of the best ways to manage object creation in a clean and decoupled manner. By incorporating design patterns like the Factory Design Pattern into development practices, we can write more robust, scalable, and maintainable code.
If you found this article helpful, connect with me on LinkedIn and follow my posts on DEV.to. Share your thoughts and questions in the comments below!
Posted on June 3, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.