Explore the Mediator Design Pattern in JavaScript and TypeScript, its intent, motivation, and how it simplifies complex object interactions by promoting loose coupling.
In the world of software design, managing interactions between objects can become increasingly complex as systems grow. The Mediator pattern offers a solution by defining an intermediary object that encapsulates how a set of objects interact. This pattern promotes loose coupling by preventing objects from referring to each other explicitly, simplifying communication and preventing chaos in complex systems.
The Mediator pattern is a behavioral design pattern that centralizes communication between objects, allowing them to interact without being tightly coupled. Instead of having objects communicate directly, they send messages to a mediator, which handles the communication logic. This approach reduces dependencies between objects, making the system more flexible and easier to maintain.
In a tightly coupled system, objects are directly dependent on each other. This can lead to several issues:
Consider a chat application where users can send messages to each other. If each user object directly communicates with every other user object, the system becomes difficult to manage as the number of users grows.
A classic analogy for the Mediator pattern is an air traffic controller. In an airport, multiple airplanes need to coordinate their actions, such as landing, taking off, and taxiing. If each airplane were to communicate directly with every other airplane, the situation would quickly become chaotic.
Instead, an air traffic controller acts as a mediator, coordinating the actions of the airplanes. Each airplane communicates with the controller, who then manages the interactions. This centralization simplifies communication and ensures that the system operates smoothly.
The Mediator pattern offers several benefits:
The Mediator pattern is particularly useful in scenarios where:
Let’s explore how to implement the Mediator pattern in JavaScript with a simple example. We’ll create a chat room where users can send messages to each other through a mediator.
// Mediator class
class ChatRoom {
constructor() {
this.users = {};
}
register(user) {
this.users[user.name] = user;
user.chatRoom = this;
}
sendMessage(message, from, to) {
if (to) {
// Direct message
to.receive(message, from);
} else {
// Broadcast message
for (let key in this.users) {
if (this.users[key] !== from) {
this.users[key].receive(message, from);
}
}
}
}
}
// User class
class User {
constructor(name) {
this.name = name;
this.chatRoom = null;
}
send(message, to) {
this.chatRoom.sendMessage(message, this, to);
}
receive(message, from) {
console.log(`${from.name} to ${this.name}: ${message}`);
}
}
// Usage
const chatRoom = new ChatRoom();
const user1 = new User('Alice');
const user2 = new User('Bob');
const user3 = new User('Charlie');
chatRoom.register(user1);
chatRoom.register(user2);
chatRoom.register(user3);
user1.send('Hello, Bob!', user2);
user2.send('Hi, Alice!', user1);
user3.send('Hello, everyone!');
In this example, the ChatRoom
class acts as the mediator, managing the communication between User
objects. Users can send messages to each other through the chat room, which handles the delivery of messages.
Now, let’s see how to implement the Mediator pattern in TypeScript, leveraging its strong typing features.
// Mediator interface
interface ChatRoomMediator {
sendMessage(message: string, from: User, to?: User): void;
register(user: User): void;
}
// Concrete Mediator class
class ChatRoom implements ChatRoomMediator {
private users: { [key: string]: User } = {};
register(user: User): void {
this.users[user.name] = user;
user.setChatRoom(this);
}
sendMessage(message: string, from: User, to?: User): void {
if (to) {
// Direct message
to.receive(message, from);
} else {
// Broadcast message
for (let key in this.users) {
if (this.users[key] !== from) {
this.users[key].receive(message, from);
}
}
}
}
}
// Colleague class
class User {
private chatRoom: ChatRoomMediator | null = null;
constructor(public name: string) {}
setChatRoom(chatRoom: ChatRoomMediator): void {
this.chatRoom = chatRoom;
}
send(message: string, to?: User): void {
this.chatRoom?.sendMessage(message, this, to);
}
receive(message: string, from: User): void {
console.log(`${from.name} to ${this.name}: ${message}`);
}
}
// Usage
const chatRoom = new ChatRoom();
const user1 = new User('Alice');
const user2 = new User('Bob');
const user3 = new User('Charlie');
chatRoom.register(user1);
chatRoom.register(user2);
chatRoom.register(user3);
user1.send('Hello, Bob!', user2);
user2.send('Hi, Alice!', user1);
user3.send('Hello, everyone!');
In this TypeScript example, we define a ChatRoomMediator
interface to enforce the contract for the mediator. The ChatRoom
class implements this interface, providing the communication logic. The User
class interacts with the mediator to send and receive messages.
To better understand the Mediator pattern, let’s visualize the interactions between objects using a sequence diagram.
sequenceDiagram participant User1 as Alice participant User2 as Bob participant User3 as Charlie participant Mediator as ChatRoom User1->>Mediator: send("Hello, Bob!", User2) Mediator->>User2: receive("Hello, Bob!", User1) User2->>Mediator: send("Hi, Alice!", User1) Mediator->>User1: receive("Hi, Alice!", User2) User3->>Mediator: send("Hello, everyone!") Mediator->>User1: receive("Hello, everyone!", User3) Mediator->>User2: receive("Hello, everyone!", User3)
Diagram Description: This sequence diagram illustrates the communication flow in the chat room. Users send messages to the ChatRoom
mediator, which then delivers the messages to the appropriate recipients.
To deepen your understanding of the Mediator pattern, try modifying the code examples:
sendMessage
method to support group messaging.Before moving on, let’s reinforce what we’ve learned:
Remember, mastering design patterns is a journey. The Mediator pattern is just one tool in your toolbox. As you continue to explore and apply design patterns, you’ll gain a deeper understanding of how to build robust and maintainable systems. Keep experimenting, stay curious, and enjoy the journey!