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!