Explore the practical applications and best practices of the Mediator Pattern in JavaScript and TypeScript, including case studies, state management, undo/redo functionality, and more.
The Mediator Pattern is a behavioral design pattern that facilitates communication between different components (or Colleagues) of a system without them needing to be directly aware of each other. This pattern is particularly useful in complex systems where direct communication between components can lead to a tangled web of dependencies. Instead, a Mediator object handles the interactions, promoting loose coupling and enhancing maintainability.
In a plugin-based architecture, various plugins need to interact with each other and the core application. Without a centralized mediator, each plugin might need to know about every other plugin it interacts with, leading to a tightly coupled system. The Mediator Pattern provides a solution by acting as an intermediary that manages these interactions.
Consider a text editor with plugins for spell-checking, grammar-checking, and formatting. Each plugin needs to communicate with the editor and potentially with each other. By using a Mediator, the editor can coordinate these plugins without them being directly aware of each other.
class EditorMediator {
private plugins: Map<string, Plugin> = new Map();
registerPlugin(name: string, plugin: Plugin) {
this.plugins.set(name, plugin);
plugin.setMediator(this);
}
notify(sender: Plugin, event: string) {
if (event === 'spellCheck') {
const grammarPlugin = this.plugins.get('grammar');
grammarPlugin?.action();
}
// Handle other events
}
}
interface Plugin {
setMediator(mediator: EditorMediator): void;
action(): void;
}
class SpellCheckPlugin implements Plugin {
private mediator: EditorMediator;
setMediator(mediator: EditorMediator) {
this.mediator = mediator;
}
action() {
console.log('Spell checking...');
this.mediator.notify(this, 'spellCheck');
}
}
class GrammarCheckPlugin implements Plugin {
private mediator: EditorMediator;
setMediator(mediator: EditorMediator) {
this.mediator = mediator;
}
action() {
console.log('Grammar checking...');
}
}
// Usage
const editorMediator = new EditorMediator();
const spellCheck = new SpellCheckPlugin();
const grammarCheck = new GrammarCheckPlugin();
editorMediator.registerPlugin('spell', spellCheck);
editorMediator.registerPlugin('grammar', grammarCheck);
spellCheck.action();
In this example, the EditorMediator
coordinates the interactions between the SpellCheckPlugin
and the GrammarCheckPlugin
. The plugins do not need to know about each other, only the mediator.
State management is crucial in applications with complex interactions and data flows. The Mediator Pattern can help manage state changes and notify relevant components without them being tightly coupled.
Imagine a shopping cart system where different components (like inventory, pricing, and user interface) need to respond to state changes. A Mediator can manage these interactions efficiently.
class CartMediator {
private components: Map<string, CartComponent> = new Map();
registerComponent(name: string, component: CartComponent) {
this.components.set(name, component);
component.setMediator(this);
}
notify(sender: CartComponent, event: string) {
if (event === 'itemAdded') {
const pricingComponent = this.components.get('pricing');
pricingComponent?.update();
}
// Handle other events
}
}
interface CartComponent {
setMediator(mediator: CartMediator): void;
update(): void;
}
class InventoryComponent implements CartComponent {
private mediator: CartMediator;
setMediator(mediator: CartMediator) {
this.mediator = mediator;
}
update() {
console.log('Updating inventory...');
this.mediator.notify(this, 'itemAdded');
}
}
class PricingComponent implements CartComponent {
private mediator: CartMediator;
setMediator(mediator: CartMediator) {
this.mediator = mediator;
}
update() {
console.log('Updating pricing...');
}
}
// Usage
const cartMediator = new CartMediator();
const inventory = new InventoryComponent();
const pricing = new PricingComponent();
cartMediator.registerComponent('inventory', inventory);
cartMediator.registerComponent('pricing', pricing);
inventory.update();
Here, the CartMediator
ensures that when the inventory is updated, the pricing component is also notified and updated accordingly.
The Mediator Pattern can also be applied to implement undo/redo functionality by centralizing control over actions and their reversals.
In a drawing application, users can perform actions like drawing, erasing, and transforming objects. These actions should be reversible, and the Mediator can manage the undo/redo stack.
class DrawingMediator {
private history: Command[] = [];
private redoStack: Command[] = [];
executeCommand(command: Command) {
command.execute();
this.history.push(command);
this.redoStack = []; // Clear redo stack
}
undo() {
const command = this.history.pop();
if (command) {
command.undo();
this.redoStack.push(command);
}
}
redo() {
const command = this.redoStack.pop();
if (command) {
command.execute();
this.history.push(command);
}
}
}
interface Command {
execute(): void;
undo(): void;
}
class DrawCommand implements Command {
execute() {
console.log('Drawing...');
}
undo() {
console.log('Undo drawing...');
}
}
// Usage
const drawingMediator = new DrawingMediator();
const drawCommand = new DrawCommand();
drawingMediator.executeCommand(drawCommand);
drawingMediator.undo();
drawingMediator.redo();
In this example, the DrawingMediator
manages the execution and reversal of commands, allowing for efficient undo/redo functionality.
While the Mediator Pattern can simplify interactions, it’s important to maintain clarity and avoid turning the mediator into a “god object” that knows too much.
Logging and monitoring are essential for understanding how components interact within a system. The Mediator Pattern can facilitate this by centralizing interactions, making it easier to log and monitor.
One of the key benefits of the Mediator Pattern is that it allows components (Colleagues) to be unaware of each other’s implementation details. This promotes loose coupling and enhances flexibility.
As applications grow, the mediator may need to handle more interactions. It’s important to scale the mediator effectively to maintain performance and manageability.
Versioning and updating mediator interfaces can be challenging, especially in large systems. It’s important to manage these changes carefully to avoid breaking existing functionality.
Clear communication protocols are essential for ensuring that components interact correctly and efficiently within a mediated system.
The Mediator Pattern can be integrated with messaging systems or event buses to enhance communication and scalability.
The Mediator Pattern is a powerful tool for managing interactions between components in complex systems. By centralizing communication, it promotes loose coupling and enhances maintainability. However, it’s important to apply best practices to maintain simplicity and clarity, scale effectively, and manage communication protocols. By following these guidelines, developers can leverage the Mediator Pattern to build scalable, maintainable, and efficient applications.