Давайте рассмотрим паттерн проектирования Посредник C# — Mediator C#, для чего он нужен и какие проблемы он решает. Где можно применять данный шаблон, а где это будет излишним.
Идея паттерна Посредник (Mediator)
Паттерн (шаблон) проектирования — это продуманный способ построения исходного кода программы для решения часто возникающих в повседневном программировании проблем проектирования. Иными словами, это уже придуманное решения, для типичной задачи. При этом паттерн не готовое решение, а просто алгоритм действий, который должен привести к желаемому результату. Давайте рассмотрим один из наиболее часто используемых поведенческих паттернов — Посредник (Mediator).
Как я уже писал ранее, существует три вида паттернов проектирования:
- Порождающие паттерны позволяют возможность выполнять инициализацию объектов наиболее удобным и оптимальным способом.
- Структурные паттерны описывают взаимоотношения между различными классами или объектами, позволяя им совместно реализовывать поставленную задачу.
- Поведенческие паттерны позволяют грамотно организовать связь между сущностями для оптимизации и упрощения их взаимодействия.
Посредник (Mediator) — это поведенческий паттерн, который позволяет организовать работу множества слабо связанных объектов без непосредственного общения между ними. То есть, Посредник выступает промежуточным звеном между объектами, принимая и перенаправляя сообщения.
Архитектура паттерна проектирования Посредник

- Mediator — определяет интерфейс посредника.
- ConcreteMediator — конкретная реализация посредника.
- Collegue — базовый интерфейс общающихся классов.
- ConcreteCollegue1, ConcreteCollegue2 — классы одного уровня абстракции, которые взаимодействуют друг
с другом косвенным образом через посредника.
Логика работы Посредника
Рассмотрим основную логику работы паттерна Посредник. Давайте смоделируем процесс работы диспетчера полетов в аэропорту. Пилоты взлетающих или идущих на посадку самолетов в районе аэропорта общаются с диспетчером вместо непосредственного общения друг с другом. Диспетчер определяет, кто и в каком порядке будет садиться или взлетать. Данный механизм взаимодействия очень точно описывает логику работы паттерна Посредник.
Реализация паттерна Посредник (Mediator) на языке C#
Для начала создадим базовый интерфейс для самолета. В обычном случае я бы организовал это через интерфейс IAircraft, но так как это просто тестовый пример и мне не хочется дублировать код я организовал абстрактный класс, в котором реализовал всю логику, а конкретные реализации моделей самолетов не содержат практически никакой логики, а служат только для демонстрации.
using System; using System.Threading; namespace Patterns.Mediator { /// <summary> /// Базовый класс самолета. /// </summary> public abstract class AircraftBase { /// <summary> /// Поток, в котором происходит процесс полета. /// </summary> protected Thread _flyThread; /// <summary> /// Состояние самолета. /// </summary> protected AircraftState _state; /// <summary> /// Максимальное количество топлива. /// </summary> protected int _maxFuel = 1000; /// <summary> /// Имя самолета. /// </summary> public string Name { get; protected set; } /// <summary> /// Топливо самолета. /// </summary> public int Fuel { get; protected set; } /// <summary> /// Расход топлива. /// </summary> public int Consumption { get; protected set; } /// <summary> /// Скорость полета. /// </summary> public int Speed { get; protected set; } /// <summary> /// Состояние самолета. /// </summary> public AircraftState State { get { return _state; } protected set { _state = value; StateChanged?.Invoke(this, value); } } /// <summary> /// Состояние самолета изменено. /// </summary> public event EventHandler<AircraftState> StateChanged; /// <summary> /// Самолет хочет выполнить посадку. /// </summary> public event EventHandler<int> GoingToLand; /// <summary> /// Создать новый экземпляр самолета. /// </summary> /// <param name="name">Имя самолета.</param> public AircraftBase(string name) { if (string.IsNullOrEmpty(name)) { throw new ArgumentNullException(nameof(name)); } Name = name; State = AircraftState.Sleep; } //// <summary> /// Начать полет самолета. /// </summary> /// <param name="distance">Дистанция, которую должен преодолеть самолет.</param> /// <param name="land">Взлетная полоса аэропорта.</param> public void Start(int distance, Land land) { // if (!GoTakeOff(land)) { return; } _flyThread = new Thread(Fly); _flyThread.Start(distance); } /// <summary> /// Разрешить посадку самолету. /// </summary> /// <param name="land">Посадочная полоса аэропорта.</param> public void Land(Land land) { // Всегда есть шанс, что самолет разобьется... if (Dead()) { return; } // Самолет приземлился и занял полосу. land.Free = false; Thread.Sleep(5000); State = AircraftState.Land; // Самолет уехал в ангар. Полет полностью завершен. Thread.Sleep(5000); State = AircraftState.Sleep; land.Free = true; _flyThread.Abort(); } /// <summary> /// Выполняется полет самолета. /// </summary> /// <param name="distance">Дистанция полета.</param> protected void Fly(object distance) { // Самолет летит по маршруту. var dist = (int)distance; while (dist > 0) { // Уменьшается дистанция и запас топлива Fuel -= Consumption; dist -= Speed; Thread.Sleep(1000); if (Fuel <= 0) { State = AircraftState.Dead; return; } } // Самолет достиг конечной точки и ожидает посадки. WaitLanding(); } /// <summary> /// Самолет ожидает посадки. /// </summary> protected void WaitLanding() { // Самолет готов садиться. State = AircraftState.GoToLand; // До тех пор пока разрешение на посадку не получено // ожидаем посадки в воздухе while (State == AircraftState.GoToLand) { GoingToLand?.Invoke(this, Fuel); Fuel -= Consumption; Thread.Sleep(1000); } } /// <summary> /// Определение случилась ли катастрофа с самолетом. /// </summary> /// <returns>Самолет разбился.</returns> protected bool Dead() { // Используем два генератора случайных чисел с разными шумом, // чтобы была вероятность выпадения одинаковых чисел. var random1 = new Random(Convert.ToInt32(DateTime.Now.Ticks % int.MaxValue)); var random2 = new Random(DateTime.Now.Millisecond); // 1000 просто магическое число, определяющее вероятность крушения самолета. var dice1 = random1.Next(0, 1000); var dice2 = random2.Next(0, 1000); if (dice1 == dice2) { State = AircraftState.Dead; return true; } return false; } /// <summary> /// Отправляем самолет на взлет. /// </summary> /// <param name="land">Взлетная полоса для самолета.</param> /// <returns>Разбился ли самолет при взлете.</returns> protected bool GoTakeOff(Land land) { // Самолет готовится ко взлету и взлетает. land.Free = false; Fuel = _maxFuel; State = AircraftState.GoToFly; Thread.Sleep(10000); if (Dead()) { return false; } // Самолет взлетел. State = AircraftState.Fly; land.Free = true; return true; } /// <summary> /// Приведение объекта к строке. /// </summary> /// <returns>Название самолета.</returns> public override string ToString() { return Name; } } }
namespace Patterns.Mediator { /// <summary> /// Самолет модели Airbus A320. /// </summary> public class AirbusA320 : AircraftBase { /// <summary> /// Создать экземпляр самолета Airbus A320. /// </summary> /// <param name="name">Имя самолета.</param> public AirbusA320(string name) : base(name) { _maxFuel = 14400; Fuel = _maxFuel; Consumption = 3; Speed = 811; } } }
namespace Patterns.Mediator { /// <summary> /// Самолет модели Boeing 737. /// </summary> public class Boeing737 : AircraftBase { /// <summary> /// Создать экземпляр самолета Boeing 737. /// </summary> /// <param name="name">Имя самолета.</param> public Boeing737(string name) : base(name) { _maxFuel = 13399; Fuel = _maxFuel; Consumption = 2; Speed = 817; } } }
Для удобства работы с кодом добавим перечисление, показывающее ткущее состояние самолета. Подробнее про работу с перечислениями можно прочитать в статье Отображение значения Enum в C# на русском.
using System; using System.ComponentModel; using System.Reflection; namespace Patterns.Mediator { /// <summary> /// Состояние самолета. /// </summary> public enum AircraftState : int { /// <summary> /// Находится в ангаре. /// </summary> [Description("Находится в ангаре")] Sleep = 0, /// <summary> /// Взлетает. /// </summary> [Description("Взлетает")] GoToFly = 1, /// <summary> /// Находится в палёте. /// </summary> [Description("Находится в палёте")] Fly = 2, /// <summary> /// Приземляется. /// </summary> [Description("Приземляется")] GoToLand = 3, /// <summary> /// Приземлился. /// </summary> [Description("Приземлился")] Land = 4, /// <summary> /// Разбился. /// </summary> [Description("Разбился")] Dead = 5 } public static class EnumHelper { /// <summary> /// Приведение значения перечисления в удобочитаемый формат. /// </summary> /// <remarks> /// Для корректной работы необходимо использовать атрибут [Description("Name")] для каждого элемента перечисления. /// </remarks> /// <param name="enumElement">Элемент перечисления</param> /// <returns>Название элемента</returns> public static string GetDescription(this Enum enumElement) { Type type = enumElement.GetType(); MemberInfo[] memInfo = type.GetMember(enumElement.ToString()); if (memInfo != null && memInfo.Length > 0) { object[] attrs = memInfo[0].GetCustomAttributes(typeof(DescriptionAttribute), false); if (attrs != null && attrs.Length > 0) return ((DescriptionAttribute)attrs[0]).Description; } return enumElement.ToString(); } } }
Кроме того для работы аэродрома необходимы взлетно-посадочные полосы. Создадим для них класс.
using System; namespace Patterns.Mediator { /// <summary> /// Взлетно-посадочная полоса. /// </summary> public class Land { /// <summary> /// Состояние полосы. /// </summary> private bool _free; /// <summary> /// Номер полосы. /// </summary> public int Number { get; private set; } /// <summary> /// Является ли полоса свободной. /// </summary> public bool Free { get { return _free; } set { _free = value; StateChanged?.Invoke(this, value); } } /// <summary> /// Событие изменения состояния занятости полосы. /// </summary> public event EventHandler<bool> StateChanged; /// <summary> /// Создать экземпляр взлетно-посадочной полосы. /// </summary> /// <param name="number">Номер полосы</param> public Land(int number) { Number = number; Free = true; } /// <summary> /// Приведение объекта к строке. /// </summary> /// <returns>Номер полосы.</returns> public override string ToString() { return Number.ToString(); } } }
Теперь самое важное. Диспетчер будет выступать посредником между самолетами и взлетно-посадочными полосами, выполняя поиск свободных полос и самолетов для взлета и посадки. То есть, не сам каждый самолет ищет свободную полосу, а он обращается к диспетчеру (посреднику) с запросом на посадку и ожидает разрешения.
using System; using System.Collections.Generic; using System.Linq; namespace Patterns.Mediator { /// <summary> /// Диспетчер аэродрома. /// </summary> public class Dispatcher { /// <summary> /// Список функционирующих самолетов. /// </summary> private List<AircraftBase> _aircrafts = null; /// <summary> /// Список взлетно-посадочных полос аэродрома. /// </summary> private List<Land> _land = null; /// <summary> /// Самолеты. /// </summary> public IReadOnlyList<AircraftBase> Aircrafts => _aircrafts; /// <summary> /// Взлетно-посадочные полосы. /// </summary> public IReadOnlyList<Land> Lands => _land; /// <summary> /// Создать экземпляр диспетчера. /// </summary> /// <param name="aircrafts">Список самолетов.</param> /// <param name="lands">Список взлетно-посадочных полос.</param> public Dispatcher(List<AircraftBase> aircrafts, List<Land> lands) { _aircrafts = aircrafts ?? throw new ArgumentNullException(nameof(aircrafts)); _land = lands ?? throw new ArgumentNullException(nameof(lands)); // Диспетчер подписывается на событие запроса на посадку самолета. _aircrafts.ForEach(a => a.GoingToLand += RequestLanding); } /// <summary> /// Отправить самолет в полет на необходимую дистанцию /// </summary> /// <param name="distance">Дистанция полета</param> /// <returns>Сообщение диспетчера.</returns> public string Send(int distance) { if(distance < 0) { throw new ArgumentException("Дистанция полета не может быть меньше нуля.", nameof(distance)); } // Ищем любой свободный самолет. var freeAircraft = _aircrafts.FirstOrDefault(a => a.State == AircraftState.Sleep); // Ищем любую свободную дорожку. var freeLand = _land.FirstOrDefault(l => l.Free); // Если дорожка и самолет найдены отправляем самолет в полет. if (freeAircraft != null && freeLand != null) { freeAircraft.Start(distance, freeLand); return $"Самолет {freeAircraft} вылетел с {freeLand} дорожки на расстояние {distance}"; } // Иначе сообщаем о невозможности полета. else { return $"Недостаточно ресурсов. Самолет {freeAircraft}, дорожка {freeLand}"; } } /// <summary> /// Обработчик события запроса на приземление. /// </summary> /// <param name="sender">Самолет, запрашивающий приземления.</param> /// <param name="e">Количество оставшегося топлива.</param> private void RequestLanding(object sender, int e) { // Идем любую свободную дорожку. var freeLand = _land.FirstOrDefault(l => l.Free == true); // Если дорожка найдена, отправляем самолет на посадку на эту дорожку. if (freeLand != null) { ((AircraftBase)sender).Land(freeLand); } } } }
Теперь нам остается только вызвать работу наших классов.
using System; using System.Collections.Generic; using System.Threading; namespace Patterns.Mediator { class Program { static void Main(string[] args) { // Создадим самолеты и взлетно-посадочные полосы. var aircrafts = new List<Aircraft>() { new Boeing737("1"), new AirbusA320("2"), new Boeing737("3"), new Boeing737("4"), new AirbusA320("5") }; var lands = new List<Land>() { new Land(1), new Land(2) }; // Подписываемся на события изменения состояний самолетов и полос. aircrafts.ForEach(a => a.StateChanged += A_StateChanged); lands.ForEach(l => l.StateChanged += L_StateChanged); // Передаем диспетчеру в управление самолеты и взлетно-посадочные полосы. var dispetcher = new Dispatcher(aircrafts, lands); // Создаем отдельный поток, в котором диспетчер будет при необходимости отправлять самолеты в полет. var aircractStartThread = new Thread(() => Start(dispetcher)); aircractStartThread.Start(); } /// <summary> /// Поток отправки самолетов. /// </summary> /// <param name="dispetcher">Диспетчер управляющий полетами.</param> private static void Start(Dispatcher dispetcher) { while (true) { // Используем два генератора случайных чисел с разными шумом, // чтобы была вероятность выпадения одинаковых чисел. var random1 = new Random(Convert.ToInt32(DateTime.Now.Ticks % int.MaxValue)); var random2 = new Random(DateTime.Now.Millisecond); // 10 просто магическое число, определяющее вероятность вылета самолета. var dice1 = random1.Next(0, 10); var dice2 = random2.Next(0, 10); if (dice1 == dice2) { var distance = random1.Next(10000, 40000); Console.WriteLine(dispetcher.Send(distance)); } Thread.Sleep(1000); } } /// <summary> /// Обработчик события изменения состояния взлетно-посадочной полосы. /// </summary> /// <param name="sender">Взлетно-посадочная полоса.</param> /// <param name="e">Состояние полосы. true - полоса свободна, false - полоса занята.</param> private static void L_StateChanged(object sender, bool e) { var state = e ? "свободна" : "занята"; Console.WriteLine($"Полоса {sender} {state}"); } private static void A_StateChanged(object sender, AircraftState e) { Console.WriteLine($"Самолет {sender} находится в состоянии {e.GetDescription()}"); } } }
Получаем следующий результат работы приложения.

Исходный код приложения можно скачать из репозитория https://github.com/shwanoff/Mediator.
Таким образом, нам достаточно просто удалось разработать элементарную систему компьютерного моделирования работы аэропорта с использованием паттерна Посредник и асинхронного программирования.
Рекомендую также изучить статью Паттерн Адаптер (Adapter). А еще подписывайтесь на группу ВКонтакте, Telegram и YouTube-канал. Там еще больше полезного и интересного для программистов.