Перейти к содержанию

Mediator (Посредник)

Категория: паттерн поведения.

Проблема

Когда объекты общаются друг с другом напрямую, граф связей между ними быстро превращается в "все со всеми": чекбокс знает про кнопку, кнопка знает про поле ввода, поле ввода знает про чекбокс и так далее. Такой код сложно менять - изменение одного компонента требует понимания всех, с кем он напрямую связан, а количество связей растёт нелинейно с числом компонентов.

Решение

  1. Вводится отдельный объект - посредник, который знает обо всех участвующих компонентах.
  2. Компоненты перестают ссылаться друг на друга напрямую - вместо этого они сообщают посреднику о своих изменениях (mediator.Notify(this, "событие")).
  3. Вся логика согласованного взаимодействия компонентов ("если чекбокс не отмечен - кнопка недоступна") переносится в посредника.
  4. В результате получается связность "звездой": каждый компонент связан только с посредником, а не друг с другом.

Структура

  • Mediator - интерфейс посредника с методом уведомления о событиях.
  • ConcreteMediator - хранит ссылки на все компоненты и реализует логику их согласования.
  • Colleague (компоненты) - хранят ссылку только на посредника, уведомляют его о своих изменениях и получают от него команды.

Явный и неявный посредник

  • Явный посредник - отдельный класс, как в примере выше (CheckoutDialog). Логика взаимодействия сосредоточена в одном месте и её легко найти.
  • Неявный посредник - роль медиатора распределена между самими компонентами (например, через события/подписки без единого координирующего класса). Такой вариант проще для маленького числа связей, но с ростом системы логика координации "размазывается" и её сложнее отследить.

Когда применять

  • Много компонентов с сильными, запутанными взаимными связями, где изменение одного требует правки нескольких других.
  • Логика взаимодействия компонентов регулярно меняется, и хочется менять её в одном месте, не трогая сами компоненты.
  • Компоненты должны быть переиспользуемы в разных контекстах (разные экраны, разные диалоги) с разной логикой согласования между ними.

Плюсы

  • Уменьшает связанность между компонентами - каждый знает только про посредника.
  • Логика взаимодействия сосредоточена в одном месте, что упрощает её изменение и тестирование.
  • Компоненты становится проще переиспользовать отдельно друг от друга.

Минусы

  • Посредник рискует превратиться в "божественный объект" (God Object), который знает слишком много и становится узким местом при сопровождении.
  • Добавляет косвенность: чтобы понять, что произойдёт при изменении одного компонента, нужно смотреть в код посредника, а не в код самого компонента.

Когда третий лишний

Не любое взаимодействие двух объектов нуждается в посреднике. Если связь между двумя конкретными классами простая, стабильная и не разрастается, введение отдельного медиатора добавляет уровень косвенности без реальной выгоды - в таком случае прямая связь или обычное событие/делегат проще и понятнее.

Пример в .NET Framework / BCL

  • Классы-контроллеры/координаторы в UI-фреймворках (WPF ViewModel, который координирует несколько Command и свойств, не позволяя элементам управления знать друг о друге).
  • Библиотека MediatR (сторонняя, но фактически имя говорит само за себя) - реализует паттерн Mediator для развязывания отправителей и обработчиков запросов/событий в приложении.
  • Оркестраторы бизнес-процессов, координирующие вызовы нескольких сервисов, которые сами ничего не знают друг о друге.

Пример реализации на C#

Mediator.cs
using System;
using System.Collections.Generic;

namespace DesignPatterns.Behavioral.Mediator
{
    // Интерфейс посредника - через него компоненты общаются друг с другом,
    // не зная напрямую друг о друге.
    public interface IDialogMediator
    {
        void Notify(object sender, string @event);
    }

    // Компоненты диалога. Каждый знает только о посреднике, а не о других компонентах.
    public sealed class CheckoutButton
    {
        private readonly IDialogMediator _mediator;
        public bool IsEnabled { get; set; }

        public CheckoutButton(IDialogMediator mediator) => _mediator = mediator;

        public void Click()
        {
            if (!IsEnabled)
            {
                Console.WriteLine("Кнопка оформления заказа недоступна");
                return;
            }

            Console.WriteLine("Оформляем заказ...");
        }
    }

    public sealed class AgreementCheckbox
    {
        private readonly IDialogMediator _mediator;
        private bool _isChecked;

        public bool IsChecked
        {
            get => _isChecked;
            set
            {
                _isChecked = value;
                _mediator.Notify(this, nameof(IsChecked));
            }
        }

        public AgreementCheckbox(IDialogMediator mediator) => _mediator = mediator;
    }

    public sealed class QuantityInput
    {
        private readonly IDialogMediator _mediator;
        private int _quantity;

        public int Quantity
        {
            get => _quantity;
            set
            {
                _quantity = value;
                _mediator.Notify(this, nameof(Quantity));
            }
        }

        public QuantityInput(IDialogMediator mediator) => _mediator = mediator;
    }

    // Конкретный посредник знает обо всех компонентах диалога и содержит логику
    // их согласованного взаимодействия. Сами компоненты об этой логике не знают.
    public sealed class CheckoutDialog : IDialogMediator
    {
        private readonly AgreementCheckbox _agreement;
        private readonly QuantityInput _quantity;
        private readonly CheckoutButton _checkoutButton;

        public CheckoutDialog()
        {
            _agreement = new AgreementCheckbox(this);
            _quantity = new QuantityInput(this);
            _checkoutButton = new CheckoutButton(this);
        }

        public AgreementCheckbox Agreement => _agreement;
        public QuantityInput Quantity => _quantity;
        public CheckoutButton CheckoutButton => _checkoutButton;

        public void Notify(object sender, string @event)
        {
            // Здесь сосредоточена вся логика согласования компонентов между собой
            _checkoutButton.IsEnabled = _agreement.IsChecked && _quantity.Quantity > 0;
            Console.WriteLine($"[Mediator] Состояние обновлено: кнопка доступна = {_checkoutButton.IsEnabled}");
        }
    }

    public static class Demo
    {
        public static void Run()
        {
            var dialog = new CheckoutDialog();

            dialog.CheckoutButton.Click(); // недоступна

            dialog.Quantity.Quantity = 2;
            dialog.CheckoutButton.Click(); // всё ещё недоступна - не принято соглашение

            dialog.Agreement.IsChecked = true;
            dialog.CheckoutButton.Click(); // теперь доступна
        }
    }
}

Открыть Mediator.cs отдельно Скачать Mediator.cs