Factory Pattern | Design Patterns

Overview

Narendra Singh Rathore
3 min readAug 15, 2024

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

  1. Encapsulation: The creation logic is abstracted away from the client code, making the code cleaner and easier to maintain.
  2. Scalability: New types of objects can be easily added by introducing new classes that implement the factory interface.
  3. Loose Coupling: The client code is decoupled from the specific classes it needs to instantiate, reducing dependencies and improving flexibility.
  4. Single Responsibility Principle: The factory method handles the creation of objects, allowing other parts of the code to focus on their specific tasks.
  5. Consistency: Ensures that objects are created in a consistent manner, especially when complex creation logic is required.

Disadvantages

  1. Increased Complexity: The pattern can introduce unnecessary complexity in scenarios where simple object creation would suffice.
  2. Overhead: The indirection introduced by the pattern might lead to performance overhead, especially in cases where object creation is straightforward.
  3. 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

  1. Dependency Injection: By injecting the NotificationFactory into UserService, you separate the concerns of creating notifications from the logic of sending them. This makes UserService easier to test and maintain.
  2. Flexibility: You can easily switch or extend the NotificationFactory without changing the UserService. For example, if you want to use a different factory implementation, you only need to update the injection point.
  3. Decoupling: The UserService is decoupled from the specific implementations of notifications. It only depends on the NotificationFactory to create the notifications, making the system more modular and adaptable.
  4. Testability: In unit tests, you can mock or stub the NotificationFactory to control the behavior of the UserService without relying on actual notification implementations.

Thanks for reading.

--

--

No responses yet