Loading content...
In the previous page, we explored how many-to-many object communication creates exponential complexity, scattered logic, and maintenance nightmares. The insight that emerged was powerful: replace direct object-to-object communication with indirect communication through a central coordinator.
The Mediator Pattern formalizes this insight into a robust, reusable design. Named after the concept of mediation in conflict resolution, where a neutral third party facilitates communication between disputing parties, the software Mediator serves as the single point of coordination for a group of collaborating objects.
This page provides a comprehensive exploration of the Mediator Pattern solution. You'll understand the architectural structure, the roles of participating objects, implementation techniques, and see complete code transformations that eliminate the coupling problems from the previous page.
The Gang of Four defines the Mediator Pattern as:
"Define an object that encapsulates how a set of objects interact. Mediator promotes loose coupling by keeping objects from referring to each other explicitly, and it lets you vary their interaction independently."
Let's unpack each part of this definition:
The Mediator Pattern transforms complex many-to-many relationships into simple star topology. Instead of every object knowing about every other object, every object knows only about the central mediator. The mediator becomes the 'brain' that coordinates behavior.
The Mediator Pattern consists of four key participants that work together to enable decoupled communication:
| Participant | Responsibility | Key Characteristic |
|---|---|---|
| Mediator (interface) | Declares the interface for communication between colleagues | Defines the notify/send protocol |
| ConcreteMediator | Implements coordination logic; maintains references to all colleagues | Contains the 'business logic' of interaction |
| Colleague (interface) | Defines common interface for objects that will be mediated | Knows only about the Mediator interface |
| ConcreteColleague | Implements specific behavior; communicates via mediator only | Independent and reusable |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
// ═══════════════════════════════════════════════════════════════════// MEDIATOR PATTERN - STRUCTURAL OVERVIEW// ═══════════════════════════════════════════════════════════════════ /** * Mediator Interface * * Declares the communication protocol between the mediator and colleagues. * Colleagues use this to notify the mediator of events that may require * coordination with other colleagues. */interface Mediator { /** * Central notification method. Colleagues call this when something * happens that other colleagues might need to know about. * * @param sender - The colleague that triggered the notification * @param event - Description of what happened * @param data - Optional payload with event details */ notify(sender: Colleague, event: string, data?: any): void;} /** * Colleague Base Class * * Provides the common infrastructure for all mediated objects. * Each colleague holds a reference to the mediator but knows nothing * about other colleagues. */abstract class Colleague { protected mediator: Mediator; constructor(mediator: Mediator) { this.mediator = mediator; } /** * Convenience method for subclasses to notify the mediator */ protected send(event: string, data?: any): void { this.mediator.notify(this, event, data); }} /** * Concrete Mediator * * Implements the actual coordination logic. This is where the * "intelligence" of the interaction lives. The mediator: * * 1. Receives notifications from colleagues * 2. Interprets what coordination is needed * 3. Calls appropriate methods on relevant colleagues */class ConcreteMediator implements Mediator { // The mediator maintains references to all colleagues private colleagueA: ConcreteColleagueA; private colleagueB: ConcreteColleagueB; private colleagueC: ConcreteColleagueC; // Registration allows dynamic colleague management public registerColleagueA(colleague: ConcreteColleagueA): void { this.colleagueA = colleague; } public registerColleagueB(colleague: ConcreteColleagueB): void { this.colleagueB = colleague; } public registerColleagueC(colleague: ConcreteColleagueC): void { this.colleagueC = colleague; } /** * Central coordination logic * * This method contains all the interaction rules that were previously * scattered across the colleague classes. It's the single source of * truth for how colleagues should coordinate. */ public notify(sender: Colleague, event: string, data?: any): void { // Coordination logic is centralized here if (sender === this.colleagueA && event === "actionX") { // When A does X, B and C need to react this.colleagueB.reactToX(data); this.colleagueC.updateFromX(data); } if (sender === this.colleagueB && event === "actionY") { // When B does Y, only A needs to react this.colleagueA.handleY(data); } if (event === "stateChange") { // Any colleague's state change might affect others // This is where complex coordination logic lives this.synchronizeState(); } } private synchronizeState(): void { // Complex multi-party synchronization logic // Previously this would be duplicated across colleagues }} /** * Concrete Colleagues * * Each colleague implements its own behavior but coordinates through * the mediator. Notice that colleagues never reference each other. */class ConcreteColleagueA extends Colleague { public doActionX(): void { // Perform local action console.log("ColleagueA performing action X"); // Notify mediator - let it coordinate with others this.send("actionX", { origin: "A", timestamp: Date.now() }); } public handleY(data: any): void { // React to coordination from mediator console.log("ColleagueA handling Y:", data); }} class ConcreteColleagueB extends Colleague { public doActionY(): void { console.log("ColleagueB performing action Y"); this.send("actionY", { origin: "B" }); } public reactToX(data: any): void { console.log("ColleagueB reacting to X:", data); }} class ConcreteColleagueC extends Colleague { public updateFromX(data: any): void { console.log("ColleagueC updating from X:", data); }}Key structural observations:
Colleagues are independent: ConcreteColleagueA has no import, no reference, and no awareness of ConcreteColleagueB or ConcreteColleagueC
The mediator is the only 'aware' entity: It knows about all colleagues and their capabilities
Coordination logic is explicit: The notify method clearly documents how events trigger reactions
Adding colleagues is additive: New colleagues can join the mediation without modifying existing colleagues
Let's implement a realistic example that transforms the problematic dialog box from the previous page into a clean, mediated design. We'll build a user profile configuration dialog with interdependent form controls.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
// ═══════════════════════════════════════════════════════════════════// MEDIATOR PATTERN - USER PROFILE DIALOG EXAMPLE// ═══════════════════════════════════════════════════════════════════ // ─────────────────────────────────────────────────────────────────────// STEP 1: Define the Mediator Interface// ───────────────────────────────────────────────────────────────────── /** * DialogMediator - contract for dialog coordination * * All UI components in the dialog will communicate through this interface. * The mediator receives component events and orchestrates responses. */interface DialogMediator { componentChanged(component: DialogComponent, event: ComponentEvent): void;} /** * Event structure for component communications */interface ComponentEvent { type: string; value?: any; metadata?: Record<string, any>;} // ─────────────────────────────────────────────────────────────────────// STEP 2: Define the Colleague Base Class// ───────────────────────────────────────────────────────────────────── /** * DialogComponent - base class for all UI components in the dialog * * Each component holds a reference to the mediator but knows nothing * about other components. Components are fully independent and reusable. */abstract class DialogComponent { protected mediator: DialogMediator; protected enabled: boolean = true; protected visible: boolean = true; constructor(mediator: DialogMediator) { this.mediator = mediator; } /** * Notify the mediator that something changed * Subclasses call this instead of directly manipulating other components */ protected notifyChange(event: ComponentEvent): void { this.mediator.componentChanged(this, event); } public enable(): void { this.enabled = true; console.log(` [${this.constructor.name}] enabled`); } public disable(): void { this.enabled = false; console.log(` [${this.constructor.name}] disabled`); } public show(): void { this.visible = true; console.log(` [${this.constructor.name}] shown`); } public hide(): void { this.visible = false; console.log(` [${this.constructor.name}] hidden`); } public isEnabled(): boolean { return this.enabled; } public isVisible(): boolean { return this.visible; }} // ─────────────────────────────────────────────────────────────────────// STEP 3: Implement Concrete UI Components (Colleagues)// ───────────────────────────────────────────────────────────────────── /** * TextInput - handles text entry * * Notice: This class has NO knowledge of dropdowns, buttons, or any * other component. It only knows about the mediator. */class TextInput extends DialogComponent { private value: string = ""; private placeholder: string = "Enter text..."; private maxLength: number = 100; public getValue(): string { return this.value; } public setValue(value: string): void { if (!this.enabled) return; this.value = value.substring(0, this.maxLength); console.log(` [TextInput] value changed to: "${this.value}"`); // Notify mediator - let it decide what else should happen this.notifyChange({ type: "textChanged", value: this.value, metadata: { isEmpty: this.value.length === 0 } }); } public setPlaceholder(placeholder: string): void { this.placeholder = placeholder; console.log(` [TextInput] placeholder: "${placeholder}"`); } public setMaxLength(length: number): void { this.maxLength = length; } public clear(): void { this.setValue(""); }} /** * Dropdown - handles option selection */class Dropdown extends DialogComponent { private options: string[] = []; private selectedValue: string | null = null; public setOptions(options: string[]): void { this.options = options; console.log(` [Dropdown] options set: [${options.join(", ")}]`); } public select(value: string): void { if (!this.enabled) return; if (!this.options.includes(value)) return; this.selectedValue = value; console.log(` [Dropdown] selected: "${value}"`); this.notifyChange({ type: "selectionChanged", value: this.selectedValue }); } public getSelectedValue(): string | null { return this.selectedValue; } public clearSelection(): void { this.selectedValue = null; console.log(` [Dropdown] selection cleared`); }} /** * Checkbox - handles boolean state */class Checkbox extends DialogComponent { private checked: boolean = false; private label: string; constructor(mediator: DialogMediator, label: string) { super(mediator); this.label = label; } public toggle(): void { if (!this.enabled) return; this.checked = !this.checked; console.log(` [Checkbox:${this.label}] toggled to: ${this.checked}`); this.notifyChange({ type: "checkboxToggled", value: this.checked, metadata: { label: this.label } }); } public isChecked(): boolean { return this.checked; } public setChecked(checked: boolean): void { this.checked = checked; console.log(` [Checkbox:${this.label}] set to: ${checked}`); }} /** * Button - handles click actions */class Button extends DialogComponent { private text: string; constructor(mediator: DialogMediator, text: string) { super(mediator); this.text = text; } public click(): void { if (!this.enabled) return; console.log(` [Button:${this.text}] clicked`); this.notifyChange({ type: "buttonClicked", metadata: { buttonText: this.text } }); } public setText(text: string): void { this.text = text; console.log(` [Button] text changed to: "${text}"`); }} /** * ValidationLabel - displays validation messages */class ValidationLabel extends DialogComponent { private message: string = ""; private isError: boolean = false; public showError(message: string): void { this.message = message; this.isError = true; console.log(` [ValidationLabel] ERROR: ${message}`); } public showSuccess(message: string): void { this.message = message; this.isError = false; console.log(` [ValidationLabel] SUCCESS: ${message}`); } public clear(): void { this.message = ""; this.isError = false; console.log(` [ValidationLabel] cleared`); } public hasError(): boolean { return this.isError; }} /** * PreviewPanel - shows real-time preview */class PreviewPanel extends DialogComponent { private previewContent: string = ""; public updatePreview(content: string): void { this.previewContent = content; console.log(` [PreviewPanel] preview updated`); } public clearPreview(): void { this.previewContent = ""; console.log(` [PreviewPanel] preview cleared`); } public getPreview(): string { return this.previewContent; }} // ─────────────────────────────────────────────────────────────────────// STEP 4: Implement the Concrete Mediator// ───────────────────────────────────────────────────────────────────── /** * ProfileDialogMediator - coordinates all dialog interactions * * This is where ALL coordination logic lives. Notice: * 1. It knows about all components * 2. It contains all the "business rules" for how components interact * 3. Components are blissfully unaware of each other */class ProfileDialogMediator implements DialogMediator { // Component references private usernameInput: TextInput; private accountTypeDropdown: Dropdown; private enableNotificationsCheckbox: Checkbox; private advancedSettingsCheckbox: Checkbox; private submitButton: Button; private resetButton: Button; private validationLabel: ValidationLabel; private previewPanel: PreviewPanel; /** * Initialize the dialog with all its components * The mediator is the factory and coordinator */ public initialize(): void { // Create all components with this mediator this.usernameInput = new TextInput(this); this.accountTypeDropdown = new Dropdown(this); this.enableNotificationsCheckbox = new Checkbox(this, "Enable Notifications"); this.advancedSettingsCheckbox = new Checkbox(this, "Advanced Settings"); this.submitButton = new Button(this, "Submit"); this.resetButton = new Button(this, "Reset"); this.validationLabel = new ValidationLabel(this); this.previewPanel = new PreviewPanel(this); // Set initial states this.accountTypeDropdown.setOptions(["Personal", "Business", "Enterprise"]); this.submitButton.disable(); // Disabled until valid input console.log("\n✅ ProfileDialogMediator initialized\n"); } /** * Central coordination hub - all component events flow through here * * This replaces the scattered logic that was previously in each component. * All business rules are explicit and documented in one place. */ public componentChanged(component: DialogComponent, event: ComponentEvent): void { console.log(`\n📬 Mediator received: ${event.type}`); // ───────────────────────────────────────────────────────────── // RULE 1: Username validation and submit button state // ───────────────────────────────────────────────────────────── if (component === this.usernameInput && event.type === "textChanged") { const username = event.value as string; const isValid = this.validateUsername(username); if (isValid) { this.validationLabel.showSuccess("Username is valid"); this.submitButton.enable(); } else if (username.length === 0) { this.validationLabel.clear(); this.submitButton.disable(); } else { this.validationLabel.showError("Username must be 3-20 characters"); this.submitButton.disable(); } // Update preview this.updateLivePreview(); } // ───────────────────────────────────────────────────────────── // RULE 2: Account type affects available options // ───────────────────────────────────────────────────────────── if (component === this.accountTypeDropdown && event.type === "selectionChanged") { const accountType = event.value as string; // Business rule: Enterprise accounts get advanced settings if (accountType === "Enterprise") { this.advancedSettingsCheckbox.show(); this.advancedSettingsCheckbox.enable(); this.usernameInput.setPlaceholder("Enter enterprise username..."); this.usernameInput.setMaxLength(50); } else { this.advancedSettingsCheckbox.hide(); this.advancedSettingsCheckbox.setChecked(false); this.usernameInput.setPlaceholder("Enter username..."); this.usernameInput.setMaxLength(20); } // Business rule: Business accounts have notifications on by default if (accountType === "Business") { this.enableNotificationsCheckbox.setChecked(true); } this.updateLivePreview(); } // ───────────────────────────────────────────────────────────── // RULE 3: Checkbox interactions // ───────────────────────────────────────────────────────────── if (event.type === "checkboxToggled") { this.updateLivePreview(); } // ───────────────────────────────────────────────────────────── // RULE 4: Button actions // ───────────────────────────────────────────────────────────── if (component === this.submitButton && event.type === "buttonClicked") { this.handleSubmit(); } if (component === this.resetButton && event.type === "buttonClicked") { this.handleReset(); } } /** * Business logic: Username validation rules */ private validateUsername(username: string): boolean { return username.length >= 3 && username.length <= 20; } /** * Coordinate the preview update across relevant components */ private updateLivePreview(): void { const username = this.usernameInput.getValue(); const accountType = this.accountTypeDropdown.getSelectedValue() || "Not selected"; const notifications = this.enableNotificationsCheckbox.isChecked(); const advanced = this.advancedSettingsCheckbox.isChecked(); const preview = [ `Username: ${username || "(empty)"}`, `Account Type: ${accountType}`, `Notifications: ${notifications ? "Enabled" : "Disabled"}`, advanced ? `Advanced Settings: Enabled` : "" ].filter(Boolean).join(" | "); this.previewPanel.updatePreview(preview); } /** * Handle form submission */ private handleSubmit(): void { console.log("\n🚀 Form submitted!"); console.log(` Preview: ${this.previewPanel.getPreview()}`); // In real app: send data to server, close dialog, etc. } /** * Handle form reset */ private handleReset(): void { console.log("\n🔄 Form reset"); this.usernameInput.clear(); this.accountTypeDropdown.clearSelection(); this.enableNotificationsCheckbox.setChecked(false); this.advancedSettingsCheckbox.setChecked(false); this.advancedSettingsCheckbox.hide(); this.validationLabel.clear(); this.previewPanel.clearPreview(); this.submitButton.disable(); } // Expose components for testing/demonstration public getComponents() { return { usernameInput: this.usernameInput, accountTypeDropdown: this.accountTypeDropdown, enableNotificationsCheckbox: this.enableNotificationsCheckbox, advancedSettingsCheckbox: this.advancedSettingsCheckbox, submitButton: this.submitButton, resetButton: this.resetButton, }; }}What we've achieved:
Complete decoupling: TextInput doesn't know Dropdown exists. Checkbox doesn't know about Button. Each component is fully independent.
Centralized coordination: All the "when X happens, do Y" logic is in ProfileDialogMediator.componentChanged(). Business rules are explicit and documented.
Reusable components: These same TextInput, Dropdown, and Button classes can be used in any dialog with any coordination logic—just provide a different mediator.
Testable design: Test the mediator's coordination logic with mock components. Test components in isolation. No complex object graphs needed.
Understanding how communication flows through the mediator is crucial for mastering this pattern. Let's trace a complete interaction sequence:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
// ═══════════════════════════════════════════════════════════════════// TRACING COMMUNICATION FLOW// ═══════════════════════════════════════════════════════════════════ /** * Demonstration of a complete interaction flow */function demonstrateCommunicationFlow(): void { // Initialize the mediated dialog const mediator = new ProfileDialogMediator(); mediator.initialize(); const { usernameInput, accountTypeDropdown, submitButton } = mediator.getComponents(); console.log("═══════════════════════════════════════════════════════════"); console.log("FLOW 1: User types a valid username"); console.log("═══════════════════════════════════════════════════════════"); // Step 1: User types "john" in the username field // This triggers this sequence: usernameInput.setValue("john"); // Flow trace: // 1. TextInput.setValue("john") executes // 2. TextInput stores the value // 3. TextInput calls this.notifyChange({ type: "textChanged", value: "john" }) // 4. This calls mediator.componentChanged(usernameInput, event) // 5. Mediator recognizes the event and sender // 6. Mediator calls validateUsername("john") → returns true // 7. Mediator calls validationLabel.showSuccess(...) // 8. Mediator calls submitButton.enable() // 9. Mediator calls updateLivePreview() console.log("\n═══════════════════════════════════════════════════════════"); console.log("FLOW 2: User selects 'Enterprise' account type"); console.log("═══════════════════════════════════════════════════════════"); accountTypeDropdown.select("Enterprise"); // Flow trace: // 1. Dropdown.select("Enterprise") executes // 2. Dropdown stores the selection // 3. Dropdown calls this.notifyChange({ type: "selectionChanged", value: "Enterprise" }) // 4. Mediator.componentChanged receives the event // 5. Mediator checks the value and applies business rules: // - Shows and enables advancedSettingsCheckbox // - Changes username placeholder to enterprise version // - Sets max length to 50 // 6. Mediator updates the live preview console.log("\n═══════════════════════════════════════════════════════════"); console.log("FLOW 3: User clicks Submit"); console.log("═══════════════════════════════════════════════════════════"); submitButton.click(); // Flow trace: // 1. Button.click() executes // 2. Button calls this.notifyChange({ type: "buttonClicked", ... }) // 3. Mediator identifies the Submit button clicked // 4. Mediator calls handleSubmit() - internal method // 5. Form data is processed/sent} // Execute the demonstrationdemonstrateCommunicationFlow();Key observations about the communication flow:
notifyChange()componentChanged(). This is the method to debug when interaction seems wrong.When something goes wrong in a mediated system, you only need to trace through the mediator's coordination logic. Set a breakpoint in componentChanged() and you'll see every interaction. No more hunting through multiple classes trying to understand the flow.
Different systems require different approaches to connecting colleagues with their mediator. The choice affects flexibility, type safety, and maintenance complexity.
123456789101112131415161718192021
// Static registration - colleagues fixed at construction time class StaticMediator implements DialogMediator { // Colleagues defined as constructor parameters constructor( private textInput: TextInput, private dropdown: Dropdown, private submitButton: Button ) { // All colleagues known at construction } public componentChanged(component: DialogComponent, event: ComponentEvent): void { // Safe to use all colleagues - guaranteed to exist }} // Usageconst input = new TextInput(/* mediator needed first - circular dependency! */); // Problem: Circular dependency between mediator and colleagues1234567891011121314151617181920212223242526272829303132
// Dynamic registration - colleagues added after construction class DynamicMediator implements DialogMediator { private textInput?: TextInput; private dropdown?: Dropdown; private submitButton?: Button; // Explicit registration methods public registerTextInput(input: TextInput): void { this.textInput = input; } public registerDropdown(dropdown: Dropdown): void { this.dropdown = dropdown; } public registerSubmitButton(button: Button): void { this.submitButton = button; } public componentChanged(component: DialogComponent, event: ComponentEvent): void { // Must check for existence before use if (this.textInput && component === this.textInput) { // Handle text input events } }} // Usage - no circular dependencyconst mediator = new DynamicMediator();const input = new TextInput(mediator);mediator.registerTextInput(input);12345678910111213141516171819202122232425262728293031
// Generic registration - type-keyed colleague storage class FlexibleMediator implements DialogMediator { private colleagues: Map<string, DialogComponent> = new Map(); // Register by name or type public register(name: string, colleague: DialogComponent): void { this.colleagues.set(name, colleague); } public getColleague<T extends DialogComponent>(name: string): T | undefined { return this.colleagues.get(name) as T | undefined; } public componentChanged(component: DialogComponent, event: ComponentEvent): void { const submitButton = this.getColleague<Button>("submitButton"); const validationLabel = this.getColleague<ValidationLabel>("validation"); // Use retrieved colleagues... }} // Advantages:// - Highly flexible - any number of colleagues// - Easy to add new colleagues without interface changes// - Supports optional colleagues naturally // Disadvantages:// - Loses compile-time type safety// - Relies on string keys (typo-prone)// - Must check existence at runtime| Strategy | Type Safety | Flexibility | Circular Dep | Best For |
|---|---|---|---|---|
| Constructor | High | Low | Problem | Fixed, known colleague set |
| Setter | Medium | Medium | Solved | Common case, balanced needs |
| Generic Map | Low | High | Solved | Dynamic, plugin-based systems |
The mediator interface can take several forms, each with different tradeoffs. The choice significantly impacts maintainability and extensibility.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
// ═══════════════════════════════════════════════════════════════════// MEDIATOR INTERFACE DESIGN PATTERNS// ═══════════════════════════════════════════════════════════════════ // ─────────────────────────────────────────────────────────────────────// PATTERN 1: Generic Notification (Used in our examples)// ───────────────────────────────────────────────────────────────────── /** * Single generic method handles all communication * * Pros: Simple interface, extensible without interface changes * Cons: Stringly-typed events, switch/if statements in implementation */interface GenericMediator { notify(sender: object, event: string, data?: any): void;} // ─────────────────────────────────────────────────────────────────────// PATTERN 2: Specific Methods// ───────────────────────────────────────────────────────────────────── /** * Dedicated methods for each type of notification * * Pros: Type-safe, self-documenting, IDE autocomplete * Cons: Interface grows with each new event type */interface SpecificMediator { onTextInputChanged(input: TextInput, newValue: string): void; onDropdownSelected(dropdown: Dropdown, selectedValue: string): void; onCheckboxToggled(checkbox: Checkbox, checked: boolean): void; onButtonClicked(button: Button): void;} // ─────────────────────────────────────────────────────────────────────// PATTERN 3: Typed Event Objects// ───────────────────────────────────────────────────────────────────── /** * Strongly-typed event hierarchy * * Pros: Type safety with extensibility, pattern matching in handler * Cons: More boilerplate, event class hierarchy needed */interface MediatorEvent { readonly sender: DialogComponent;} class TextChangedEvent implements MediatorEvent { constructor( public readonly sender: TextInput, public readonly newValue: string, public readonly previousValue: string ) {}} class SelectionChangedEvent implements MediatorEvent { constructor( public readonly sender: Dropdown, public readonly selectedValue: string ) {}} interface TypedMediator { handle(event: MediatorEvent): void;} class TypedMediatorImpl implements TypedMediator { handle(event: MediatorEvent): void { if (event instanceof TextChangedEvent) { // Type-safe access to event.newValue, event.previousValue this.handleTextChange(event); } else if (event instanceof SelectionChangedEvent) { // Type-safe access to event.selectedValue this.handleSelection(event); } } private handleTextChange(event: TextChangedEvent): void { console.log(`Text changed from "${event.previousValue}" to "${event.newValue}"`); } private handleSelection(event: SelectionChangedEvent): void { console.log(`Selected: ${event.selectedValue}`); }} // ─────────────────────────────────────────────────────────────────────// PATTERN 4: Command-Style (with explicit response)// ───────────────────────────────────────────────────────────────────── /** * Request-response pattern for two-way communication * * Pros: Colleagues can get information back from mediator * Cons: More complex, may introduce blocking */interface RequestResponseMediator { send<TResponse>(request: MediatorRequest<TResponse>): TResponse;} interface MediatorRequest<TResponse> { execute(mediator: RequestResponseMediatorImpl): TResponse;} class ValidateUsernameRequest implements MediatorRequest<boolean> { constructor(private username: string) {} execute(mediator: RequestResponseMediatorImpl): boolean { return mediator.validateUsername(this.username); }} class RequestResponseMediatorImpl implements RequestResponseMediator { send<TResponse>(request: MediatorRequest<TResponse>): TResponse { return request.execute(this); } validateUsername(username: string): boolean { return username.length >= 3; }}For most applications, start with the Generic Notification pattern (Pattern 1). It's simple, extensible, and the minor loss of type safety is acceptable. Move to Typed Event Objects (Pattern 3) when the event space becomes large or when team discipline around string events becomes problematic.
Let's crystallize the transformation by comparing the direct communication approach with the mediated approach:
| Metric | Direct | Mediated | Improvement |
|---|---|---|---|
| Total Dependencies | 28 | 8 | 72% reduction |
| Lines in Coordination Logic | ~200 (scattered) | ~80 (centralized) | 60% reduction |
| Files Modified to Add Component | 8 | 1 | 87% reduction |
| Mock Objects for Unit Test | 7 | 1 | 86% reduction |
The Mediator Pattern doesn't just organize code differently—it fundamentally changes the scalability characteristics of your system. Adding the 9th, 10th, or 20th component has constant cost instead of linear cost. This is the difference between maintainable software and legacy nightmares.
We've comprehensively explored the Mediator Pattern solution. Let's consolidate the key insights:
What's next:
With the Mediator Pattern solution firmly understood, the next page compares Mediator with Facade—two patterns that look similar but serve fundamentally different purposes. Understanding this distinction is crucial for applying patterns correctly.
You now understand how the Mediator Pattern transforms chaotic many-to-many communication into clean, centralized coordination. You can implement mediators, design appropriate interfaces, and choose registration strategies. Next, we'll contrast Mediator with the similar-looking Facade pattern.