1. Mediator giải quyết vấn đề gì
Mediator Pattern hữu ích nhất khi bạn cần giảm sự phụ thuộc lẫn nhau giữa các đối tượng bằng cách tập trung xử lý tương tác vào một đối tượng trung gian. Pattern này giúp các đối tượng không cần biết về sự tồn tại của nhau, thay vào đó chúng chỉ cần giao tiếp thông qua mediator.
Các thành phần chính trong Mediator Pattern:
- Mediator: Interface định nghĩa các phương thức giao tiếp giữa các đối tượng
- ConcreteMediator: Triển khai Mediator interface và điều phối tương tác giữa các đối tượng
- Colleague: Interface cho các đối tượng tham gia tương tác
- ConcreteColleague: Triển khai Colleague interface và giao tiếp thông qua mediator
Structure
Mediator Pattern có cấu trúc như sau:
classDiagram
class Mediator {
<<interface>>
+notify(sender, event)
}
class ConcreteMediator {
-colleague1: Colleague
-colleague2: Colleague
+notify(sender, event)
}
class Colleague {
<<interface>>
#mediator: Mediator
+setMediator(mediator)
}
class ConcreteColleague1 {
+doSomething()
+reactToEvent()
}
class ConcreteColleague2 {
+doSomething()
+reactToEvent()
}
Mediator <|.. ConcreteMediator
Colleague <|.. ConcreteColleague1
Colleague <|.. ConcreteColleague2
ConcreteMediator o--> ConcreteColleague1
ConcreteMediator o--> ConcreteColleague2
ConcreteColleague1 --> Mediator
ConcreteColleague2 --> Mediator
note for Mediator "Interface định nghĩa<br>phương thức giao tiếp"
note for ConcreteMediator "Triển khai logic điều phối<br>giữa các đối tượng"
note for Colleague "Interface cho các đối tượng<br>tham gia tương tác"
note for ConcreteColleague1 "Triển khai Colleague<br>và giao tiếp qua mediator"
Các đặc điểm chính của Mediator Pattern:
- Giảm sự kết nối trực tiếp: Các đối tượng không cần biết về nhau, chỉ giao tiếp qua mediator
- Tập trung xử lý logic: Logic điều phối được tập trung vào mediator, dễ quản lý và bảo trì
- Giảm sự phụ thuộc: Các đối tượng chỉ phụ thuộc vào mediator, không phụ thuộc lẫn nhau
- Dễ dàng mở rộng: Có thể thêm đối tượng mới mà không cần thay đổi các đối tượng hiện có
2. Triển khai trong JavaScript
2.1 Ví dụ về Chat Room
// Mediator
class ChatRoom {
constructor() {
this.users = new Map();
}
register(user) {
this.users.set(user.name, user);
user.chatRoom = this;
}
send(message, from, to) {
if (to) {
// Private message
const receiver = this.users.get(to);
if (receiver) {
receiver.receive(message, from);
}
} else {
// Broadcast message
this.users.forEach((user) => {
if (user.name !== from) {
user.receive(message, from);
}
});
}
}
}
// Colleague
class User {
constructor(name) {
this.name = name;
this.chatRoom = null;
}
send(message, to = null) {
console.log(`${this.name} sends message: ${message}`);
this.chatRoom.send(message, this.name, to);
}
receive(message, from) {
console.log(`${this.name} receives message from ${from}: ${message}`);
}
}
// Usage
const chatRoom = new ChatRoom();
const john = new User("John");
const jane = new User("Jane");
const bob = new User("Bob");
chatRoom.register(john);
chatRoom.register(jane);
chatRoom.register(bob);
john.send("Hello everyone!");
jane.send("Hi John!", "John");
bob.send("Hey guys!");
2.2 Ví dụ về Air Traffic Control
// Mediator
class AirTrafficControl {
constructor() {
this.aircraft = new Map();
}
register(aircraft) {
this.aircraft.set(aircraft.id, aircraft);
aircraft.atc = this;
}
requestLanding(aircraft) {
const otherAircraft = Array.from(this.aircraft.values()).filter(
(a) => a.id !== aircraft.id
);
const isSafeToLand = otherAircraft.every(
(a) => Math.abs(a.altitude - aircraft.altitude) > 1000
);
if (isSafeToLand) {
aircraft.land();
this.notifyOthers(`Aircraft ${aircraft.id} is landing`, aircraft.id);
} else {
aircraft.wait();
this.notifyOthers(
`Aircraft ${aircraft.id} is waiting to land`,
aircraft.id
);
}
}
notifyOthers(message, excludeId) {
this.aircraft.forEach((aircraft, id) => {
if (id !== excludeId) {
aircraft.receive(message);
}
});
}
}
// Colleague
class Aircraft {
constructor(id) {
this.id = id;
this.altitude = Math.floor(Math.random() * 10000);
this.atc = null;
}
requestLanding() {
console.log(`Aircraft ${this.id} requesting landing permission`);
this.atc.requestLanding(this);
}
land() {
console.log(`Aircraft ${this.id} landing`);
this.altitude = 0;
}
wait() {
console.log(`Aircraft ${this.id} holding position`);
}
receive(message) {
console.log(`Aircraft ${this.id} receives message: ${message}`);
}
}
// Usage
const atc = new AirTrafficControl();
const flight1 = new Aircraft("AA123");
const flight2 = new Aircraft("BA456");
const flight3 = new Aircraft("UA789");
atc.register(flight1);
atc.register(flight2);
atc.register(flight3);
flight1.requestLanding();
flight2.requestLanding();
flight3.requestLanding();
3. Triển khai trong TypeScript
3.1 Ví dụ về Event Bus
// Event types
type EventType = string;
type EventHandler = (data: any) => void;
// Mediator interface
interface EventBus {
subscribe(event: EventType, handler: EventHandler): void;
unsubscribe(event: EventType, handler: EventHandler): void;
publish(event: EventType, data: any): void;
}
// Concrete Mediator
class ApplicationEventBus implements EventBus {
private handlers: Map<EventType, Set<EventHandler>>;
constructor() {
this.handlers = new Map();
}
subscribe(event: EventType, handler: EventHandler): void {
if (!this.handlers.has(event)) {
this.handlers.set(event, new Set());
}
this.handlers.get(event)!.add(handler);
}
unsubscribe(event: EventType, handler: EventHandler): void {
const eventHandlers = this.handlers.get(event);
if (eventHandlers) {
eventHandlers.delete(handler);
}
}
publish(event: EventType, data: any): void {
const eventHandlers = this.handlers.get(event);
if (eventHandlers) {
eventHandlers.forEach((handler) => handler(data));
}
}
}
// Components using the Event Bus
class UserService {
constructor(private eventBus: EventBus) {}
login(username: string, password: string): void {
// Simulate login
console.log(`User ${username} logging in...`);
this.eventBus.publish("userLoggedIn", { username });
}
logout(username: string): void {
console.log(`User ${username} logging out...`);
this.eventBus.publish("userLoggedOut", { username });
}
}
class NotificationService {
constructor(private eventBus: EventBus) {
this.setupSubscriptions();
}
private setupSubscriptions(): void {
this.eventBus.subscribe("userLoggedIn", this.handleUserLogin.bind(this));
this.eventBus.subscribe("userLoggedOut", this.handleUserLogout.bind(this));
}
private handleUserLogin(data: { username: string }): void {
console.log(`Notification: Welcome back, ${data.username}!`);
}
private handleUserLogout(data: { username: string }): void {
console.log(`Notification: Goodbye, ${data.username}!`);
}
}
class AuditService {
constructor(private eventBus: EventBus) {
this.setupSubscriptions();
}
private setupSubscriptions(): void {
this.eventBus.subscribe("userLoggedIn", this.logActivity.bind(this));
this.eventBus.subscribe("userLoggedOut", this.logActivity.bind(this));
}
private logActivity(data: { username: string }): void {
const timestamp = new Date().toISOString();
console.log(
`Audit Log [${timestamp}]: User activity recorded for ${data.username}`
);
}
}
// Usage
const eventBus = new ApplicationEventBus();
const userService = new UserService(eventBus);
const notificationService = new NotificationService(eventBus);
const auditService = new AuditService(eventBus);
userService.login("john.doe", "password123");
userService.logout("john.doe");
3.2 Ví dụ về Form Validation
// Mediator interface
interface FormMediator {
notify(sender: FormComponent, event: string): void;
validateForm(): boolean;
}
// Base component
abstract class FormComponent {
protected mediator: FormMediator;
protected value: string = "";
constructor(mediator: FormMediator) {
this.mediator = mediator;
}
setValue(value: string): void {
this.value = value;
this.mediator.notify(this, "valueChanged");
}
getValue(): string {
return this.value;
}
abstract validate(): boolean;
}
// Concrete components
class UsernameField extends FormComponent {
validate(): boolean {
return this.value.length >= 3;
}
}
class EmailField extends FormComponent {
validate(): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.value);
}
}
class PasswordField extends FormComponent {
validate(): boolean {
return this.value.length >= 8;
}
}
class ConfirmPasswordField extends FormComponent {
validate(): boolean {
const passwordField = (
this.mediator as RegistrationFormMediator
).getPasswordField();
return this.value === passwordField.getValue();
}
}
// Concrete mediator
class RegistrationFormMediator implements FormMediator {
private username: UsernameField;
private email: EmailField;
private password: PasswordField;
private confirmPassword: ConfirmPasswordField;
private submitButton: HTMLButtonElement;
constructor() {
this.username = new UsernameField(this);
this.email = new EmailField(this);
this.password = new PasswordField(this);
this.confirmPassword = new ConfirmPasswordField(this);
this.submitButton = document.createElement("button");
}
notify(sender: FormComponent, event: string): void {
if (event === "valueChanged") {
this.validateForm();
}
}
validateForm(): boolean {
const isValid =
this.username.validate() &&
this.email.validate() &&
this.password.validate() &&
this.confirmPassword.validate();
this.submitButton.disabled = !isValid;
return isValid;
}
getPasswordField(): PasswordField {
return this.password;
}
setFormValues(data: {
username: string;
email: string;
password: string;
confirmPassword: string;
}): void {
this.username.setValue(data.username);
this.email.setValue(data.email);
this.password.setValue(data.password);
this.confirmPassword.setValue(data.confirmPassword);
}
}
// Usage
const formMediator = new RegistrationFormMediator();
formMediator.setFormValues({
username: "john_doe",
email: "[email protected]",
password: "password123",
confirmPassword: "password123",
});
console.log("Form is valid:", formMediator.validateForm());
4. Trường hợp sử dụng thực tế
4.1 Quản lý Giao tiếp trong Game
interface GameMediator {
notify(sender: GameObject, event: GameEvent): void;
}
interface GameEvent {
type: string;
data: any;
}
abstract class GameObject {
protected mediator: GameMediator;
protected position: { x: number; y: number };
constructor(mediator: GameMediator, x: number, y: number) {
this.mediator = mediator;
this.position = { x, y };
}
abstract update(): void;
}
class Player extends GameObject {
private health: number = 100;
update(): void {
// Player logic
}
takeDamage(amount: number): void {
this.health -= amount;
this.mediator.notify(this, {
type: "playerDamaged",
data: { health: this.health },
});
if (this.health <= 0) {
this.mediator.notify(this, {
type: "playerDied",
data: null,
});
}
}
}
class Enemy extends GameObject {
private isActive: boolean = true;
update(): void {
if (this.isActive) {
// Check collision with player
this.mediator.notify(this, {
type: "enemyAttack",
data: { position: this.position },
});
}
}
destroy(): void {
this.isActive = false;
this.mediator.notify(this, {
type: "enemyDestroyed",
data: { position: this.position },
});
}
}
class GameUI {
updateHealth(health: number): void {
console.log(`Health updated: ${health}`);
}
showGameOver(): void {
console.log("Game Over!");
}
}
class ScoreSystem {
private score: number = 0;
addScore(points: number): void {
this.score += points;
console.log(`Score: ${this.score}`);
}
}
class GameDirector implements GameMediator {
private player: Player;
private enemies: Enemy[] = [];
private ui: GameUI;
private scoreSystem: ScoreSystem;
constructor() {
this.ui = new GameUI();
this.scoreSystem = new ScoreSystem();
this.player = new Player(this, 0, 0);
// Create some enemies
for (let i = 0; i < 3; i++) {
this.enemies.push(new Enemy(this, i * 100, i * 100));
}
}
notify(sender: GameObject, event: GameEvent): void {
switch (event.type) {
case "playerDamaged":
this.ui.updateHealth(event.data.health);
break;
case "playerDied":
this.ui.showGameOver();
break;
case "enemyAttack":
this.handleEnemyAttack(sender as Enemy, event.data);
break;
case "enemyDestroyed":
this.scoreSystem.addScore(100);
break;
}
}
private handleEnemyAttack(enemy: Enemy, data: any): void {
// Check if player is in range
const distance = Math.sqrt(
Math.pow(this.player.position.x - data.position.x, 2) +
Math.pow(this.player.position.y - data.position.y, 2)
);
if (distance < 50) {
this.player.takeDamage(10);
}
}
update(): void {
this.player.update();
this.enemies.forEach((enemy) => enemy.update());
}
}
// Usage
const game = new GameDirector();
// Simulate game loop
game.update();
// Simulate enemy being destroyed
const enemy = game["enemies"][0];
enemy.destroy();
4.2 Quản lý Workflow
interface WorkflowMediator {
notify(sender: WorkflowComponent, event: string, data?: any): void;
}
abstract class WorkflowComponent {
constructor(protected mediator: WorkflowMediator) {}
abstract execute(): void;
}
class DataValidator extends WorkflowComponent {
execute(): void {
console.log("Validating data...");
// Simulate validation
const isValid = Math.random() > 0.1;
if (isValid) {
this.mediator.notify(this, "validationComplete");
} else {
this.mediator.notify(this, "validationFailed", {
error: "Invalid data format",
});
}
}
}
class DataTransformer extends WorkflowComponent {
execute(): void {
console.log("Transforming data...");
// Simulate transformation
const success = Math.random() > 0.1;
if (success) {
this.mediator.notify(this, "transformationComplete");
} else {
this.mediator.notify(this, "transformationFailed", {
error: "Transformation error",
});
}
}
}
class DataPersistence extends WorkflowComponent {
execute(): void {
console.log("Saving data...");
// Simulate saving
const success = Math.random() > 0.1;
if (success) {
this.mediator.notify(this, "saveComplete");
} else {
this.mediator.notify(this, "saveFailed", {
error: "Database error",
});
}
}
}
class NotificationService extends WorkflowComponent {
execute(): void {
// This component only handles notifications
}
sendNotification(message: string): void {
console.log(`Notification: ${message}`);
}
}
class WorkflowOrchestrator implements WorkflowMediator {
private validator: DataValidator;
private transformer: DataTransformer;
private persistence: DataPersistence;
private notificationService: NotificationService;
constructor() {
this.validator = new DataValidator(this);
this.transformer = new DataTransformer(this);
this.persistence = new DataPersistence(this);
this.notificationService = new NotificationService(this);
}
notify(sender: WorkflowComponent, event: string, data?: any): void {
switch (event) {
case "validationComplete":
this.notificationService.sendNotification("Validation successful");
this.transformer.execute();
break;
case "validationFailed":
this.notificationService.sendNotification(
`Validation failed: ${data.error}`
);
break;
case "transformationComplete":
this.notificationService.sendNotification("Transformation successful");
this.persistence.execute();
break;
case "transformationFailed":
this.notificationService.sendNotification(
`Transformation failed: ${data.error}`
);
break;
case "saveComplete":
this.notificationService.sendNotification("Save successful");
break;
case "saveFailed":
this.notificationService.sendNotification(`Save failed: ${data.error}`);
break;
}
}
startWorkflow(): void {
this.validator.execute();
}
}
// Usage
const workflow = new WorkflowOrchestrator();
workflow.startWorkflow();
5. Khi mediator biến thành god object
Mediator giúp các component không gọi nhau trực tiếp, nhưng đổi lại mọi tương tác có xu hướng chảy về một chỗ. Nếu không kiểm soát, mediator rất dễ thành “god object”: biết quá nhiều event, giữ quá nhiều state, và chứa luôn business rule của cả hệ thống.
Dấu hiệu nên tách mediator:
notify()có mộtswitchdài vài chục case.- Một thay đổi nhỏ ở component A luôn phải sửa mediator, component B, rồi test lại component C.
- Mediator vừa điều phối UI, vừa gọi API, vừa ghi analytics, vừa quyết định business rule.
- Không test được colleague độc lập vì tất cả phụ thuộc vào mediator thật.
Cách giảm rủi ro là chia mediator theo bounded context nhỏ. Form mediator chỉ điều phối form. Checkout mediator chỉ điều phối checkout. Event bus cấp app chỉ nên route event, không nên chứa business flow.
6. Mediator khác gì Pub/Sub?
Hai pattern này nhìn giống nhau vì đều tránh gọi trực tiếp. Khác biệt thực dụng nằm ở mức độ điều phối:
| Câu hỏi | Mediator | Pub/Sub |
|---|---|---|
| Ai biết flow tổng thể? | Mediator biết | Không ai biết đầy đủ |
| Có thứ tự bước rõ không? | Có | Thường không |
| Dễ debug luồng nghiệp vụ không? | Dễ hơn | Khó hơn nếu event nhiều |
| Phù hợp với | Form, wizard, workflow, game loop | Notification, analytics, broadcast event |
Nếu bạn cần một người điều phối có chủ ý, dùng Mediator. Nếu bạn chỉ cần phát tín hiệu cho nhiều subscriber độc lập, Pub/Sub thường nhẹ hơn.
7. Checklist test mediator
- Test mỗi event quan trọng đi qua mediator và tạo đúng side effect.
- Test colleague bằng fake mediator để không kéo cả hệ thống vào unit test.
- Log event name và sender khi mediator điều phối workflow production.
- Đặt tên event theo domain, ví dụ
payment_authorized, không đặt kiểustep2_done. - Không để mediator giữ state lâu nếu state đó thuộc về aggregate hoặc store khác.
8. Trade-off cần nhớ
8.1 Ưu điểm
- Giảm sự phụ thuộc: Các đối tượng không cần biết về nhau, chỉ cần biết về mediator.
- Dễ bảo trì: Tập trung logic tương tác vào một nơi, dễ dàng thay đổi và bảo trì.
- Tái sử dụng: Các đối tượng có thể được tái sử dụng trong các ngữ cảnh khác nhau.
8.2 Nhược điểm
- Phức tạp hóa: Mediator có thể trở nên phức tạp khi số lượng đối tượng tăng lên.
- Single Point of Failure: Nếu mediator gặp vấn đề, toàn bộ hệ thống có thể bị ảnh hưởng.
- Hiệu năng: Có thể tạo ra overhead khi tất cả tương tác phải thông qua mediator.
Điểm đáng giữ của Mediator là gom một flow phức tạp về một nơi có thể đọc và test. Nếu nơi đó bắt đầu biết mọi thứ về cả ứng dụng, pattern đã đổi tên thành nợ kỹ thuật.