lesson

Introduction to the Mediator Pattern

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:

typescript
1class InventoryService {
2 checkStock(productId: string): boolean {
3 // Check inventory...
4 return true;
5 }
6
7 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}
13
14class 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}
22
23class 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}
31
32class NotificationService {
33 sendEmail(recipient: string, message: string): void {
34 // Send notifications...
35 // But who calls this? Everyone!
36 }
37}

Problems with Direct Communication

  1. Tight Coupling: Each service knows about and depends on multiple other services
  2. Hard to Maintain: Changes in one service ripple through all dependent services
  3. Hard to Test: Testing one service requires mocking all its dependencies
  4. Hard to Reuse: Services can't be reused independently
  5. Complex Logic: Business logic is scattered across multiple services

The dependency graph looks like spaghetti:

1InventoryService ←→ PaymentService
2 ↓ ↑ ↓ ↑
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 interactions
3ShippingService →
4NotificationService →

Now services only depend on the mediator, not on each other:

typescript
1interface OrderMediator {
2 processOrder(order: Order): void;
3 notifyInventoryReserved(orderId: string): void;
4 notifyPaymentProcessed(orderId: string, success: boolean): void;
5 notifyShippingScheduled(orderId: string): void;
6}
7
8class InventoryService {
9 constructor(private mediator: OrderMediator) {}
10
11 reserveStock(orderId: string, productId: string, quantity: number): void {
12 // Reserve inventory...
13 console.log(`Reserved ${quantity} units of ${productId}`);
14
15 // Notify mediator - let it coordinate next steps
16 this.mediator.notifyInventoryReserved(orderId);
17 }
18}
19
20class PaymentService {
21 constructor(private mediator: OrderMediator) {}
22
23 processPayment(orderId: string, amount: number): void {
24 // Process payment...
25 const success = true; // Simplified
26 console.log(`Payment processed for order ${orderId}`);
27
28 // Let mediator handle what happens next
29 this.mediator.notifyPaymentProcessed(orderId, success);
30 }
31}

Pattern Structure

The Mediator pattern consists of these key components:

typescript
1// 1. Mediator Interface
2interface Mediator {
3 notify(sender: Component, event: string, data?: any): void;
4}
5
6// 2. Concrete Mediator - coordinates interactions
7class ConcreteMediator implements Mediator {
8 private component1: Component1;
9 private component2: Component2;
10
11 notify(sender: Component, event: string, data?: any): void {
12 // Coordinate responses based on sender and event
13 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}
21
22// 3. Components (Colleagues) - communicate through mediator
23abstract class Component {
24 constructor(protected mediator: Mediator) {}
25}
26
27class Component1 extends Component {
28 doA(): void {
29 console.log('Component1 does A');
30 this.mediator.notify(this, 'A');
31 }
32}
33
34class 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 │ implements
10
11┌─────────────────────┐
12│ ConcreteMediator │
13├─────────────────────┤
14│ - component1 │
15│ - component2 │
16├─────────────────────┤
17│ + notify() │
18└─────────────────────┘
19 │ │
20 │ uses │ uses
21 ↓ ↓
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:

  1. Inventory Check: Verify product availability
  2. Inventory Reservation: Reserve stock for the order
  3. Payment Processing: Charge the customer
  4. Shipping Scheduling: Arrange delivery
  5. Customer Notification: Send status updates

Without Mediator (Problematic)

typescript
1class 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();
7
8 // Orchestration logic mixed with business logic
9 if (inventory.checkStock(order.productId, order.quantity)) {
10 inventory.reserveStock(order.productId, order.quantity);
11
12 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)

typescript
1interface 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}
8
9class OrderProcessingHub implements OrderProcessingMediator {
10 private orders = new Map<string, Order>();
11
12 constructor(
13 private inventory: InventoryService,
14 private payment: PaymentService,
15 private shipping: ShippingService,
16 private notification: NotificationService
17 ) {}
18
19 processOrder(order: Order): void {
20 this.orders.set(order.id, order);
21 this.inventory.checkStock(order.id, order.productId, order.quantity);
22 }
23
24 onInventoryChecked(orderId: string, available: boolean): void {
25 const order = this.orders.get(orderId)!;
26
27 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 }
36
37 onInventoryReserved(orderId: string): void {
38 const order = this.orders.get(orderId)!;
39 this.payment.processPayment(orderId, order.total);
40 }
41
42 onPaymentProcessed(orderId: string, success: boolean): void {
43 const order = this.orders.get(orderId)!;
44
45 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 }
55
56 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:

typescript
1class InventoryService {
2 constructor(private mediator: OrderProcessingMediator) {}
3
4 checkStock(orderId: string, productId: string, quantity: number): void {
5 const available = this.getStockLevel(productId) >= quantity;
6 this.mediator.onInventoryChecked(orderId, available);
7 }
8
9 reserveStock(orderId: string, productId: string, quantity: number): void {
10 // Reserve logic...
11 this.mediator.onInventoryReserved(orderId);
12 }
13
14 private getStockLevel(productId: string): number {
15 // Check database...
16 return 100;
17 }
18}

Benefits of the Mediator Pattern

  1. Reduced Coupling: Components don't reference each other directly
  2. Single Responsibility: Each component focuses on its core logic
  3. Easier Testing: Components can be tested independently with a mock mediator
  4. Centralized Control: Workflow logic is in one place (the mediator)
  5. Easier to Extend: Add new components without modifying existing ones
  6. 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

  1. Air Traffic Control: Pilots don't communicate directly; they coordinate through ATC
  2. Chat Room: Users send messages to the chat room, which distributes them
  3. GUI Framework: UI components notify the dialog/form, which coordinates responses
  4. Order Processing: Multiple services coordinate through a central hub
  5. 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.