Prerequisites
- Basic understanding of JavaScript ๐
- TypeScript installation โก
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write type-safe code โจ
๐ฏ Introduction
Welcome to this exciting tutorial on the Mediator Pattern! ๐ In this guide, weโll explore how to manage complex object communication in TypeScript using this powerful design pattern.
Have you ever tried to coordinate a group chat where everyone talks to everyone? It gets messy fast! ๐ The Mediator Pattern is like having a group admin who manages all the messages - making communication organized and efficient.
By the end of this tutorial, youโll feel confident implementing the Mediator Pattern in your own projects! Letโs dive in! ๐โโ๏ธ
๐ Understanding Mediator Pattern
๐ค What is the Mediator Pattern?
The Mediator Pattern is like an air traffic controller at an airport ๐ซ. Instead of planes talking directly to each other (chaos!), they all communicate through the control tower. This central mediator coordinates all interactions, keeping things safe and organized.
In TypeScript terms, the Mediator Pattern defines how a set of objects interact by encapsulating their communication in a mediator object. This means you can:
- โจ Reduce dependencies between communicating objects
- ๐ Change interaction logic in one place
- ๐ก๏ธ Keep objects loosely coupled
๐ก Why Use the Mediator Pattern?
Hereโs why developers love the Mediator Pattern:
- Decoupling ๐: Objects donโt need to know about each other
- Reusability โป๏ธ: Components can be reused in different contexts
- Maintainability ๐ง: Changes to communication logic are centralized
- Testability ๐งช: Easier to test individual components
Real-world example: Imagine building a smart home system ๐ . With the Mediator Pattern, your lights, thermostat, and security system donโt talk directly - they communicate through a central hub!
๐ง Basic Syntax and Usage
๐ Simple Example
Letโs start with a friendly chat room example:
// ๐ Hello, Mediator Pattern!
interface Mediator {
sendMessage(message: string, sender: User): void;
addUser(user: User): void;
}
// ๐จ Creating a user class
class User {
constructor(
public name: string,
private mediator: Mediator
) {}
// ๐ฌ Send a message
send(message: string): void {
console.log(`${this.name}: ${message}`);
this.mediator.sendMessage(message, this);
}
// ๐จ Receive a message
receive(message: string, sender: string): void {
console.log(` ${this.name} received from ${sender}: ${message}`);
}
}
// ๐ฏ The chat room mediator
class ChatRoom implements Mediator {
private users: User[] = [];
addUser(user: User): void {
this.users.push(user);
console.log(`โจ ${user.name} joined the chat!`);
}
sendMessage(message: string, sender: User): void {
this.users.forEach(user => {
if (user !== sender) {
user.receive(message, sender.name);
}
});
}
}
๐ก Explanation: Notice how users donโt know about each other - they only know about the mediator! The ChatRoom handles all the message routing.
๐ฏ Common Patterns
Here are patterns youโll use daily:
// ๐๏ธ Pattern 1: Abstract mediator
abstract class BaseMediator {
protected components: Component[] = [];
abstract notify(sender: Component, event: string): void;
register(component: Component): void {
this.components.push(component);
component.setMediator(this);
}
}
// ๐จ Pattern 2: Component interface
interface Component {
setMediator(mediator: BaseMediator): void;
operation(): void;
}
// ๐ Pattern 3: Concrete implementation
class ConcreteComponent implements Component {
private mediator!: BaseMediator;
setMediator(mediator: BaseMediator): void {
this.mediator = mediator;
}
operation(): void {
this.mediator.notify(this, "operation");
}
}
๐ก Practical Examples
๐ Example 1: Smart Shopping System
Letโs build a shopping system where inventory, payment, and shipping communicate:
// ๐๏ธ Define our shopping mediator
interface ShoppingMediator {
processOrder(orderId: string): void;
notifyComponent(sender: string, event: string, data?: any): void;
}
// ๐ฆ Inventory system
class InventorySystem {
private mediator: ShoppingMediator;
private stock: Map<string, number> = new Map([
["laptop", 5],
["phone", 10],
["tablet", 3]
]);
constructor(mediator: ShoppingMediator) {
this.mediator = mediator;
}
// ๐ Check if item is available
checkStock(item: string, quantity: number): boolean {
const available = this.stock.get(item) || 0;
const hasStock = available >= quantity;
console.log(`๐ฆ Inventory: ${item} - ${hasStock ? "โ
In stock" : "โ Out of stock"}`);
this.mediator.notifyComponent("inventory", "stock-checked", { item, hasStock });
return hasStock;
}
// ๐ Reserve items
reserveItems(item: string, quantity: number): void {
const current = this.stock.get(item) || 0;
this.stock.set(item, current - quantity);
console.log(`๐ฆ Reserved ${quantity} ${item}(s)`);
}
}
// ๐ณ Payment system
class PaymentSystem {
private mediator: ShoppingMediator;
constructor(mediator: ShoppingMediator) {
this.mediator = mediator;
}
// ๐ฐ Process payment
processPayment(amount: number): boolean {
const success = Math.random() > 0.1; // 90% success rate
if (success) {
console.log(`๐ณ Payment: $${amount} processed successfully! โ
`);
this.mediator.notifyComponent("payment", "payment-success", { amount });
} else {
console.log(`๐ณ Payment: Failed to process $${amount} โ`);
this.mediator.notifyComponent("payment", "payment-failed");
}
return success;
}
}
// ๐ Shipping system
class ShippingSystem {
private mediator: ShoppingMediator;
constructor(mediator: ShoppingMediator) {
this.mediator = mediator;
}
// ๐ฎ Schedule delivery
scheduleDelivery(orderId: string): void {
const deliveryDate = new Date();
deliveryDate.setDate(deliveryDate.getDate() + 3);
console.log(`๐ Shipping: Order ${orderId} scheduled for ${deliveryDate.toDateString()}`);
this.mediator.notifyComponent("shipping", "delivery-scheduled", { orderId, date: deliveryDate });
}
}
// ๐ฎ The shopping mediator
class ShoppingSystemMediator implements ShoppingMediator {
private inventory: InventorySystem;
private payment: PaymentSystem;
private shipping: ShippingSystem;
constructor() {
this.inventory = new InventorySystem(this);
this.payment = new PaymentSystem(this);
this.shipping = new ShippingSystem(this);
}
// ๐ Process complete order
processOrder(orderId: string): void {
console.log(`\n๐ Processing order ${orderId}...`);
// Check inventory
if (this.inventory.checkStock("laptop", 1)) {
// Process payment
if (this.payment.processPayment(999.99)) {
// Reserve items and schedule shipping
this.inventory.reserveItems("laptop", 1);
this.shipping.scheduleDelivery(orderId);
console.log(`\n๐ Order ${orderId} completed successfully!`);
} else {
console.log(`\n๐ Order ${orderId} failed - payment issue`);
}
} else {
console.log(`\n๐ Order ${orderId} failed - out of stock`);
}
}
// ๐ข Handle notifications between components
notifyComponent(sender: string, event: string, data?: any): void {
// Central logging or additional coordination logic
console.log(`๐ข Event from ${sender}: ${event}`);
}
}
// ๐ฎ Let's use it!
const shop = new ShoppingSystemMediator();
shop.processOrder("ORD-12345");
๐ฏ Try it yourself: Add a notification system that emails customers about order status!
๐ฎ Example 2: Game Event System
Letโs make a game where players, enemies, and the UI communicate:
// ๐ Game mediator for coordinating events
interface GameMediator {
notify(sender: GameObject, event: GameEvent): void;
registerObject(obj: GameObject): void;
}
// ๐ฏ Game events
type GameEvent =
| { type: "damage"; amount: number; target: string }
| { type: "heal"; amount: number }
| { type: "levelUp" }
| { type: "enemyDefeated"; exp: number };
// ๐ฎ Base game object
abstract class GameObject {
constructor(
protected name: string,
protected mediator: GameMediator
) {
mediator.registerObject(this);
}
abstract handleEvent(event: GameEvent): void;
}
// ๐ฆธ Player class
class Player extends GameObject {
private health = 100;
private level = 1;
private exp = 0;
constructor(name: string, mediator: GameMediator) {
super(name, mediator);
console.log(`๐ฆธ ${name} enters the game!`);
}
// โ๏ธ Attack an enemy
attack(targetName: string): void {
const damage = 10 + this.level * 5;
console.log(`โ๏ธ ${this.name} attacks for ${damage} damage!`);
this.mediator.notify(this, { type: "damage", amount: damage, target: targetName });
}
// ๐จ Handle game events
handleEvent(event: GameEvent): void {
switch (event.type) {
case "damage":
if (event.target === this.name) {
this.health -= event.amount;
console.log(`๐ ${this.name} takes ${event.amount} damage! Health: ${this.health}`);
}
break;
case "enemyDefeated":
this.exp += event.exp;
console.log(`โจ ${this.name} gains ${event.exp} exp!`);
if (this.exp >= this.level * 100) {
this.levelUp();
}
break;
}
}
// ๐ Level up!
private levelUp(): void {
this.level++;
this.health = 100 + this.level * 20;
console.log(`๐ ${this.name} reached level ${this.level}!`);
this.mediator.notify(this, { type: "levelUp" });
}
}
// ๐พ Enemy class
class Enemy extends GameObject {
private health: number;
private expReward: number;
constructor(name: string, health: number, expReward: number, mediator: GameMediator) {
super(name, mediator);
this.health = health;
this.expReward = expReward;
console.log(`๐พ Wild ${name} appears!`);
}
handleEvent(event: GameEvent): void {
if (event.type === "damage" && event.target === this.name) {
this.health -= event.amount;
console.log(`๐ฅ ${this.name} takes ${event.amount} damage! Health: ${this.health}`);
if (this.health <= 0) {
console.log(`โ ๏ธ ${this.name} is defeated!`);
this.mediator.notify(this, { type: "enemyDefeated", exp: this.expReward });
}
}
}
}
// ๐ UI System
class GameUI extends GameObject {
constructor(mediator: GameMediator) {
super("UI", mediator);
}
handleEvent(event: GameEvent): void {
switch (event.type) {
case "levelUp":
this.showNotification("๐ LEVEL UP! ๐");
break;
case "enemyDefeated":
this.showNotification("๐ Enemy Defeated! ๐");
break;
}
}
private showNotification(message: string): void {
console.log(`\n๐ผ๏ธ UI Notification: ${message}\n`);
}
}
// ๐ฏ The game mediator
class BattleMediator implements GameMediator {
private gameObjects: GameObject[] = [];
registerObject(obj: GameObject): void {
this.gameObjects.push(obj);
}
notify(sender: GameObject, event: GameEvent): void {
// Broadcast event to all game objects
this.gameObjects.forEach(obj => {
if (obj !== sender) {
obj.handleEvent(event);
}
});
}
}
// ๐ฎ Let's play!
const game = new BattleMediator();
const ui = new GameUI(game);
const hero = new Player("TypeScript Hero", game);
const goblin = new Enemy("Goblin", 30, 50, game);
hero.attack("Goblin");
hero.attack("Goblin");
hero.attack("Goblin");
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: Type-Safe Event System
When youโre ready to level up, try this advanced pattern:
// ๐ฏ Advanced type-safe mediator
type EventMap = {
userJoined: { userId: string; timestamp: Date };
messagePosted: { userId: string; message: string };
userLeft: { userId: string };
};
type EventType = keyof EventMap;
class TypeSafeMediator {
private handlers = new Map<EventType, Set<(data: any) => void>>();
// โจ Type-safe event subscription
on<T extends EventType>(
event: T,
handler: (data: EventMap[T]) => void
): void {
if (!this.handlers.has(event)) {
this.handlers.set(event, new Set());
}
this.handlers.get(event)!.add(handler);
}
// ๐ Type-safe event emission
emit<T extends EventType>(event: T, data: EventMap[T]): void {
const eventHandlers = this.handlers.get(event);
if (eventHandlers) {
eventHandlers.forEach(handler => handler(data));
}
}
}
// ๐ช Using the type-safe mediator
const mediator = new TypeSafeMediator();
mediator.on("userJoined", (data) => {
// TypeScript knows data has userId and timestamp! โจ
console.log(`๐ ${data.userId} joined at ${data.timestamp}`);
});
mediator.emit("userJoined", {
userId: "hero123",
timestamp: new Date()
});
๐๏ธ Advanced Topic 2: Async Mediator Pattern
For the brave developers handling async operations:
// ๐ Async mediator for complex operations
class AsyncMediator {
private asyncHandlers = new Map<string, (data: any) => Promise<any>>();
// ๐ฏ Register async handler
registerHandler(event: string, handler: (data: any) => Promise<any>): void {
this.asyncHandlers.set(event, handler);
}
// โก Process async request
async request<T>(event: string, data: any): Promise<T> {
const handler = this.asyncHandlers.get(event);
if (!handler) {
throw new Error(`No handler for event: ${event}`);
}
console.log(`โณ Processing ${event}...`);
const result = await handler(data);
console.log(`โ
${event} completed!`);
return result;
}
}
// ๐ฎ Example usage
const asyncMediator = new AsyncMediator();
// Register handlers
asyncMediator.registerHandler("fetchUser", async (userId: string) => {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
return { id: userId, name: "TypeScript Master", level: 42 };
});
// Use it!
async function demo() {
const user = await asyncMediator.request("fetchUser", "user123");
console.log(`๐ Fetched user: ${user.name}`);
}
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: The God Mediator
// โ Wrong way - mediator knows too much!
class GodMediator {
private userAge: number = 0;
private userEmail: string = "";
private productPrice: number = 0;
// ... hundreds of properties ๐ฐ
handleEverything(): void {
// Complex business logic everywhere! ๐ฅ
}
}
// โ
Correct way - keep mediator focused!
class FocusedMediator {
notify(sender: Component, event: string): void {
// Just coordinate, don't store business data! ๐ก๏ธ
this.components.forEach(c => {
if (c !== sender) {
c.handleEvent(event);
}
});
}
}
๐คฏ Pitfall 2: Direct Component References
// โ Dangerous - components know each other!
class BadComponent {
constructor(private otherComponent: BadComponent) {} // ๐ฅ Coupling!
doSomething(): void {
this.otherComponent.react(); // Direct call!
}
}
// โ
Safe - components only know mediator!
class GoodComponent {
constructor(private mediator: Mediator) {} // โ
Decoupled!
doSomething(): void {
this.mediator.notify(this, "something-happened");
}
}
๐ ๏ธ Best Practices
- ๐ฏ Keep Mediator Simple: Donโt put business logic in the mediator
- ๐ Clear Event Names: Use descriptive event names like โorder-completedโ
- ๐ก๏ธ Type Safety: Use TypeScriptโs type system for event data
- ๐จ Single Responsibility: Each component should have one job
- โจ Loose Coupling: Components shouldnโt know about each other
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Smart Home System
Create a type-safe smart home system with mediator pattern:
๐ Requirements:
- โ Devices: Lights, Thermostat, Security System, Smart Speaker
- ๐ท๏ธ Events: motion detected, temperature changed, voice command
- ๐ค Scenarios: Away mode, Sleep mode, Party mode
- ๐ Time-based automation
- ๐จ Each device needs an emoji!
๐ Bonus Points:
- Add energy saving features
- Implement scene presets
- Create a mobile app controller component
๐ก Solution
๐ Click to see solution
// ๐ฏ Our smart home mediator system!
interface SmartHomeMediator {
notify(sender: SmartDevice, event: HomeEvent): void;
registerDevice(device: SmartDevice): void;
activateScene(scene: "away" | "sleep" | "party"): void;
}
type HomeEvent =
| { type: "motion"; location: string }
| { type: "temperature"; value: number }
| { type: "voice"; command: string }
| { type: "lightChange"; brightness: number }
| { type: "securityAlert"; severity: "low" | "high" };
abstract class SmartDevice {
constructor(
protected name: string,
protected emoji: string,
protected mediator: SmartHomeMediator
) {
mediator.registerDevice(this);
console.log(`${emoji} ${name} connected to smart home!`);
}
abstract handleEvent(event: HomeEvent): void;
}
// ๐ก Smart lights
class SmartLights extends SmartDevice {
private brightness = 50;
constructor(location: string, mediator: SmartHomeMediator) {
super(`${location} Lights`, "๐ก", mediator);
}
setBrightness(level: number): void {
this.brightness = level;
console.log(`${this.emoji} ${this.name}: Brightness set to ${level}%`);
this.mediator.notify(this, { type: "lightChange", brightness: level });
}
handleEvent(event: HomeEvent): void {
switch (event.type) {
case "motion":
console.log(`${this.emoji} Motion detected - turning on lights!`);
this.setBrightness(100);
break;
case "voice":
if (event.command.includes("lights off")) {
this.setBrightness(0);
}
break;
}
}
}
// ๐ก๏ธ Smart thermostat
class SmartThermostat extends SmartDevice {
private temperature = 22;
constructor(mediator: SmartHomeMediator) {
super("Thermostat", "๐ก๏ธ", mediator);
}
setTemperature(temp: number): void {
this.temperature = temp;
console.log(`${this.emoji} Temperature set to ${temp}ยฐC`);
this.mediator.notify(this, { type: "temperature", value: temp });
}
handleEvent(event: HomeEvent): void {
if (event.type === "lightChange" && event.brightness === 0) {
console.log(`${this.emoji} Lights off - entering sleep mode`);
this.setTemperature(18);
}
}
}
// ๐ Security system
class SecuritySystem extends SmartDevice {
private armed = false;
constructor(mediator: SmartHomeMediator) {
super("Security System", "๐", mediator);
}
arm(): void {
this.armed = true;
console.log(`${this.emoji} Security system ARMED ๐จ`);
}
disarm(): void {
this.armed = false;
console.log(`${this.emoji} Security system disarmed โ
`);
}
handleEvent(event: HomeEvent): void {
if (event.type === "motion" && this.armed) {
console.log(`${this.emoji} ALERT! Motion detected while armed! ๐จ`);
this.mediator.notify(this, { type: "securityAlert", severity: "high" });
}
}
}
// ๐ The home automation hub
class SmartHomeHub implements SmartHomeMediator {
private devices: SmartDevice[] = [];
registerDevice(device: SmartDevice): void {
this.devices.push(device);
}
notify(sender: SmartDevice, event: HomeEvent): void {
this.devices.forEach(device => {
if (device !== sender) {
device.handleEvent(event);
}
});
}
activateScene(scene: "away" | "sleep" | "party"): void {
console.log(`\n๐ฌ Activating ${scene} mode...`);
switch (scene) {
case "away":
this.notify(this as any, { type: "voice", command: "lights off" });
// In real implementation, would arm security
break;
case "sleep":
this.notify(this as any, { type: "lightChange", brightness: 10 });
break;
case "party":
this.notify(this as any, { type: "lightChange", brightness: 100 });
console.log("๐ Party mode activated!");
break;
}
}
}
// ๐ฎ Test it out!
const home = new SmartHomeHub();
const livingRoomLights = new SmartLights("Living Room", home);
const thermostat = new SmartThermostat(home);
const security = new SecuritySystem(home);
// Simulate events
console.log("\n๐ Someone enters the room...");
livingRoomLights.handleEvent({ type: "motion", location: "Living Room" });
console.log("\n๐ด Time for bed...");
home.activateScene("sleep");
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Create mediator patterns with confidence ๐ช
- โ Avoid tight coupling between objects ๐ก๏ธ
- โ Apply the pattern in real projects ๐ฏ
- โ Debug communication issues like a pro ๐
- โ Build scalable systems with TypeScript! ๐
Remember: The Mediator Pattern is your friend when objects need to talk but shouldnโt know each otherโs phone numbers! ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered the Mediator Pattern!
Hereโs what to do next:
- ๐ป Practice with the smart home exercise above
- ๐๏ธ Refactor existing code to use mediator pattern
- ๐ Move on to our next tutorial: Observer Pattern
- ๐ Share your mediator implementations with others!
Remember: Every TypeScript expert was once a beginner. Keep coding, keep learning, and most importantly, have fun! ๐
Happy coding! ๐๐โจ