Mediator
Mediator — это поведенческий паттерн проектирования, который позволяет уменьшить связанность множества классов между собой, благодаря перемещению этих связей в один класс-посредник.
Проблема
Представьте, что вы создаете диалог для создания пользователя. Диалог состоит из различных элементов управления: текстовые поля для имени и фамилии, чекбокс "Я согласен с условиями", кнопка "Создать" и кнопка "Отмена".
Когда пользователь вводит имя и фамилию, нужно проверить, заполнены ли оба поля, и только тогда активировать кнопку "Создать". Когда пользователь снимает галочку с чекбокса, нужно деактивировать кнопку "Создать".
Если вы добавите эту логику прямо в классы элементов управления, они станут тесно связанными друг с другом, что затруднит их переиспользование.
Решение
Паттерн Mediator предлагает создать объект-посредник, который будет обрабатывать взаимодействие между компонентами системы. Вместо прямого общения компоненты будут общаться через посредника.
Структура
Mediator
└── notify(sender: Component, event: string): void
ConcreteMediator
├── components: Component[]
├── notify(sender: Component, event: string): void
└── addComponent(component: Component): void
Component
├── mediator: Mediator
├── setMediator(mediator: Mediator): void
└── notify(event: string): void
ConcreteComponent
├── mediator: Mediator
├── setMediator(mediator: Mediator): void
└── notify(event: string): void
Пример реализации
JavaScript
// Интерфейс посредника
class Mediator {
notify(sender, event) {
throw new Error('Метод notify должен быть переопределен');
}
}
// Интерфейс компонента
class Component {
constructor() {
this.mediator = null;
}
setMediator(mediator) {
this.mediator = mediator;
}
notify(event) {
if (this.mediator) {
this.mediator.notify(this, event);
}
}
}
// Конкретный посредник - диалог создания пользователя
class UserCreationDialogMediator extends Mediator {
constructor() {
super();
this.firstNameField = null;
this.lastNameField = null;
this.agreeCheckbox = null;
this.createButton = null;
this.cancelButton = null;
}
setComponents(firstNameField, lastNameField, agreeCheckbox, createButton, cancelButton) {
this.firstNameField = firstNameField;
this.lastNameField = lastNameField;
this.agreeCheckbox = agreeCheckbox;
this.createButton = createButton;
this.cancelButton = cancelButton;
}
notify(sender, event) {
switch (event) {
case 'firstNameChanged':
this.checkFormValidity();
break;
case 'lastNameChanged':
this.checkFormValidity();
break;
case 'agreeChanged':
this.checkFormValidity();
break;
case 'createClicked':
this.createUser();
break;
case 'cancelClicked':
this.cancelCreation();
break;
}
}
checkFormValidity() {
const firstNameValid = this.firstNameField && this.firstNameField.getValue().trim() !== '';
const lastNameValid = this.lastNameField && this.lastNameField.getValue().trim() !== '';
const agreeChecked = this.agreeCheckbox && this.agreeCheckbox.isChecked();
const isValid = firstNameValid && lastNameValid && agreeChecked;
if (this.createButton) {
this.createButton.setEnabled(isValid);
}
console.log(`Форма ${isValid ? 'валидна' : 'невалидна'}`);
}
createUser() {
const firstName = this.firstNameField.getValue();
const lastName = this.lastNameField.getValue();
console.log(`Создание пользователя: ${firstName} ${lastName}`);
console.log('Пользователь создан успешно!');
// Очищаем форму
this.firstNameField.setValue('');
this.lastNameField.setValue('');
this.agreeCheckbox.setChecked(false);
this.createButton.setEnabled(false);
}
cancelCreation() {
console.log('Отмена создания пользователя');
// Очищаем форму
this.firstNameField.setValue('');
this.lastNameField.setValue('');
this.agreeCheckbox.setChecked(false);
this.createButton.setEnabled(false);
}
}
// Конкретные компоненты
class TextField extends Component {
constructor(name) {
super();
this.name = name;
this.value = '';
}
setValue(value) {
this.value = value;
console.log(`${this.name}: значение изменено на "${value}"`);
this.notify('valueChanged');
}
getValue() {
return this.value;
}
// Симуляция ввода пользователя
simulateInput(value) {
this.setValue(value);
this.notify(`${this.name.toLowerCase()}Changed`);
}
}
class Checkbox extends Component {
constructor(name) {
super();
this.name = name;
this.checked = false;
}
setChecked(checked) {
this.checked = checked;
console.log(`${this.name}: ${checked ? 'отмечен' : 'снят'}`);
this.notify('checkedChanged');
}
isChecked() {
return this.checked;
}
// Симуляция клика пользователя
simulateClick() {
this.setChecked(!this.checked);
this.notify('agreeChanged');
}
}
class Button extends Component {
constructor(name) {
super();
this.name = name;
this.enabled = false;
}
setEnabled(enabled) {
this.enabled = enabled;
console.log(`${this.name}: ${enabled ? 'активирована' : 'деактивирована'}`);
}
isEnabled() {
return this.enabled;
}
// Симуляция клика пользователя
simulateClick() {
if (this.enabled) {
console.log(`${this.name}: клик`);
this.notify(`${this.name.toLowerCase()}Clicked`);
} else {
console.log(`${this.name}: клик заблокирован (кнопка неактивна)`);
}
}
}
// Использование
const mediator = new UserCreationDialogMediator();
// Создаем компоненты
const firstNameField = new TextField('Имя');
const lastNameField = new TextField('Фамилия');
const agreeCheckbox = new Checkbox('Согласие');
const createButton = new Button('Создать');
const cancelButton = new Button('Отмена');
// Устанавливаем посредника для всех компонентов
firstNameField.setMediator(mediator);
lastNameField.setMediator(mediator);
agreeCheckbox.setMediator(mediator);
createButton.setMediator(mediator);
cancelButton.setMediator(mediator);
// Настраиваем посредника
mediator.setComponents(firstNameField, lastNameField, agreeCheckbox, createButton, cancelButton);
console.log('=== Тестирование диалога создания пользователя ===');
// Симулируем действия пользователя
firstNameField.simulateInput('Иван');
lastNameField.simulateInput('Петров');
agreeCheckbox.simulateClick();
console.log('\n--- Попытка создать пользователя ---');
createButton.simulateClick();
console.log('\n--- Отмена создания ---');
cancelButton.simulateClick();
console.log('\n--- Попытка создать без согласия ---');
firstNameField.simulateInput('Мария');
lastNameField.simulateInput('Сидорова');
createButton.simulateClick(); // Должна быть заблокирована
TypeScript с интерфейсами
// Интерфейс посредника
interface Mediator {
notify(sender: Component, event: string): void;
}
// Интерфейс компонента
interface Component {
setMediator(mediator: Mediator): void;
notify(event: string): void;
}
// Конкретный посредник - система управления воздушным движением
class AirTrafficControlMediator implements Mediator {
private aircrafts: Aircraft[] = [];
private runways: Runway[] = [];
addAircraft(aircraft: Aircraft): void {
this.aircrafts.push(aircraft);
aircraft.setMediator(this);
}
addRunway(runway: Runway): void {
this.runways.push(runway);
runway.setMediator(this);
}
notify(sender: Component, event: string): void {
if (sender instanceof Aircraft) {
this.handleAircraftEvent(sender, event);
} else if (sender instanceof Runway) {
this.handleRunwayEvent(sender, event);
}
}
private handleAircraftEvent(aircraft: Aircraft, event: string): void {
switch (event) {
case 'requestLanding':
this.handleLandingRequest(aircraft);
break;
case 'requestTakeoff':
this.handleTakeoffRequest(aircraft);
break;
case 'landingComplete':
this.handleLandingComplete(aircraft);
break;
case 'takeoffComplete':
this.handleTakeoffComplete(aircraft);
break;
}
}
private handleRunwayEvent(runway: Runway, event: string): void {
switch (event) {
case 'runwayAvailable':
this.handleRunwayAvailable(runway);
break;
case 'runwayOccupied':
this.handleRunwayOccupied(runway);
break;
}
}
private handleLandingRequest(aircraft: Aircraft): void {
const availableRunway = this.runways.find(runway => runway.isAvailable());
if (availableRunway) {
console.log(`Разрешение на посадку для ${aircraft.getName()} на полосу ${availableRunway.getId()}`);
availableRunway.assignAircraft(aircraft);
aircraft.receiveLandingPermission(availableRunway);
} else {
console.log(`Нет свободных полос для ${aircraft.getName()}. Ожидание...`);
aircraft.receiveWaitingInstruction();
}
}
private handleTakeoffRequest(aircraft: Aircraft): void {
const availableRunway = this.runways.find(runway => runway.isAvailable());
if (availableRunway) {
console.log(`Разрешение на взлет для ${aircraft.getName()} с полосы ${availableRunway.getId()}`);
availableRunway.assignAircraft(aircraft);
aircraft.receiveTakeoffPermission(availableRunway);
} else {
console.log(`Нет свободных полос для ${aircraft.getName()}. Ожидание...`);
aircraft.receiveWaitingInstruction();
}
}
private handleLandingComplete(aircraft: Aircraft): void {
console.log(`Посадка ${aircraft.getName()} завершена`);
// Освобождаем полосу
const runway = this.runways.find(r => r.getAssignedAircraft() === aircraft);
if (runway) {
runway.releaseAircraft();
}
}
private handleTakeoffComplete(aircraft: Aircraft): void {
console.log(`Взлет ${aircraft.getName()} завершен`);
// Освобождаем полосу
const runway = this.runways.find(r => r.getAssignedAircraft() === aircraft);
if (runway) {
runway.releaseAircraft();
}
}
private handleRunwayAvailable(runway: Runway): void {
console.log(`Полоса ${runway.getId()} свободна`);
}
private handleRunwayOccupied(runway: Runway): void {
console.log(`Полоса ${runway.getId()} занята`);
}
}
// Конкретные компоненты
class Aircraft implements Component {
private mediator: Mediator | null = null;
private name: string;
private status: string = 'flying';
constructor(name: string) {
this.name = name;
}
setMediator(mediator: Mediator): void {
this.mediator = mediator;
}
notify(event: string): void {
if (this.mediator) {
this.mediator.notify(this, event);
}
}
getName(): string {
return this.name;
}
requestLanding(): void {
console.log(`${this.name} запрашивает разрешение на посадку`);
this.notify('requestLanding');
}
requestTakeoff(): void {
console.log(`${this.name} запрашивает разрешение на взлет`);
this.notify('requestTakeoff');
}
receiveLandingPermission(runway: Runway): void {
console.log(`${this.name} получил разрешение на посадку на полосу ${runway.getId()}`);
this.status = 'landing';
// Симулируем посадку
setTimeout(() => {
this.status = 'landed';
this.notify('landingComplete');
}, 1000);
}
receiveTakeoffPermission(runway: Runway): void {
console.log(`${this.name} получил разрешение на взлет с полосы ${runway.getId()}`);
this.status = 'taking_off';
// Симулируем взлет
setTimeout(() => {
this.status = 'flying';
this.notify('takeoffComplete');
}, 1000);
}
receiveWaitingInstruction(): void {
console.log(`${this.name} получил инструкцию ожидать`);
}
}
class Runway implements Component {
private mediator: Mediator | null = null;
private id: string;
private assignedAircraft: Aircraft | null = null;
constructor(id: string) {
this.id = id;
}
setMediator(mediator: Mediator): void {
this.mediator = mediator;
}
notify(event: string): void {
if (this.mediator) {
this.mediator.notify(this, event);
}
}
getId(): string {
return this.id;
}
isAvailable(): boolean {
return this.assignedAircraft === null;
}
assignAircraft(aircraft: Aircraft): void {
this.assignedAircraft = aircraft;
console.log(`Полоса ${this.id} назначена для ${aircraft.getName()}`);
this.notify('runwayOccupied');
}
releaseAircraft(): void {
if (this.assignedAircraft) {
console.log(`Полоса ${this.id} освобождена от ${this.assignedAircraft.getName()}`);
this.assignedAircraft = null;
this.notify('runwayAvailable');
}
}
getAssignedAircraft(): Aircraft | null {
return this.assignedAircraft;
}
}
// Использование
const atc = new AirTrafficControlMediator();
// Создаем полосы
const runway1 = new Runway('RWY-01');
const runway2 = new Runway('RWY-02');
atc.addRunway(runway1);
atc.addRunway(runway2);
// Создаем самолеты
const flight101 = new Aircraft('Flight 101');
const flight202 = new Aircraft('Flight 202');
const flight303 = new Aircraft('Flight 303');
atc.addAircraft(flight101);
atc.addAircraft(flight202);
atc.addAircraft(flight303);
console.log('=== Система управления воздушным движением ===');
// Симулируем запросы на посадку и взлет
flight101.requestLanding();
flight202.requestLanding();
flight303.requestTakeoff();
Когда использовать
Используйте Mediator, когда:
- Много объектов взаимодействуют друг с другом
- Объекты тесно связаны и сложно изменять их независимо
- Нужно централизовать логику взаимодействия
- Хотите уменьшить связанность между компонентами
Преимущества
- Уменьшает связанность между компонентами
- Централизует логику взаимодействия
- Принцип единственной ответственности
- Легко добавлять новые компоненты
Недостатки
- Посредник может стать сложным и трудно поддерживаемым
- Может стать "божественным объектом"
- Увеличивает количество классов
Отличия от других паттернов
- Observer — уведомляет множество объектов об изменениях
- Facade — предоставляет упрощенный интерфейс к подсистеме
- Command — инкапсулирует запросы как объекты
Реальный пример
// Mediator для системы чата
class ChatMediator {
constructor() {
this.users = new Map();
this.rooms = new Map();
}
addUser(user) {
this.users.set(user.getId(), user);
user.setMediator(this);
}
addRoom(room) {
this.rooms.set(room.getId(), room);
room.setMediator(this);
}
notify(sender, event, data = {}) {
if (sender instanceof User) {
this.handleUserEvent(sender, event, data);
} else if (sender instanceof Room) {
this.handleRoomEvent(sender, event, data);
}
}
handleUserEvent(user, event, data) {
switch (event) {
case 'sendMessage':
this.handleMessage(user, data.message, data.roomId);
break;
case 'joinRoom':
this.handleJoinRoom(user, data.roomId);
break;
case 'leaveRoom':
this.handleLeaveRoom(user, data.roomId);
break;
}
}
handleRoomEvent(room, event, data) {
switch (event) {
case 'userJoined':
this.broadcastToRoom(room.getId(), `${data.user.getName()} присоединился к комнате`);
break;
case 'userLeft':
this.broadcastToRoom(room.getId(), `${data.user.getName()} покинул комнату`);
break;
}
}
handleMessage(user, message, roomId) {
const room = this.rooms.get(roomId);
if (room && room.hasUser(user)) {
this.broadcastToRoom(roomId, `${user.getName()}: ${message}`);
}
}
handleJoinRoom(user, roomId) {
const room = this.rooms.get(roomId);
if (room) {
room.addUser(user);
user.joinRoom(room);
}
}
handleLeaveRoom(user, roomId) {
const room = this.rooms.get(roomId);
if (room) {
room.removeUser(user);
user.leaveRoom(room);
}
}
broadcastToRoom(roomId, message) {
const room = this.rooms.get(roomId);
if (room) {
room.getUsers().forEach(user => {
user.receiveMessage(message);
});
}
}
}
// Использование
const chatMediator = new ChatMediator();
const generalRoom = new Room('general');
const techRoom = new Room('tech');
chatMediator.addRoom(generalRoom);
chatMediator.addRoom(techRoom);
const alice = new User('Alice');
const bob = new User('Bob');
const charlie = new User('Charlie');
chatMediator.addUser(alice);
chatMediator.addUser(bob);
chatMediator.addUser(charlie);
// Симулируем взаимодействие
alice.joinRoom('general');
bob.joinRoom('general');
charlie.joinRoom('tech');
alice.sendMessage('Привет всем!', 'general');
bob.sendMessage('Привет, Alice!', 'general');
charlie.sendMessage('Обсуждаем новые технологии', 'tech');
Mediator — это централизованное управление взаимодействием!