How to Implement Design Patterns in TypeScript: Factory
What is the Factory Design Pattern?
The Factory design pattern is a creational design pattern that provides an interface for creating objects but allows subclasses to decide which class to instantiate. It encapsulates the object creation logic within a factory method, providing a centralized way to create objects without exposing the instantiation details to the client.
The key participants in the Factory pattern are:
Product: This is the interface or abstract class that defines the common interface for the objects that the factory method will create.
Concrete Products: These are the classes that implement or extend the Product interface or class. Each concrete product represents a specific type of object that the factory can create.
Creator (Factory): This is the class or method responsible for creating and returning instances of the concrete products. It encapsulates the object creation logic and provides a common interface to the client for creating objects.
The main advantages of using the Factory pattern are:
Encapsulation: The Factory pattern encapsulates the object creation logic within a centralized factory, hiding the instantiation details from the client. This enhances code organization and maintainability.
Abstraction: The client code depends on the abstract Product interface or class, rather than concrete implementations. This promotes loose coupling and allows for flexibility in switching or adding new product implementations.
Extensibility: The Factory pattern allows for easy extension by adding new concrete product classes and corresponding creation logic in the factory. It provides a scalable solution to handle a growing set of product types.
Decoupling: By delegating the responsibility of object creation to a factory, the client code is decoupled from specific product classes. It simplifies the client code and reduces dependencies.
Overall, the Factory pattern provides a way to create objects through a centralized factory, promoting code reusability, flexibility, and maintainability. It is particularly useful when you have multiple product types or variations and want to abstract the object creation process.
How to Implement Factory?
Let’s consider an example of a messaging application where users can send messages through different channels such as email, SMS, and social media. In this scenario, the Factory design pattern can be applied to create the appropriate message sender based on the selected channel.
The Factory design pattern provides an interface for creating objects but allows subclasses to decide which class to instantiate. It encapsulates the object creation logic in a factory method, providing flexibility and extensibility.
Here’s how you can apply the Factory pattern in TypeScript for the messaging application:
- Define the Product interface: Create an interface called
MessageSender
that declares a method for sending messages, such assendMessage()
.
interface MessageSender {
sendMessage(message: string): void;
}
2. Implement the Concrete Products: Create classes that implement the MessageSender
interface for each messaging channel. For example, you can have classes like EmailSender
, SMSSender
, and SocialMediaSender
. Each class will provide its own implementation of the sendMessage()
method.
class EmailSender implements MessageSender {
sendMessage(message: string): void {
// Implement email sending logic here
console.log(`Sending email: ${message}`);
}
}
class SMSSender implements MessageSender {
sendMessage(message: string): void {
// Implement SMS sending logic here
console.log(`Sending SMS: ${message}`);
}
}
class SocialMediaSender implements MessageSender {
sendMessage(message: string): void {
// Implement social media sending logic here
console.log(`Sending social media message: ${message}`);
}
}
3. Implement the Factory: Create a factory class called MessageSenderFactory
that encapsulates the creation logic and returns the appropriate MessageSender
object based on the requested channel.
class MessageSenderFactory {
static createMessageSender(channel: string): MessageSender {
if (channel === 'email') {
return new EmailSender();
} else if (channel === 'sms') {
return new SMSSender();
} else if (channel === 'social') {
return new SocialMediaSender();
} else {
throw new Error('Invalid channel');
}
}
}
4. Now, when a user wants to send a message, you can use the factory to create the appropriate MessageSender
object based on the selected channel.
const channel = 'email'; // Or any other channel
const message = 'Hello, World!';
const messageSender = MessageSenderFactory.createMessageSender(channel);
messageSender.sendMessage(message);
By utilizing the Factory pattern, you can decouple the client code from the specific message sender implementations and provide a centralized way to create message senders based on the requested channel. This promotes flexibility, maintainability, and separation of concerns in your project.