Learn how to implement the Mediator pattern in TypeScript using interfaces and strong typing for enhanced clarity and error checking.
The Mediator pattern is a behavioral design pattern that facilitates communication between different components (or objects) in a system by introducing a mediator object. This pattern helps reduce the dependencies between communicating components, promoting loose coupling and enhancing maintainability. In this section, we will explore how to implement the Mediator pattern in TypeScript, leveraging its strong typing and interface capabilities.
Before diving into the implementation, let’s briefly recap the Mediator pattern. The pattern involves three main components:
By using the Mediator pattern, we can centralize communication logic, making it easier to manage and modify.
In TypeScript, interfaces play a crucial role in defining contracts between different parts of the system. Let’s start by defining interfaces for the Mediator and Colleague components.
// Define the Mediator interface
interface Mediator {
notify(sender: Colleague, event: string): void;
}
// Define the Colleague interface
interface Colleague {
setMediator(mediator: Mediator): void;
}
In the above code, the Mediator interface declares a notify method that takes a Colleague and an event string as parameters. The Colleague interface declares a setMediator method to establish a connection with the mediator.
Let’s implement a simple chat room example using the Mediator pattern. In this example, the chat room acts as the mediator, and users are the colleagues.
We’ll start by creating a ChatRoom class that implements the Mediator interface.
class ChatRoom implements Mediator {
private users: Map<string, User> = new Map();
public registerUser(user: User): void {
this.users.set(user.getName(), user);
user.setMediator(this);
}
public notify(sender: Colleague, event: string): void {
this.users.forEach((user) => {
if (user !== sender) {
user.receive(event);
}
});
}
}
In the ChatRoom class, we maintain a list of users and provide a registerUser method to add users to the chat room. The notify method sends messages to all users except the sender.
Next, we’ll create a User class that implements the Colleague interface.
class User implements Colleague {
private mediator: Mediator | null = null;
private name: string;
constructor(name: string) {
this.name = name;
}
public setMediator(mediator: Mediator): void {
this.mediator = mediator;
}
public getName(): string {
return this.name;
}
public send(message: string): void {
console.log(`${this.name} sends: ${message}`);
if (this.mediator) {
this.mediator.notify(this, message);
}
}
public receive(message: string): void {
console.log(`${this.name} receives: ${message}`);
}
}
The User class has methods to send and receive messages. When a user sends a message, it is passed to the mediator, which then notifies other users.
TypeScript’s type system ensures that the contracts defined by interfaces are adhered to by the implementing classes. This provides several advantages:
Using TypeScript to implement the Mediator pattern offers several benefits:
We can further enhance our mediator implementation by using generics. This allows us to create more flexible and reusable components.
// Define a generic Mediator interface
interface GenericMediator<T> {
notify(sender: T, event: string): void;
}
// Define a generic Colleague interface
interface GenericColleague<T> {
setMediator(mediator: GenericMediator<T>): void;
}
// Implement a generic ChatRoom
class GenericChatRoom<T extends GenericColleague<T>> implements GenericMediator<T> {
private colleagues: Set<T> = new Set();
public registerColleague(colleague: T): void {
this.colleagues.add(colleague);
colleague.setMediator(this);
}
public notify(sender: T, event: string): void {
this.colleagues.forEach((colleague) => {
if (colleague !== sender) {
colleague.receive(event);
}
});
}
}
// Implement a generic User
class GenericUser implements GenericColleague<GenericUser> {
private mediator: GenericMediator<GenericUser> | null = null;
private name: string;
constructor(name: string) {
this.name = name;
}
public setMediator(mediator: GenericMediator<GenericUser>): void {
this.mediator = mediator;
}
public getName(): string {
return this.name;
}
public send(message: string): void {
console.log(`${this.name} sends: ${message}`);
if (this.mediator) {
this.mediator.notify(this, message);
}
}
public receive(message: string): void {
console.log(`${this.name} receives: ${message}`);
}
}
In this generic implementation, the GenericMediator and GenericColleague interfaces use a type parameter T to specify the type of colleagues they work with. This allows us to create more flexible and reusable mediator components.
To deepen your understanding, try modifying the code examples:
AdminUser, that has additional methods or properties.ChatRoom class, such as private messaging between users.To better understand the flow of communication in the Mediator pattern, let’s visualize it using a sequence diagram.
sequenceDiagram
participant User1
participant ChatRoom
participant User2
User1->>ChatRoom: send(message)
ChatRoom->>User2: notify(message)
User2->>User2: receive(message)
Diagram Description: This sequence diagram illustrates the communication flow in the Mediator pattern. When User1 sends a message, it is passed to the ChatRoom (mediator), which then notifies User2.
Remember, mastering design patterns is a journey. As you continue to explore and implement these patterns, you’ll gain a deeper understanding of how to create robust and maintainable software systems. Keep experimenting, stay curious, and enjoy the process!