Decoupling component communication with the Mediator pattern
The mediator pattern is a behavioral design pattern that reduces direct dependencies between components by encapsulating their interaction logic. It is particularly suitable for complex component communication scenarios, effectively lowering system coupling and improving maintainability. In JavaScript, implementing the mediator pattern can elegantly handle many-to-many relationships between UI components, modules, or services.
Core Concept of the Mediator Pattern
The mediator pattern defines a mediator object to encapsulate interactions among a group of objects. Instead of directly referencing each other, objects now communicate only with the mediator, transforming a web of dependencies into a star structure. This transformation offers two key advantages:
- Reduces direct coupling between objects, as each object only needs to know the mediator.
- Centralizes control of interaction logic, making system behavior easier to understand and modify.
A typical mediator structure consists of the following roles:
- Mediator: Defines the interface for colleague objects to interact with the mediator.
- ConcreteMediator: Implements the logic for coordinating colleague objects.
- Colleague: Knows its mediator object and forwards communication through it.
Implementation Approaches in JavaScript
There are three main ways to implement the mediator pattern in JavaScript:
Event-Based Implementation
class EventMediator {
constructor() {
this.events = {};
}
subscribe(event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
}
publish(event, ...args) {
if (!this.events[event]) return;
this.events[event].forEach(callback => callback(...args));
}
}
// Usage Example
const mediator = new EventMediator();
// Component A subscribes to an event
mediator.subscribe('dataLoaded', (data) => {
console.log('Component A received:', data);
});
// Component B triggers the event
fetch('/api/data').then(res => res.json()).then(data => {
mediator.publish('dataLoaded', data);
});
Command-Based Implementation
class CommandMediator {
constructor() {
this.commands = new Map();
}
register(command, handler) {
this.commands.set(command, handler);
}
execute(command, ...args) {
if (!this.commands.has(command)) {
throw new Error(`Command ${command} not registered`);
}
return this.commands.get(command)(...args);
}
}
// Usage Example
const mediator = new CommandMediator();
// Register command handler
mediator.register('updateUser', (userId, data) => {
console.log(`Updating user ${userId} with`, data);
return true;
});
// Execute command
mediator.execute('updateUser', 123, { name: 'John' });
Hybrid Implementation
Combining the strengths of events and commands:
class AdvancedMediator {
constructor() {
this.events = {};
this.commands = new Map();
}
// Event section
on(event, callback) { /* Same as event implementation */ }
emit(event, ...args) { /* Same as event implementation */ }
// Command section
command(name, handler) { /* Same as command implementation */ }
exec(name, ...args) { /* Same as command implementation */ }
// New request/response pattern
request(type, data) {
return new Promise((resolve) => {
const requestId = Math.random().toString(36).substr(2);
this.once(`${type}.${requestId}`, resolve);
this.emit(type, { requestId, data });
});
}
}
Practical Application Scenarios
Complex Form Validation
Consider a user registration form with username, email, password, and confirm password fields, requiring real-time validation and cross-field checks:
class FormMediator {
constructor() {
this.fields = {};
this.errors = {};
}
registerField(name, validateFn) {
this.fields[name] = validateFn;
}
validateField(name, value) {
const error = this.fields[name](value);
this.errors[name] = error;
this.checkFormValidity();
return error;
}
checkFormValidity() {
const isValid = Object.values(this.errors).every(err => !err);
this.emit('validityChange', isValid);
}
}
// Usage Example
const mediator = new FormMediator();
// Register field validation
mediator.registerField('username', (val) => {
if (!val) return 'Required';
if (val.length < 3) return 'Too short';
return null;
});
mediator.registerField('password', (val) => {
if (!val) return 'Required';
if (val.length < 8) return 'At least 8 characters';
return null;
});
// Notify mediator on field changes
document.getElementById('username').addEventListener('input', (e) => {
const error = mediator.validateField('username', e.target.value);
showError('username', error);
});
Cross-Component State Synchronization
In an e-commerce site, product list, shopping cart, and inventory components need to stay synchronized:
class ECommerceMediator {
constructor() {
this.products = [];
this.cart = [];
}
addToCart(productId) {
const product = this.products.find(p => p.id === productId);
if (!product || product.stock <= 0) return false;
product.stock--;
this.cart.push({...product, quantity: 1});
this.notifyAll();
return true;
}
notifyAll() {
this.emit('productsUpdated', this.products);
this.emit('cartUpdated', this.cart);
}
}
// Components only need to listen for relevant events
const mediator = new ECommerceMediator();
// Product list component
mediator.on('productsUpdated', (products) => {
renderProductList(products);
});
// Shopping cart component
mediator.on('cartUpdated', (cart) => {
updateCartUI(cart);
});
Performance Optimization and Considerations
While the mediator pattern effectively decouples components, improper implementation can lead to performance issues:
- Avoid Over-Notification: Precisely control notification scope, broadcasting only when necessary.
// Bad practice: Notifying all for any change
updateProduct() {
this.product = newValue;
this.mediator.notifyAll();
}
// Good practice: Targeted notification
updateStock() {
this.stock = newValue;
this.mediator.notify('stockChanged', this.id, this.stock);
}
- Memory Management: Clean up unused subscriptions promptly.
// When unmounting a component
componentWillUnmount() {
this.mediator.off('eventName', this.handler);
}
- Circular Dependencies: Be mindful of circular references between mediators and colleagues.
// May cause memory leaks
class Colleague {
constructor(mediator) {
this.mediator = mediator;
mediator.register(this); // Mediator also holds colleague reference
}
}
Differences from the Observer Pattern
While both patterns involve object communication, they differ fundamentally:
Feature | Mediator Pattern | Observer Pattern |
---|---|---|
Communication Direction | Bidirectional (between mediator and colleagues) | Unidirectional (subject to observer) |
Coupling | Loose coupling (indirect via mediator) | Subject knows observers exist |
Control Center | Explicit central mediator node | No central node |
Use Case | Complex web-like communication | Simple one-to-many dependencies |
// Observer Pattern Implementation
class Subject {
constructor() {
this.observers = [];
}
addObserver(obs) {
this.observers.push(obs);
}
notify(data) {
this.observers.forEach(obs => obs.update(data));
}
}
// Mediator emphasizes coordinating complex interactions
class ChatRoom {
constructor() {
this.users = {};
}
register(user) {
this.users[user.id] = user;
user.chatroom = this;
}
send(message, from, to) {
if (to) {
this.users[to].receive(message, from);
} else {
Object.values(this.users).forEach(user => {
if (user.id !== from) user.receive(message, from);
});
}
}
}
Applications in Popular Frameworks
State Lifting in React
React's state lifting is essentially a simplified mediator pattern:
function ParentComponent() {
const [sharedState, setSharedState] = useState(null);
return (
<>
<ChildA
state={sharedState}
onChange={setSharedState}
/>
<ChildB
state={sharedState}
onReset={() => setSharedState(null)}
/>
</>
);
}
Vue's Event Bus
Common global event bus in Vue 2:
// eventBus.js
import Vue from 'vue';
export const EventBus = new Vue();
// ComponentA.vue
EventBus.$emit('message', 'Hello');
// ComponentB.vue
EventBus.$on('message', (msg) => {
console.log(msg);
});
Angular Service Mediator
Angular often uses services as mediators:
@Injectable({ providedIn: 'root' })
export class DataMediator {
private dataSubject = new BehaviorSubject<any>(null);
public data$ = this.dataSubject.asObservable();
updateData(data: any) {
this.dataSubject.next(data);
}
}
// Component A
constructor(private mediator: DataMediator) {}
this.mediator.updateData(newData);
// Component B
constructor(private mediator: DataMediator) {}
this.mediator.data$.subscribe(data => this.process(data));
Pattern Variants and Advanced Techniques
Domain-Specific Mediators
Custom mediators for specific domains:
class UIMediator {
constructor() {
this.components = {};
}
register(component) {
this.components[component.name] = component;
component.setMediator(this);
}
notify(sender, event, data) {
switch(event) {
case 'buttonClick':
this.components.form.validate();
break;
case 'validationComplete':
this.components.submitButton.setEnabled(data.isValid);
break;
// Other UI interaction logic
}
}
}
Middleware Enhancement
Adding middleware pipelines to mediators:
class PipelineMediator {
constructor() {
this.middlewares = [];
}
use(middleware) {
this.middlewares.push(middleware);
}
async execute(event, data) {
let current = 0;
const next = async () => {
if (current < this.middlewares.length) {
const middleware = this.middlewares[current++];
await middleware(event, data, next);
}
};
await next();
}
}
// Using middleware
mediator.use(async (event, data, next) => {
console.log(`Logging event: ${event}`);
await next();
console.log(`Event ${event} processed`);
});
Distributed Mediators
For cross-application communication in micro-frontend architectures:
class CrossAppMediator {
constructor() {
window.__MEDIATOR_CHANNEL__ = new BroadcastChannel('mediator');
}
publish(event, data) {
window.__MEDIATOR_CHANNEL__.postMessage({ event, data });
}
subscribe(event, callback) {
const handler = (e) => {
if (e.data.event === event) callback(e.data.data);
};
window.__MEDIATOR_CHANNEL__.addEventListener('message', handler);
return () => {
window.__MEDIATOR_CHANNEL__.removeEventListener('message', handler);
};
}
}
Testing Strategies
Testing the mediator pattern should focus on two aspects:
- The Mediator Itself: Verify message routing and transformation logic.
test('should route messages correctly', () => {
const mediator = new TestMediator();
const mockA = { receive: jest.fn() };
const mockB = { receive: jest.fn() };
mediator.register('A', mockA);
mediator.register('B', mockB);
mediator.send('A', 'B', 'hello');
expect(mockB.receive).toHaveBeenCalledWith('hello', 'A');
expect(mockA.receive).not.toHaveBeenCalled();
});
- Colleague Objects: Verify correct mediator usage.
test('should use mediator for communication', () => {
const mockMediator = { send: jest.fn() };
const component = new Component(mockMediator);
component.handleClick();
expect(mockMediator.send).toHaveBeenCalledWith(
'Component',
'TargetComponent',
'clickEvent'
);
});
Anti-Patterns and Misuses
While powerful, the mediator pattern can cause issues in these scenarios:
- God Object: Mediator takes on too many responsibilities.
// Anti-pattern: Mediator knows too many details
class BadMediator {
handleUserAction() {
if (this.uiState === 'login') {
// Handle 10 different login scenarios
} else if (this.uiState === 'checkout') {
// Handle 15 checkout cases
}
// 50 more condition branches...
}
}
- Over-Engineering: Using mediators for simple scenarios.
// Unnecessary complexity
const mediator = new Mediator();
mediator.register('button', {
onClick: () => mediator.notify('buttonClick')
});
mediator.register('text', {
update: (msg) => console.log(msg)
});
// Simpler direct approach is better
button.addEventListener('click', () => {
textElement.textContent = 'Clicked';
});
- Performance Bottlenecks: High-frequency messages overwhelming the mediator.
// High-frequency events may cause performance issues
window.addEventListener('mousemove', (e) => {
mediator.publish('mouseMove', e); // May trigger extensive processing
});
// Better approach: Throttle or handle directly
const throttlePublish = _.throttle(
(e) => mediator.publish('mouseMove', e),
100
);
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn