Introduction to the Mediator Pattern
Overview
The Mediator pattern is a behavioral design pattern that reduces chaotic dependencies between objects by making them communicate indirectly through a special mediator object. Instead of objects referring to each other directly, they communicate through the mediator, which encapsulates how a set of objects interact.
The Problem: Tight Coupling in Many-to-Many Relationships
Imagine building an order processing system where multiple components need to coordinate:
typescript1class InventoryService {2 checkStock(productId: string): boolean {3 // Check inventory...4 return true;5 }67 reserveStock(productId: string, quantity: number): void {8 // Reserve inventory...9 // Need to notify shipping, update payment status...10 // Direct dependencies on other services!11 }12}1314class PaymentService {15 processPayment(orderId: string, amount: number): boolean {16 // Process payment...17 // If successful, need to trigger shipping...18 // If failed, need to release inventory...19 // More direct dependencies!20 }21}2223class ShippingService {24 scheduleDelivery(orderId: string): void {25 // Need to check inventory status...26 // Need to verify payment...27 // Need to notify customer...28 // Even more dependencies!29 }30}3132class NotificationService {33 sendEmail(recipient: string, message: string): void {34 // Send notifications...35 // But who calls this? Everyone!36 }37}
Problems with Direct Communication
- Tight Coupling: Each service knows about and depends on multiple other services
- Hard to Maintain: Changes in one service ripple through all dependent services
- Hard to Test: Testing one service requires mocking all its dependencies
- Hard to Reuse: Services can't be reused independently
- Complex Logic: Business logic is scattered across multiple services
The dependency graph looks like spaghetti:
1InventoryService ←→ PaymentService2 ↓ ↑ ↓ ↑3 ↓ ↑ ↓ ↑4ShippingService ←→ NotificationService
Every service knows about every other service, creating a maintenance nightmare.
The Solution: Centralize Communication
The Mediator pattern solves this by introducing a mediator object that encapsulates how these objects interact:
1InventoryService →2PaymentService → OrderMediator → coordinates interactions3ShippingService →4NotificationService →
Now services only depend on the mediator, not on each other:
typescript1interface OrderMediator {2 processOrder(order: Order): void;3 notifyInventoryReserved(orderId: string): void;4 notifyPaymentProcessed(orderId: string, success: boolean): void;5 notifyShippingScheduled(orderId: string): void;6}78class InventoryService {9 constructor(private mediator: OrderMediator) {}1011 reserveStock(orderId: string, productId: string, quantity: number): void {12 // Reserve inventory...13 console.log(`Reserved ${quantity} units of ${productId}`);1415 // Notify mediator - let it coordinate next steps16 this.mediator.notifyInventoryReserved(orderId);17 }18}1920class PaymentService {21 constructor(private mediator: OrderMediator) {}2223 processPayment(orderId: string, amount: number): void {24 // Process payment...25 const success = true; // Simplified26 console.log(`Payment processed for order ${orderId}`);2728 // Let mediator handle what happens next29 this.mediator.notifyPaymentProcessed(orderId, success);30 }31}
Pattern Structure
The Mediator pattern consists of these key components:
typescript1// 1. Mediator Interface2interface Mediator {3 notify(sender: Component, event: string, data?: any): void;4}56// 2. Concrete Mediator - coordinates interactions7class ConcreteMediator implements Mediator {8 private component1: Component1;9 private component2: Component2;1011 notify(sender: Component, event: string, data?: any): void {12 // Coordinate responses based on sender and event13 if (sender === this.component1 && event === 'A') {14 this.component2.doC();15 }16 if (sender === this.component2 && event === 'B') {17 this.component1.doD();18 }19 }20}2122// 3. Components (Colleagues) - communicate through mediator23abstract class Component {24 constructor(protected mediator: Mediator) {}25}2627class Component1 extends Component {28 doA(): void {29 console.log('Component1 does A');30 this.mediator.notify(this, 'A');31 }32}3334class Component2 extends Component {35 doB(): void {36 console.log('Component2 does B');37 this.mediator.notify(this, 'B');38 }39}
Class Diagram
1┌─────────────────────┐2│ <<interface>> │3│ Mediator │4├─────────────────────┤5│ + notify(sender, │6│ event, data) │7└─────────────────────┘8 △9 │ implements10 │11┌─────────────────────┐12│ ConcreteMediator │13├─────────────────────┤14│ - component1 │15│ - component2 │16├─────────────────────┤17│ + notify() │18└─────────────────────┘19 │ │20 │ uses │ uses21 ↓ ↓22┌──────────┐ ┌──────────┐23│Component1│ │Component2│24├──────────┤ ├──────────┤25│-mediator │ │-mediator │26├──────────┤ ├──────────┤27│+ doA() │ │+ doB() │28└──────────┘ └──────────┘
Logistics Context: Order Processing Hub
Let's see how the Mediator pattern applies to a real-world logistics scenario:
The Scenario
An e-commerce logistics system needs to process orders through multiple stages:
- Inventory Check: Verify product availability
- Inventory Reservation: Reserve stock for the order
- Payment Processing: Charge the customer
- Shipping Scheduling: Arrange delivery
- Customer Notification: Send status updates
Without Mediator (Problematic)
typescript1class OrderProcessor {2 processOrder(order: Order): void {3 const inventory = new InventoryService();4 const payment = new PaymentService();5 const shipping = new ShippingService();6 const notification = new NotificationService();78 // Orchestration logic mixed with business logic9 if (inventory.checkStock(order.productId, order.quantity)) {10 inventory.reserveStock(order.productId, order.quantity);1112 if (payment.processPayment(order.id, order.total)) {13 shipping.scheduleDelivery(order.id);14 notification.sendEmail(order.customerEmail, 'Order confirmed');15 } else {16 inventory.releaseStock(order.productId, order.quantity);17 notification.sendEmail(order.customerEmail, 'Payment failed');18 }19 } else {20 notification.sendEmail(order.customerEmail, 'Out of stock');21 }22 }23}
Problems:
- Tight coupling between all services
- Hard to add new services or change workflow
- Difficult to test individual services
- Business logic scattered everywhere
With Mediator (Clean Solution)
typescript1interface OrderProcessingMediator {2 processOrder(order: Order): void;3 onInventoryChecked(orderId: string, available: boolean): void;4 onInventoryReserved(orderId: string): void;5 onPaymentProcessed(orderId: string, success: boolean): void;6 onShippingScheduled(orderId: string, trackingNumber: string): void;7}89class OrderProcessingHub implements OrderProcessingMediator {10 private orders = new Map<string, Order>();1112 constructor(13 private inventory: InventoryService,14 private payment: PaymentService,15 private shipping: ShippingService,16 private notification: NotificationService17 ) {}1819 processOrder(order: Order): void {20 this.orders.set(order.id, order);21 this.inventory.checkStock(order.id, order.productId, order.quantity);22 }2324 onInventoryChecked(orderId: string, available: boolean): void {25 const order = this.orders.get(orderId)!;2627 if (available) {28 this.inventory.reserveStock(orderId, order.productId, order.quantity);29 } else {30 this.notification.sendEmail(31 order.customerEmail,32 'Sorry, product is out of stock'33 );34 }35 }3637 onInventoryReserved(orderId: string): void {38 const order = this.orders.get(orderId)!;39 this.payment.processPayment(orderId, order.total);40 }4142 onPaymentProcessed(orderId: string, success: boolean): void {43 const order = this.orders.get(orderId)!;4445 if (success) {46 this.shipping.scheduleDelivery(orderId);47 } else {48 this.inventory.releaseStock(order.productId, order.quantity);49 this.notification.sendEmail(50 order.customerEmail,51 'Payment failed - order cancelled'52 );53 }54 }5556 onShippingScheduled(orderId: string, trackingNumber: string): void {57 const order = this.orders.get(orderId)!;58 this.notification.sendEmail(59 order.customerEmail,60 `Order confirmed! Tracking: ${trackingNumber}`61 );62 }63}
Now each service is independent and only communicates through the mediator:
typescript1class InventoryService {2 constructor(private mediator: OrderProcessingMediator) {}34 checkStock(orderId: string, productId: string, quantity: number): void {5 const available = this.getStockLevel(productId) >= quantity;6 this.mediator.onInventoryChecked(orderId, available);7 }89 reserveStock(orderId: string, productId: string, quantity: number): void {10 // Reserve logic...11 this.mediator.onInventoryReserved(orderId);12 }1314 private getStockLevel(productId: string): number {15 // Check database...16 return 100;17 }18}
Benefits of the Mediator Pattern
- Reduced Coupling: Components don't reference each other directly
- Single Responsibility: Each component focuses on its core logic
- Easier Testing: Components can be tested independently with a mock mediator
- Centralized Control: Workflow logic is in one place (the mediator)
- Easier to Extend: Add new components without modifying existing ones
- Better Reusability: Components can be reused in different contexts
When to Use the Mediator Pattern
Use the Mediator pattern when:
- Multiple objects interact in complex ways: The mediator simplifies these interactions
- Objects are tightly coupled: You want to reduce dependencies between them
- Workflow coordination is complex: Centralize the orchestration logic
- Components need to be reusable: Components shouldn't depend on each other
- Many-to-many relationships exist: Convert them to one-to-many (all → mediator)
Real-World Examples
- Air Traffic Control: Pilots don't communicate directly; they coordinate through ATC
- Chat Room: Users send messages to the chat room, which distributes them
- GUI Framework: UI components notify the dialog/form, which coordinates responses
- Order Processing: Multiple services coordinate through a central hub
- Smart Home System: Devices communicate through a central hub/controller
Key Takeaways
- The Mediator pattern centralizes complex communications between objects
- It converts many-to-many relationships into one-to-many relationships
- Components (colleagues) communicate only through the mediator
- The mediator encapsulates how objects interact
- This reduces coupling and makes the system easier to maintain and extend
- In logistics, mediators coordinate workflows across multiple services
In the next lesson, we'll implement the Mediator pattern in TypeScript and explore different implementation approaches.