🙋🏻♀️ What is the Mediator Pattern?
The Mediator Pattern is a behavioral design pattern in which objects interact exclusively through a mediator
, rather than communicating directly with each other. Also know as Controller Pattern, this pattern promotes loose coupling by encapsulating the interactions among objects (or colleagues
).
mediator
: an interface that provides a way of communication between objects(colleagues).colleague
: a concrete object using themediator
to interact with other colleagues.
Facade Pattern vs Mediator Pattern
The Mediator Pattern may seem similar to the Facade Pattern as both aim to simplify interactions and abstract complex systems. However, they tackle slightly different issues!
🏛 Facade Pattern
- A structural pattern that describes the composition of the objects.
- Merely exposes existing functionality but from a different perspective.
- Its protocol is unidirectional, flowing from the client to the subsystems via the
facade
.
🎹️ Mediator Pattern
- A behavioral pattern that outlines how objects interact.
- Introduces new functionality by combining different objects.
- Its protocol is multidirectional, from a colleague to another.
mediator
serves as a hub for communication between them.
✨ Simple TypeScript Example
Imagine you are building a chat app for Ember and Wade, characters from the movie Elemental. To begin, let’s define some basic types that we’ll use throughout the process
type MessageText = string
type DirectMessage = { type: 'outbound' | 'inbound'; text: MessageText }
type UserID = string
Next, define the mediator
interface IChatRoom
. This interface outlines two key events that can occur in the chat room: sending a direct message (dm
) and following a user(follow
.)
// mediator
interface IChatRoom {
dm(from: UserID, to: UserID, text: MessageText): void
follow(from: UserID, to: UserID): void
}
IUser
is the interface that represents a user (colleague
).
// colleague
interface IUser {
id: UserID
followers: UserID[]
followings: UserID[]
messageBox: Record<UserID, DirectMessage[]>
sendDM(text: MessageText, to: UserID): void
receiveDM(text: MessageText, from: UserID): void
addFollower(id: UserID): void
addFollowing(id: UserID): void
}
The class User
implements all the methods declared in IUser
, enabling a user to send and receive messages, and to add followers and followings.
// concrete colleague
class User implements IUser {
id: IUser['id']
followers: IUser['followers']
followings: IUser['followings']
messageBox: Record<string, DirectMessage[]>
constructor(id: string) {
this.id = id
this.followers = []
this.followings = []
this.messageBox = {}
}
sendDM(text: MessageText, to: UserID): void {
if (!this.messageBox[to]) {
this.messageBox[to] = []
}
this.messageBox[to].push({ type: 'outbound', text })
}
receiveDM(text: MessageText, from: UserID): void {
if (!this.messageBox[from]) {
this.messageBox[from] = []
}
this.messageBox[from].push({ type: 'inbound', text })
}
addFollower(id: UserID): void {
if (this.followers.includes(id)) return
this.followers.push(id)
}
addFollowing(id: string): void {
if (this.followings.includes(id)) return
this.followings.push(id)
}
}
ChatRoom
implementsdm
and follow
methods between users.
// concrete mediator
class ChatRoom implements IChatRoom {
private users: Map<UserID, IUser> = new Map()
constructor(...users: IUser[]) {
for (const user of users) {
this.users.set(user.id, user)
}
}
dm(from: UserID, to: UserID, text: MessageText): void {
const fromUser = this.users.get(from)
const toUser = this.users.get(to)
if (!fromUser || !toUser) return
fromUser.sendDM(text, to)
toUser.receiveDM(text, from)
}
follow(from: UserID, to: UserID): void {
const fromUser = this.users.get(from)
const toUser = this.users.get(to)
if (!fromUser || !toUser) return
fromUser.addFollower(to)
toUser.addFollowing(from)
}
}
Finally, in the client code, you can create users, Ember and Wade, and a chat room with these users.
// Client code
const ember = new User('Ember')
const wade = new User('Wade')
const chatRoom = new ChatRoom(ember, wade)
chatRoom.dm(ember.id, wade.id, 'Hi!')
chatRoom.dm(wade.id, ember.id, 'Oh, hey! How are you?')
chatRoom.follow(wade.id, ember.id)
console.log(ember.messageBox[wade.id])
// [
// { type: 'outbound', text: 'Hi!' },
// { type: 'inbound', text: 'Oh, hey! How are you?' },
// ]
console.log(ember.followers)
// ['Wade']
console.log(wade.followings)
// ['Ember']
Notice that all communication between users the colleagues
goes through this mediator
. Instead of Amber and Wade communicating directly, the mediator ChatRoom
manages and simplifies the interaction between them. This reduces the complexity and coupling of the system, making it more flexible and easier to maintain. 😉
🧑💻 Use it or Avoid it
When to use it:
- If the communication between objects is complex and hard to understand or maintain
- If you want to encapsulate the rules for the interaction between a set of objects in a single location.
When to avoid it:
- When the number of
mediators
increases, it can become a problem because changes to themediator
might affect many classes…