Mediator Pattern in TypeScript — ✨ Structural Design Pattern #3

365kim
3 min readJul 30, 2023
Photo by Cristi Tohatan on Unsplash

🙋🏻‍♀️ 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 the mediator 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 the mediator might affect many classes…

--

--

365kim

Web Front-End Developer who believes Every Day Counts!