Mediator C# | Паттерн Посредник C#

Давайте рассмотрим паттерн проектирования Посредник C# — Mediator C#, для чего он нужен и какие проблемы он решает. Где можно применять данный шаблон, а где это будет излишним.

Идея паттерна Посредник (Mediator)

Паттерн (шаблон) проектирования — это продуманный способ построения исходного кода программы для решения часто возникающих в повседневном программировании проблем проектирования. Иными словами, это уже придуманное решения, для типичной задачи. При этом паттерн не готовое решение, а просто алгоритм действий, который должен привести к желаемому результату. Давайте рассмотрим один из наиболее часто используемых поведенческих паттернов — Посредник (Mediator).

Как я уже писал ранее, существует три вида паттернов проектирования:

  • Порождающие паттерны позволяют возможность выполнять инициализацию объектов наиболее удобным и оптимальным способом.
  • Структурные паттерны описывают взаимоотношения между различными классами или объектами, позволяя им совместно реализовывать поставленную задачу.
  • Поведенческие паттерны позволяют грамотно организовать связь между сущностями для оптимизации и упрощения их взаимодействия.

Посредник (Mediator) — это поведенческий паттерн, который позволяет организовать работу множества слабо связанных объектов без непосредственного общения между ними. То есть, Посредник выступает промежуточным звеном между объектами, принимая и перенаправляя сообщения.

Архитектура паттерна проектирования Посредник

Mediator C# - Посредник C#
Посредник (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()}");
        }
    }
}

Получаем следующий результат работы приложения.

Mediator result
Mediator result

Исходный код приложения можно скачать из репозитория https://github.com/shwanoff/Mediator.

Таким образом, нам достаточно просто удалось разработать элементарную систему компьютерного моделирования работы аэропорта с использованием паттерна Посредник и асинхронного программирования.

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