Mediator Pattern
Introduction
The Mediator pattern is a behavioral design pattern that reduces coupling between classes by providing a central authority through which different components may communicate and interact. It’s particularly useful in scenarios where a system has multiple interacting components, and you want to centralize complex communications and control mechanisms.
Purpose
- To centralize complex communications and control between related objects.
- To reduce coupling between classes that communicate with each other.
Use Cases
- When a set of objects communicate in well-defined but complex ways, the components become mutually dependent.
- To centralize control and communication between related components in a single mediator object.
Advantages/Disadvantages
Advantages:
- Reduces direct communications between the components and limits dependencies.
- Centralizes control in one location.
- Simplifies maintenance and interactions between components.
Disadvantages:
- The mediator can become overly complex, taking on too many responsibilities.
- May lead to a ‘god object’ antipattern if not implemented carefully.
Implementation in JavaScript
class Mediator {
notify(sender, event) {}
}
class ConcreteMediator extends Mediator {
constructor(component1, component2) {
super();
this.component1 = component1;
this.component1.setMediator(this);
this.component2 = component2;
this.component2.setMediator(this);
}
notify(sender, event) {
if (event === 'A') {
console.log('Mediator reacts on A and triggers following operations:');
this.component2.doC();
} else if (event === 'D') {
console.log('Mediator reacts on D and triggers following operations:');
this.component1.doB();
this.component2.doC();
}
}
}
class BaseComponent {
constructor(mediator = null) {
this.mediator = mediator;
}
setMediator(mediator) {
this.mediator = mediator;
}
}
class Component1 extends BaseComponent {
doA() {
console.log('Component 1 does A.');
this.mediator.notify(this, 'A');
}
doB() {
console.log('Component 1 does B.');
this.mediator.notify(this, 'B');
}
}
class Component2 extends BaseComponent {
doC() {
console.log('Component 2 does C.');
this.mediator.notify(this, 'C');
}
doD() {
console.log('Component 2 does D.');
this.mediator.notify(this, 'D');
}
}
// Usage
const c1 = new Component1();
const c2 = new Component2();
const mediator = new ConcreteMediator(c1, c2);
console.log('Client triggers operation A.');
c1.doA();
console.log('Client triggers operation D.');
c2.doD();
Practical Example
Imagine a scenario in a backend system where various services like a database service, logging service, and authentication service need to interact with each other. Using a mediator can simplify these interactions.
class DatabaseService {
save(data) {
console.log(`Saving data: ${data}`);
}
}
class LoggingService {
log(message) {
console.log(`Logging: ${message}`);
}
}
class AuthenticationService {
authenticate(user) {
console.log(`Authenticating user: ${user}`);
return true;
}
}
class SystemMediator {
constructor() {
this.databaseService = new DatabaseService();
this.loggingService = new LoggingService();
this.authenticationService = new AuthenticationService();
}
handle(event, data) {
switch (event) {
case 'save':
this.databaseService.save(data);
break;
case 'log':
this.loggingService.log(data);
break;
case 'authenticate':
return this.authenticationService.authenticate(data);
default:
console.log('Unknown event');
}
}
}
// Usage
const mediator = new SystemMediator();
mediator.handle('authenticate', 'User123');
mediator.handle('save', 'UserData');
mediator.handle('log', 'User authenticated and data saved');
In this example, SystemMediator
acts as a central hub for different services, thus simplifying their interactions and reducing direct dependencies among them.