Factory Pattern | Design Patterns
Overview
The Factory Pattern is a creational design pattern that provides an interface for creating objects in a super class, but allows subclasses to alter the type of objects that will be created. Instead of directly calling a constructor to create an object, you use a factory method to create it.
Advantages
- Encapsulation: The creation logic is abstracted away from the client code, making the code cleaner and easier to maintain.
- Scalability: New types of objects can be easily added by introducing new classes that implement the factory interface.
- Loose Coupling: The client code is decoupled from the specific classes it needs to instantiate, reducing dependencies and improving flexibility.
- Single Responsibility Principle: The factory method handles the creation of objects, allowing other parts of the code to focus on their specific tasks.
- Consistency: Ensures that objects are created in a consistent manner, especially when complex creation logic is required.
Disadvantages
- Increased Complexity: The pattern can introduce unnecessary complexity in scenarios where simple object creation would suffice.
- Overhead: The indirection introduced by the pattern might lead to performance overhead, especially in cases where object creation is straightforward.
- Code Bloat: It can lead to more classes and interfaces, increasing the codebase size and making it harder to navigate.
TypeScript Code Sample
Overview
Suppose we have a UserService
class that needs to send notifications to users. Instead of creating notifications directly, it will use a NotificationFactory
to obtain the appropriate notification type. This allows us to easily switch or extend notification types without modifying the UserService
class.
// Step 1: Define a Notification interface and concrete implementations
interface Notification {
send(message: string): void;
}
class EmailNotification implements Notification {
send(message: string): void {
console.log(`Sending Email with message: ${message}`);
}
}
class SMSNotification implements Notification {
send(message: string): void {
console.log(`Sending SMS with message: ${message}`);
}
}
class PushNotification implements Notification {
send(message: string): void {
console.log(`Sending Push Notification with message: ${message}`);
}
}
// Step 2: Create a Factory class for Notification creation
class NotificationFactory {
static createNotification(type: string): Notification {
switch (type) {
case 'email':
return new EmailNotification();
case 'sms':
return new SMSNotification();
case 'push':
return new PushNotification();
default:
throw new Error('Invalid notification type');
}
}
}
// Step 3: Create a UserService class that depends on NotificationFactory
class UserService {
private notificationFactory: NotificationFactory;
constructor(notificationFactory: NotificationFactory) {
this.notificationFactory = notificationFactory;
}
notifyUser(userId: string, notificationType: string, message: string): void {
const notification = this.notificationFactory.createNotification(notificationType);
notification.send(message);
}
}
// Step 4: Inject the NotificationFactory into UserService and use it
const factory = NotificationFactory;
const userService = new UserService(factory);
// Sending notifications
userService.notifyUser('user123', 'email', 'Welcome to our service!');
// Output: Sending Email with message: Welcome to our service!
userService.notifyUser('user123', 'sms', 'Your code is 1234.');
// Output: Sending SMS with message: Your code is 1234.
userService.notifyUser('user123', 'push', 'You have a new message.');
// Output: Sending Push Notification with message: You have a new message.
How It Helps Improve Code
- Dependency Injection: By injecting the
NotificationFactory
intoUserService
, you separate the concerns of creating notifications from the logic of sending them. This makesUserService
easier to test and maintain. - Flexibility: You can easily switch or extend the
NotificationFactory
without changing theUserService
. For example, if you want to use a different factory implementation, you only need to update the injection point. - Decoupling: The
UserService
is decoupled from the specific implementations of notifications. It only depends on theNotificationFactory
to create the notifications, making the system more modular and adaptable. - Testability: In unit tests, you can mock or stub the
NotificationFactory
to control the behavior of theUserService
without relying on actual notification implementations.
Thanks for reading.