Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Паттерны проектирования с примерами на C#

License

NotificationsYou must be signed in to change notification settings

stacenko-developer/Patterns

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

В данном репозитории содержатся реализации паттернов на языке программирования C#. Ниже вы можете ознакомиться с описанием паттернов, их назначением, а также преимуществами и недостатками.
После изучения теории можете переходить в код - он хорошо задокументирован, поэтому разобраться в нем не составит труда.
Помимо документации в данном файле будет пошагово описана реализация каждого паттерна, которыцй есть в репозитории.

В папке каждого паттерна содержатся:

  • его реализация в библиотеке классов;
  • демонстрация работы в консольном приложении;
  • тестирование методов классов и проверка корректности реализации паттерна

Оглавление

  1. Порождающие паттерны (Creational)
    1. Абстрактная фабрика (Abstract Factory)
    2. Одиночка (Singleton)
    3. Строитель (Builder)
    4. Прототип (Prototype)
    5. Фабричный метод (Factory Method)
  2. Структурные паттерны
    1. Адаптер (Adapter)
    2. Декоратор (Decorator)
    3. Компоновщик (Composite)
    4. Фасад (Facade)
  3. Поведенческие (Behavioral)
    1. Итератор (Iterator)
    2. Наблюдатель (Observer)
    3. Посредник (Mediator)
    4. Шаблонный метод (Template Method)
  4. Архитектурные (Architectural)
    1. Внедрение зависимостей (Dependency Injection)

Порождающие паттерны

С помощью пораждающих паттернов (Creational) у нас есть возможность удобно и безопасносно создавать объекты или группы объектов.


Абстрактная фабрика

Абстрактная фабрика (Abstract Factory) – это порождающий паттерн проектирования, который позволяет создавать семейства связанных объектов, не привязываясь к конкретным классам создаваемых объектов.
Нам нужен такой способ создавать объекты, чтобы они сочетались с другими одного и того же семейства. Кроме того, мы не хотим вносить изменения в существующий код при добавлении новых объектов в программу.

Данный паттерн необходимо использовать, когда система не должна зависеть от способа создания и компоновки новых объектов и когда создаваемые объекты должны использоваться вместе и являются взаимосвязанными.


Предположим, что у нас есть какая-то компания, в которой работают сотрудники. Для простоты у нас есть два типа работников:программист идиректор. У каждого сотрудника есть девайс (компьютер и ноутбук) и служебный транспорт (пусть будет BMW и LADA).

1️⃣ Для начала создадим два абстрактных класса:WorkingCar (транспорт для нашего сотрудника) иWorkingDevice (устройство, на котором наш сотрудник будет работать). Начнем с автомобиля: у него есть модель, цена и год выпуска. Помимо этого у нас будет абстрактный метод получения стоимости налога:

/// <summary>/// Рабочий автомобиль./// </summary>publicabstractclassWorkingCar{/// <summary>/// Модель автомобиля./// </summary>protectedstring_model;/// <summary>/// Цена./// </summary>protectedint_price;/// <summary>/// Год выпуска./// </summary>protectedint_releaseYear;/// <summary>/// Получить стоимость налога./// </summary>/// <returns>Налог.</returns>publicabstractintGetTax();}

Аналогично реализовываем абстрактный класс рабочего устройства, у коготого есть цена и модель. В данном классе тоже будет абстрактный метод, который будет расчитывать стоимость дополнительных аксессуаров (например, зарядка, мышка и так далее).

/// <summary>/// Рабочее устройство./// </summary>publicabstractclassWorkingDevice{/// <summary>/// Модель./// </summary>protectedstring_model;/// <summary>/// Цена./// </summary>protectedint_price;/// <summary>/// Получить стоимость дополнительных аксессуаров./// </summary>/// <returns>Стоимость дополнительных аксессуаров.</returns>publicabstractintGetAccessoriesCost();}

2️⃣ После этого нам необходимо создатьклассы наследники: у WorkingCar классами-наследниками будут LADA и BMW, у WorkingDevice - Laptop и Computer. В данных классах наследникам мы реализуем методы, которые были в наших абстрактных классах.
3️⃣ Теперь нам необходимо реализоватьинтерфейс фабрики по созданию сотрудника, в котором будет 2 метода: создание объектарабочего устройства и создание объектарабочего автомобиля.

/// <summary>/// Фабрика сотрудника./// </summary>publicinterfaceIWorkerFactory{/// <summary>/// Создание рабочего устройства для сотрудника./// </summary>/// <returns>Рабочее устройство.</returns>WorkingDeviceCreateWorkingDevice();/// <summary>/// Создание рабочего автомобиля./// </summary>/// <returns>Рабочий автомобиль.</returns>WorkingCarCreateWorkingCar();}

Далее нам необходимо создать фабрику по созданию программиста и директора, реализующую интерфейсIWorkerFactory. Рассмотрим пример фабрики по созданию директора. У нас методCreateWorkingCar() возвращает объект автомобиля маркиBMW и методCreateWorkingDevice() объекткомпьютера в качестве рабочего устройства.

/// <summary>/// Фабрика директора./// </summary>publicclassDirectorFactory:IWorkerFactory{/// <summary>/// Создание рабочего автомобиля./// </summary>/// <returns>Рабочий автомобиль.</returns>publicWorkingCarCreateWorkingCar()=>newBMW();/// <summary>/// Создание рабочего устройства./// </summary>/// <returns>Рабочее устройство.</returns>publicWorkingDeviceCreateWorkingDevice()=>newComputer();}

Аналогично реализована фабрика по созданию объекта программиста: методCreateWorkingCar() возвращает объект Lada и методCreateWorkingDevice() возвращает ноутбук.


4️⃣ Теперь у нас есть все условия для того, чтобы создать класс самого сотрудника. Как было сказано ранее, у сотрудника есть служебный автомобиль и рабочее устройство. Добавим их в поля сотрудника.
В конструкторе в качестве параметра мы будем принимать интерфейсIWorkerFactory. Напомню, его у нас реализуют ProgrammerFactory и DirectorFactory.
В итоге у нас получился следующий код:

/// <summary>/// Сотрудник./// </summary>publicclassWorker{/// <summary>/// Рабочий автомобиль./// </summary>privateWorkingCar_workingCar;/// <summary>/// Рабочее устройство./// </summary>privateWorkingDevice_workingDevice;/// <summary>/// Создание сотрудника с помощью указанных параметров./// </summary>/// <param name="workerFactory">Фабрика сотрудника.</param>/// <exception cref="ArgumentNullException">Фабрика сотрудника равна null!</exception>publicWorker(IWorkerFactoryworkerFactory){if(workerFactory==null){thrownewArgumentNullException(nameof(workerFactory),"Фабрика для создания сотрудника равна null!");}_workingCar=workerFactory.CreateWorkingCar();_workingDevice=workerFactory.CreateWorkingDevice();}/// <summary>/// Получить стоимость налога за автомобиль./// </summary>/// <returns>Стоимость налога.</returns>publicintGetTax()=>_workingCar.GetTax();/// <summary>/// Получить стоимость дополнительных аксессуаров для рабочего устройства./// </summary>/// <returns>Стоимость дополнительных аксессуаров.</returns>publicintGetAccessoriesCost()=>_workingDevice.GetAccessoriesCost();}

Преимущества паттерна Abstract Factory: упрощение добавления новых продуктов, их сочетаемость, а также избавление кода от привязки к конкретным классам продуктов.
Недостатки: возможное усложнение кода из-за создания огромного количества вспомогательных классов.


Одиночка

Одиночка (Singleton) - этопаттерн, который позволяет гарантировать, что класс имеет только один экземпляр, обеспечивая при этом глобальную точку доступа к этому экземпляру.
Модель Singleton решает две проблемы одновременно,нарушая принцип единой ответственности:

1️⃣ Гарантия того, что класс имеет только один экземпляр. Это может пригодиться, когда необходимо контролировать доступ к какому-либо общему ресурсу, например, к базе данных или файлу.
2️⃣ Предоставление глобальной точки доступа к этому экземпляру.

Шаблон требует специальной обработки вмногопоточной среде, чтобы несколько потоков не создавали экземпляр класса несколько раз.

Теперь перейдем к реализации данногопаттерна. Пусть у нас есть какой-то сайт, в котором есть разделы. Раздел будет иметь следующие свойства: название и код (идентификатор).

/// <summary>/// Раздел./// </summary>publicclassSection{/// <summary>/// Название раздела./// </summary>publicstringName{get;set;}/// <summary>/// Код раздела./// </summary>publicstringCode{get;set;}}

Теперь мы хотим создать экземпляр класса базы данныхSectionDatabase, в которой будут храниться наши разделы.

1️⃣ В данном классе создаем статическое поле, имеющее тот же тип, что и сам класс: SectionDatabase. По умолчанию он будет равен null, так как еще ни разу не был создан экземпляр данного класса.
2️⃣ Создаем заблокированный объект, который мы будем использовать для синхронизации. Это означает, что в критическую область кода потоки будут заходить по очереди.
3️⃣ Создаем список разделов, в который мы будем добавлять созданные разделы.
4️⃣ Создаем защищенный конструктор. Это необходимо для того, чтобы у нас не было возможности вызвать публичный конструктор, так как в этом случае мы не сможем контролировать количество созданных экземпляров класса SectionDatabase.
5️⃣ Добавляем публичный методInitialize. Его назначение - инициализировать объект базы данных, а также проверять: если объект базы данных уже был создан, то необходимо возврать уже ранее созданный экземпляр. Также не забываем про использование синхронизации для критической секции.

Теперь посмотрим на получившийся результат (код представлен в упрощенном виде, полная реализация доступна в репозитории):

/// <summary>/// База данных разделов. Реализация паттерна Singleton./// </summary>publicclassSectionDatabase{/// <summary>/// База данных разделов./// </summary>privatestaticSectionDatabaseDatabase=null;/// <summary>/// Заблокированный объект./// Служит для синхронизации потоков./// </summary>privatestaticobjectLockObject=newobject();/// <summary>/// Список разделов./// </summary>privateList<Section>_sectionsList;/// <summary>/// Создает хранилище данных разделов./// </summary>protectedSectionDatabase(){}/// <summary>/// Инициализация хранилища данных разделов./// </summary>/// <returns>Хранилище данных разделов.</returns>publicstaticSectionDatabaseInitialize(){if(Database==null){lock(LockObject){if(Database==null){Database=newSectionDatabase();}}}returnDatabase;}}

Преимущества паттерна Singleton: класс гарантированно имеет только один экземпляр и не более, у нас есть точка доступа к единственному экземпляру (в нашем случае это метод Initialize).
Недостатки: нарушение принципа единой ответственности (Single Responsibility Principle), требуется особая обработка в многопоточной среде.


Строитель

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

❌ Проблема заключается в том, что у нас может быть какой-то сложный объект и его создание может привести к огромному количеству кода в конструктореПаттерн Builder (Строитель) состоить из двух участников:

  • Строитель (Builder) – предоставляет методы для сборки частей экземпляра класса;
  • Распорядитель (Director) – определяет саму стратегию того, как будет происходить сборка: определяет, в каком порядке будут вызываться методы Строителя.

Реализуем данный паттерн на основе примера: у нас есть завод по производству компьютеров. У нас есть разработчики компьютеров и директор.
1️⃣ Создадим класс компьютера, для простоты он будет содержать всего 4 характеристики:

/// <summary>/// Содержит методы для разработчика компьютеров./// </summary>publicinterfaceIComputerDeveloper{/// <summary>/// Установка процессора./// </summary>voidSetProcessor();/// <summary>/// Установка оперативной памяти./// </summary>voidSetRandomAccessMemory();/// <summary>/// Установка операционной системы./// </summary>voidSetOperationSystem();/// <summary>/// Получение компьютера./// </summary>/// <returns>Компьютер.</returns>ComputerGetComputer();}

2️⃣ Теперь создадим интерфейсIComputerDeveloper, которые будет содержать методы разработчика компьютеров:

/// <summary>/// Содержит методы для разработчика компьютеров./// </summary>publicinterfaceIComputerDeveloper{/// <summary>/// Установка процессора./// </summary>voidSetProcessor();/// <summary>/// Установка оперативной памяти./// </summary>voidSetRandomAccessMemory();/// <summary>/// Установка операционной системы./// </summary>voidSetOperationSystem();/// <summary>/// Получение компьютера./// </summary>/// <returns>Компьютер.</returns>ComputerGetComputer();}

3️⃣ Затем создадим классы HPComputerDeveloper (разработчик компьютеров HP) и DELLComputerDeveloper (Разработчик компьютеров DELL), в которых будет реализован интерфейс IComputerDeveloper. Рассмотрим реализация класса HPComputerDeveloper, класс DELLComputerDeveloper реализован аналогично.

/// <summary>/// Разработчик компьютеров HP./// </summary>publicclassHPComputerDeveloper:IComputerDeveloper{/// <summary>/// Компьютер./// </summary>privateComputer_computer;/// <summary>/// Модель./// </summary>privatestring_model="HP";/// <summary>/// Процессор./// </summary>privatestring_processor="Intel Core i5-7400";/// <summary>/// Количество оперативной памяти./// </summary>privateint_randomAccessMemoryCount=8;/// <summary>/// Операционная система./// </summary>privatestring_operationSystem="Windows 10 Pro";/// <summary>/// Создание разработчика компьютеров HP./// </summary>publicHPComputerDeveloper(){_computer=newComputer();_computer.Model=_model;}/// <summary>/// Установка процессора./// </summary>publicvoidSetProcessor(){_computer.Processor=_processor;}/// <summary>/// Установка оперативной памяти./// </summary>publicvoidSetRandomAccessMemory(){_computer.RandomAccessMemory=_randomAccessMemoryCount;}/// <summary>/// Установка операционной системы./// </summary>publicvoidSetOperationSystem(){_computer.OperationSystem=_operationSystem;}/// <summary>/// Получение компьютера./// </summary>/// <returns>Компьютер.</returns>publicComputerGetComputer()=>_computer;}

4️⃣ Теперь создадим класс Director, который будет иметь поле IComputerDeveloper, то есть, он будет принимать в конструкторе одного из разработчиков компьютеров и в зависимости от разработчика создавать определенный компьютер.

/// <summary>/// Директор./// </summary>publicclassDirector{/// <summary>/// Разработчик компьютеров./// </summary>privateIComputerDeveloper_computerDeveloper;/// <summary>/// Создание директора с помощью указанных параметров./// </summary>/// <param name="computerDeveloper">Разработчик компьютеров.</param>/// <exception cref="ArgumentNullException">Разработчик компьютеров равен null!</exception>publicDirector(IComputerDevelopercomputerDeveloper){if(computerDeveloper==null){thrownewArgumentNullException(nameof(computerDeveloper),"Разработчик компьютеров равен null!");}_computerDeveloper=computerDeveloper;}/// <summary>/// Создание полноценного компьютера./// </summary>/// <returns>Созданный компьютер.</returns>publicComputerCreateFullComputer(){_computerDeveloper.SetProcessor();_computerDeveloper.SetRandomAccessMemory();_computerDeveloper.SetOperationSystem();return_computerDeveloper.GetComputer();}/// <summary>/// Создание компьютера без операционной системы./// </summary>/// <returns>Созданный компьютер.</returns>publicComputerCreateComputerWithoutOperationSystem(){_computerDeveloper.SetProcessor();_computerDeveloper.SetRandomAccessMemory();return_computerDeveloper.GetComputer();}}

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

Преимущества паттерна Builder: контроль за этапами создания экземпляра класса, в зависимости от этапов можно получить различные объекты.
Недостатки: жесткая связка конкретного Builder и продукта, который он создает.


Прототип

Прототип (Prototype) - это такой паттерн, который используется ​для создания новых объектов с помощью клонирования существующих. Для того, чтобы определение паттерна было более понятно, приведу конкретный пример.
Предположим, что у нас есть биржа фриланса, где есть заказчики и исполнители.
1️⃣ Для начала создадим абстрактный класс User - пользователь биржи фриланса. У пользователя будут идентификатор, ФИО и абстрактный метод Clone. Это означает, что мы обязательно должны реализовать его в классе-наследнике.

/// <summary>/// Пользователь/// </summary>publicabstractclassUser{/// <summary>/// Идентификатор./// </summary>publicintId{get;set;}/// <summary>/// Имя./// </summary>publicstringFirstName{get;set;}/// <summary>/// Фамилия./// </summary>publicstringLastName{get;set;}/// <summary>/// Отчество./// </summary>publicstringPatronymic{get;set;}/// <summary>/// Клонирование пользователя./// </summary>/// <returns>Нового пользователя.</returns>publicabstractUserClone();/// <summary>/// Строковое представления объекта пользователя./// </summary>/// <returns>Данные пользователя в виде строки.</returns>publicoverridestringToString()=>$"Идентификатор -{Id} Имя -{FirstName} Фамилия -{LastName} Отчество -{Patronymic}";}

2️⃣ Теперь реализуем класс-наследник исполнителя: в него не будем добавлять дополнительные свойства. Реализация метода Clone будет выглядеть так:

/// <summary>/// Исполнитель./// </summary>publicclassExecutor:User{/// <summary>/// Клонирование пользователя./// </summary>/// <returns>Нового пользователя.</returns>publicoverrideUserClone()=>(Executor)MemberwiseClone();/// <summary>/// Строковое представления объекта исполнителя./// </summary>/// <returns>Данные исполнителя в виде строки.</returns>publicoverridestringToString()=>$"Данные исполнителя:{base.ToString()}";}

В данном случае мы можем выполнить неполное (поверхностное) копирование. Это подходит тогда, когда у нас все поля Executor являются значимыми типами (исключение: string).

Теперь рассмотрим второй класс-наследник: Customer.

/// <summary>/// Заказчик/// </summary>publicclassCustomer:User{/// <summary>/// Паспорт./// </summary>publicPassportPassport{get;set;}/// <summary>/// Создание пользователя./// </summary>publicCustomer(){Passport=newPassport();}/// <summary>/// Клонирование пользователя./// </summary>/// <returns>Нового пользователя.</returns>publicoverrideUserClone(){varcustomer=(Customer)MemberwiseClone();customer.Passport=newPassport();customer.Passport.Series=Passport.Series;customer.Passport.Number=Passport.Number;customer.Passport.ReceiptPlace=Passport.ReceiptPlace;returncustomer;}/// <summary>/// Строковое представления объекта заказчика./// </summary>/// <returns>Данные заказчика в виде строки.</returns>publicoverridestringToString()=>$"Данные заказчика:{base.ToString()}{Passport}";}

Здесь у нас уже присутствует класс Passport - это ссылочный тип, соответсвенно, мы не можем использовать неполное копирование. Если мы будем использовать неполное копирование, то у нас будет два заказчика ссылаться на один и тот же объект паспортных данных. Поэтому нам придется создать новый объект паспорта и вручную его проинициализировать.

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

Преимущества паттерна Prototype: клонирование объектов без привязки к конкретным классам, сокращение кода инициализации экземплятор классов.
Недостатки: Проблемы с клонированием составных объектов, то есть, тех объектов, которые внутри содержат другие объекты.


Фабричный метод

Фабричный метод (Factory Method) - это порождающий паттерн проектирования, который определяет интерфейс для создания объектов определенного класса, но именно в подклассах принимается решение о типе объекта, который будет создан.

✅ Фабричный метод будет полезен, если мы заранее не знаем, объекты каких типов мы хотим создать.

У нас есть следующие участники:

  • базовый класс какого-либо продукта;
  • наследники базового класса продукта;
  • базовый класс создателя этого продукта;
  • создатели-наследники базового класса создателя, которые созданию продукты-наследники базового класса продукта

1️⃣ Реализуем паттерн на примере создания телефонов. Пусть у нас будет базовый классPhone, содержащий следующие свойства:

/// <summary>/// Телефон./// </summary>publicabstractclassPhone{/// <summary>/// Цена./// </summary>publicdecimalPrice{get;set;}/// <summary>/// Модель./// </summary>publicstringModel{get;set;}=string.Empty;/// <summary>/// Процессор./// </summary>publicstringProcessor{get;set;}=string.Empty;/// <summary>/// Оперативная память./// </summary>publicintRandomAccessMemory{get;set;}/// <summary>/// Строковое представления объекта телефона./// </summary>/// <returns>Данные телефона в виде строки.</returns>publicoverridestringToString()=>$"Цена ={Price} Модель ={Model} Процессор ={Processor} Оперативная память ={RandomAccessMemory}";}

2️⃣ Создадим два класса наследника: Nokia и Samsung, в них добавим по одному дополнительному свойству.

/// <summary>/// Нокиа./// </summary>publicclassNokia:Phone{/// <summary>/// Работает ли батарея./// </summary>publicboolIsBatteryWork{get;set;}/// <summary>/// Строковое представления объекта Нокиа./// </summary>/// <returns>Данные Нокиа в виде строки.</returns>publicoverridestringToString()=>$"Нокиа:{base.ToString()} Работает ли батарея ={IsBatteryWork}";}/// <summary>/// Самсунг./// </summary>publicclassSamsung:Phone{/// <summary>/// Влючена ли сейчас фронтальная камера./// </summary>publicboolIsFrontCamera{get;set;}/// <summary>/// Строковое представления объекта Самсунга./// </summary>/// <returns>Данные Самсунга в виде строки.</returns>publicoverridestringToString()=>$"Самсунг:{base.ToString()} Включена ли фронтальная камера ={IsFrontCamera}";}

3️⃣ Создадим интерфейс создателя телефонов IPhoneDeveloper:

/// <summary>/// Содержит методы для рабработчика телефонов./// </summary>publicinterfaceIPhoneDeveloper{/// <summary>/// Создание телефона./// </summary>/// <returns>Телефон.</returns>PhoneCreatePhone();}

4️⃣ Создадим разработчиков телефонов Нокиа и Самсунга, реализующих интерфейсыIPhoneDeveloper:

/// <summary>/// Разработчик телефонов фирмы Нокиа./// </summary>publicclassNokiaDeveloper:IPhoneDeveloper{/// <summary>/// Создание телефона./// </summary>/// <returns>Телефон.</returns>publicPhoneCreatePhone()=>newNokia();}/// <summary>/// Разработчик телефонов фирмы Самсунг./// </summary>publicclassSamsungDeveloper{/// <summary>/// Создание телефона./// </summary>/// <returns>Телефон.</returns>publicPhoneCreatePhone()=>newSamsung();}

Преимущества паттерна Factory Method: упрощение поддержки кода, так как продукт создается в отдельном классе.
Недостатки: Значительное увеличение кода, так как для каждого класса продукта необходимо будет добавлять класс-создатель, который будет создавать данный продукт.


Структурные паттерны

Структурные паттерны (Structural) - цель их применения заключается в том, что благодаря им вы можете совмещать и сочетать сущности вместе.


Адаптер

Адаптер (Adapter) - это структурный шаблон проектирования, который используется для организации использования методов объекта, недоступного для модификации, через специально созданный интерфейс.

Проблема: у нас уже есть конкретный класс и нужно, чтобы этот класс реализовывал определенный интерфейс, при этом сам класс менять нельзя.

✅ Решение: мы пишем класс, который будет реализовывать необходимый интерфейс, затем мы наследуем наш класс от того класса, который нам нужно изменить без прямого вмешательства в тот класс.Предположим, что у нас есть офис, в котором работают сотрудники. У офиса есть название, адрес:

/// <summary>/// Офис./// </summary>publicclassOffice{/// <summary>/// Название./// </summary>protectedstring_name;/// <summary>/// Адрес./// </summary>protectedstring_address;/// <summary>/// Создает офис с помощью указанных параметров./// </summary>/// <param name="name">Название офиса.</param>/// <param name="address">Адрес офиса.</param>publicOffice(stringname,stringaddress){Validator.ValidateStringText(name);Validator.ValidateStringText(address);_name=name;_address=address;}}

Теперь нам необходимо добавить метод "переезда офиса". То есть метод, который устанавливает новое значение для адреса. Не будем забывать, что существующий класс нельзя модифицировать. Решим проблему с помощью паттерна Адаптер:
1️⃣ Создадим интерфейс IMovable, в котором будет один метод Move, принимающий новый адрес офиса.

/// <summary>/// Содержит методы, связанные с переездом офиса./// </summary>publicinterfaceIMovable{/// <summary>/// Изменение адреса офиса./// </summary>/// <param name="newAddress">Новый адрес.</param>voidMove(stringnewAddress);}

2️⃣ Создадим класс-наследник нашего базового класса Office, пусть это будет офис какой-то конкретной компании (например, Норбит). В данном классе мы реализуем интерфейсIMovable.

/// <summary>/// Офис Норбит. Реализация паттерна Адаптер./// </summary>publicclassNrbOffice:Office,IMovable{/// <summary>/// Создает офис Норбит с помощью указанных параметров./// </summary>/// <param name="name">Название офиса.</param>/// <param name="address">Адрес офиса.</param>publicNrbOffice(stringname,stringaddress):base(name,address){}/// <summary>/// Изменение адреса офиса./// </summary>/// <param name="newAddress">Новый адрес.</param>publicvoidMove(stringnewAddress){Validator.ValidateStringText(newAddress);_address=newAddress;}}

Преимущества паттерна Adapter: возможность отделения интерфейса или кода преобразования данных от основной бизнес-логики программы.
Недостатки: общая сложность кода увеличивается.


Декоратор

Декоратор (Decorator) - это паттерн, который позволяет динамически подключать к объекту дополнительную функциональность, оборачивая объект в обертки.
Для того, чтобы определить какой-либо новый функционал, как правило, мы прибегаем к наследованию. Декораторы в отличие от наследования позволяют динамически в процессе выполнения определять новые возможности у объектов.

Можно использовать несколько разных обёрток одновременно и вы получите бъединённое поведение сразу всех обёрток.

Давайте теперь реализуем данный паттерн. Пусть у нас также будут сотрудники какой-то компании. У нас естьзадача: отфильтровать сотрудника по определенному признаку.
1️⃣ Для начала создадим абстрактный классWorkersFilter, в котором будет методGetFiltratedList(), который будет возвращать нам отфильтрованный список сотрудников.

/// <summary>/// Фильтр./// </summary>publicabstractclassWorkersFilter{/// <summary>/// Получение отфильтрованного списка./// </summary>/// <returns>Отфильтрованный список.</returns>publicabstractList<Worker>GetFiltratedList();}

2️⃣ Создаем класс-наследник NorbitWorkersFilter, базовый класс у которого WorkersFilter. В наследнике мы реализуем логику метода GetFiltratedList(). То есть текущий фильтр будет возвращать коллекцию, у всех сотрудников которой значение свойстваOrganization равноNorbit.

/// <summary>/// Фильтр сотрудников./// </summary>publicclassNorbitWorkersFilter:WorkersFilter{/// <summary>/// Сотрудники./// </summary>protectedstaticList<Worker>Workers;/// <summary>/// Название организации по умолчанию./// </summary>privatestring_defaultOrganization="Норбит";/// <summary>/// Создание фильтра сотрудников с помощью указанных параметров./// </summary>/// <param name="workers">Список сотрудников.</param>/// <exception cref="ArgumentNullException">Список сотрудников или его элементы равны null!</exception>publicNorbitWorkersFilter(List<Worker>workers){if(workers==null||workers.FindIndex(worker=>worker==null)!=-1){thrownewArgumentNullException(nameof(workers),"Список сотрудников или его элементы равны null!");}Workers=workers;}/// <summary>/// Получение отфильтрованного списка сотрудников./// </summary>/// <returns>Отфильтрованный список сотрудников.</returns>publicoverrideList<Worker>GetFiltratedList()=>Workers.Where(worker=>worker.Organization==_defaultOrganization).ToList();}

3️⃣ Создаем класс дополнительного условия фильтрации, который будет классом-наследником класс NorbitWorkersFilter. Он будет создаваться с помощью конструктора, который будет приниматт объект типа NorbitWorkersFilter.

/// <summary>/// Дополнительное условие фильтрации./// </summary>publicclassAdditionalFilteringCondition:NorbitWorkersFilter{/// <summary>/// Фильтр сотрудников Норбит./// </summary>protectedNorbitWorkersFilter_filter;/// <summary>/// Создает дополнительное условие фильтрации с помощью указанных параметров./// </summary>/// <param name="filter">Фильтр сотрудников Норбит.</param>publicAdditionalFilteringCondition(NorbitWorkersFilterfilter):base(Workers){if(filter==null){thrownewArgumentNullException(nameof(filter),"Фильтр равен null!");}_filter=filter;Workers=base.GetFiltratedList();}}

Перед тем, как список будет отфильтрован дополнительным условием, он сначала будет отфильтрован фильтров базового метода с помощьюbase.GetFiltratedList()

4️⃣ Добавим классы, которые будут фильтровать сотрудников по возрасту и должности.

/// <summary>/// Фильтр сотрудников по возрасту./// </summary>publicclassAgeWorkersFilter:AdditionalFilteringCondition{/// <summary>/// Минимальное допустимое значение возраста./// </summary>privateint_defaultMinCorrectAge=25;/// <summary>/// Создание фильтра сотрудников по возрасту с помощью указанных параметров./// </summary>/// <param name="filter">Базовый фильтр сотрудников Норбит.</param>publicAgeWorkersFilter(NorbitWorkersFilterfilter):base(filter){Workers=filter.GetFiltratedList();}/// <summary>/// Получение отфильтрованного списка сотрудников./// </summary>/// <returns>Отфильтрованный список сотрудников.</returns>publicoverrideList<Worker>GetFiltratedList()=>Workers.Where(worker=>worker.Age>=_defaultMinCorrectAge).ToList();}

По такому же принципу реализован класс PostWorkersFilter. Для подробного ознакомления рекомендую перейти в репозиторий. Также с помощью консольного приложения вы можете наблюдать снижения количества сотрудников в коллекции по мере добавления к базовому фильтру сотрудников дополнительный оберток.

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


Компоновщик

Компоновщик (Composite) - это структурный паттерн проектирования, который используется, когда объекты должны быть реализованы в виде древовидной структуры и когда клиенты аналогично управляют как целыми объектами, так и составными частями.

✅ Реализуем данный паттерн на примере файловой системы.

1️⃣ Создадим абстрактный класс компонента файловой системы, единственное поле у которого будет название компонента. Мы также можем добавлять в компонент другие компоненты и аналогично удалять их.

/// <summary>/// Компонент файловой системы./// </summary>publicabstractclassFileSystemComponent{/// <summary>/// Название./// </summary>protectedstring_name;/// <summary>/// Создание компонента файловой системы с помощью указанных параметров./// </summary>/// <param name="name">Название компонента файловой системы.</param>publicFileSystemComponent(stringname){Validator.ValidateStringText(name);_name=name;}/// <summary>/// Проверка корректности компонента./// </summary>/// <param name="component">Компонент, который необходимо проверить.</param>/// <exception cref="ArgumentNullException">Компонент равен null!</exception>protectedvoidValidateComponent(FileSystemComponentcomponent){if(component==null){thrownewArgumentNullException(nameof(component),"Компонент равен null!");}}/// <summary>/// Добавление компонента./// </summary>/// <param name="component">Компонент, который необходимо добавить.</param>publicvirtualvoidAdd(FileSystemComponentcomponent){ValidateComponent(component);}/// <summary>/// Добавление компонента./// </summary>/// <param name="component">Компонент, который необходимо добавить.</param>publicvirtualvoidRemove(FileSystemComponentcomponent){ValidateComponent(component);}/// <summary>/// Строковое преставление объекта компонента файловой системы./// </summary>/// <returns>Данные объекта компонента файловой системы в виде строки.</returns>publicoverridestringToString()=>_name;}

2️⃣ Создадим первый класс-наследник компонента файловой системы: файл. У файла не будут переопределены методы добавления и удаления - базовая реализация нас вполне устраивает, поскольку мы не может добавлять в файл другие файлы.

/// <summary>/// Файл./// </summary>publicclassFile:FileSystemComponent{/// <summary>/// Создание файла с помощью указанных параметров./// </summary>/// <param name="name">Название файла.</param>publicFile(stringname):base(name){}}

3️⃣ Добавим второй класс-наследник компонента файловой системы: папка. У папки будет список компонентов файловой системы, поскольку в папку мы уже можем добавить как другие файлы, так и другие папки. Также будут переопределены методы добавления и удаления компонентов файловой системы из директории.

/// <summary>/// Папка./// </summary>publicclassDirectory:FileSystemComponent{/// <summary>/// Список компонентов файловой системы, которые находятся в папке./// </summary>privateList<FileSystemComponent>_components=newList<FileSystemComponent>();/// <summary>/// Создает папку с помощью указанных параметров./// </summary>/// <param name="name">Название папки.</param>publicDirectory(stringname):base(name){}/// <summary>/// Добавление компонента./// </summary>/// <param name="component">Компонент, который необходимо добавить.</param>publicoverridevoidAdd(FileSystemComponentcomponent){ValidateComponent(component);_components.Add(component);}/// <summary>/// Добавление компонента./// </summary>/// <param name="component">Компонент, который необходимо добавить.</param>/// <exception cref="ArgumentNullException">Указанный компонент файловой системы отсутствует в директории!</exception>publicoverridevoidRemove(FileSystemComponentcomponent){ValidateComponent(component);if(!_components.Contains(component)){thrownewArgumentNullException("Указанный компонент файловой системы отсутствует в директории!");}_components.Remove(component);}/// <summary>/// Строковое преставление объекта компонента файловой системы./// </summary>/// <returns>Данные объекта компонента файловой системы в виде строки.</returns>publicoverridestringToString()=>$"{_name}:{string.Join("=>",_components)}{Environment.NewLine}";}

Преимущества паттерна Composite: упрощение работы с деревом компонентов, также более удобно добавлять в программу новые компоненты.
Недостатки: слишком общий интерфейс для классов, функциональность которых может сильно отличаться.


Фасад

Фасад (Facade) -

Поведенческие паттерны

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


Итератор

Итератор (Iterator) - это поведенческий паттерн проектирования, благодаря которому у нас есть возможность последовательно обходить элементы составных объектов, при этом не раскрывая их внутреннего представления.
Идея паттерна в том, чтобы вынести поведение обхода коллекции из самой коллекции вотдельный класс.

✅ Зная эту информацию, давайте теперь его реализуем.

Пусть у нас будет файловая система, которая будет хранить файлы. У каждого файла есть следующие свойства:

/// <summary>/// Файл./// </summary>publicclassFile{/// <summary>/// Идентификатор./// </summary>publicGuidId{get;set;}/// <summary>/// Название./// </summary>publicstringName{get;set;}/// <summary>/// Тип./// </summary>publicstringType{get;set;}/// <summary>/// Строковое преставление объекта файла./// </summary>/// <returns>Данные объекта файла в виде строки.</returns>publicoverridestringToString()=>$"Идентификатор:{Id} Название:{Name} Тип:{Type}";}

1️⃣ Создадим интерфейс IFileIterator итератора для файловой системы:

/// <summary>/// Содержит методы для итератора файловой системы./// </summary>publicinterfaceIFileIterator{/// <summary>/// Проверяет, есть ли в коллекции следующий элемент./// </summary>/// <returns>Результат проверки.</returns>boolHasNext();/// <summary>/// Получает следующий элемент./// </summary>/// <returns>Следующий элемент.</returns>FileNext();}

2️⃣ Создадим интерфейс IFileNumerator, содержащий методы получения итератора из коллекции:

/// <summary>/// Содержит методы получения итератора из коллекции./// </summary>publicinterfaceIFileNumerable{/// <summary>/// Создание итератора./// </summary>/// <returns>Созданный итератор.</returns>IFileIteratorCreateNumerator();/// <summary>/// Количество элементов в коллекции./// </summary>intCount{get;}/// <summary>/// Получение элемента из коллекции по индексу./// </summary>/// <param name="index">Индекс элемента, который необходимо получить.</param>/// <returns>Элемент по индексу.</returns>Filethis[intindex]{get;}}

3️⃣ Теперь мы можем создать конкретную файловую систему, реализующую интерфейс IFileNumerable:

/// <summary>/// Файловая система./// </summary>publicclassFileSystem:IFileNumerable{/// <summary>/// Файлы, хранящиеся в файловой системе./// </summary>privateList<File>_files;/// <summary>/// Количество файлов в файловой системе./// </summary>publicintCount=>_files.Count;/// <summary>/// Создание файловой системы./// </summary>publicFileSystem(){_files=newList<File>();}/// <summary>/// Проверяет выход индекса за границы списка файлов файловой системы./// </summary>/// <param name="index">Порядковый номер элемента.</param>/// <exception cref="ArgumentOutOfRangeException">Индекс вышел за границы!/// </exception>privatevoidValidateIndex(intindex){if(index<0||index>=_files.Count){thrownewArgumentOutOfRangeException("Индекс вышел за границы массива!");}}/// <summary>/// Создание итератора./// </summary>/// <returns>Итератор.</returns>publicIFileIteratorCreateNumerator()=>newFileSystemNumerator(this);// Данный класс мы создадим далее./// <summary>/// Доступ к элементам файловой системы./// </summary>/// <param name="index">Позиция элемента, к которому необходим доступ.</param>/// <exception cref="ArgumentOutOfRangeException">Индекс вышел за границы!</exception>publicFilethis[intindex]{get{ValidateIndex(index);return_files[index];}}}

4️⃣ Теперь последний шаг - реализуем класс-алгоритм обхода файловой системы, реализующий интерфейс IFileIterator.

/// <summary>/// Реализует алгоритм обхода файловой системы./// </summary>publicclassFileSystemNumerator:IFileIterator{/// <summary>/// Содержит методы для создания объекта-итератора./// </summary>privateIFileNumerable_aggregate;/// <summary>/// Индекс текущего элемента./// </summary>privateint_index=0;/// <summary>/// Создание итератора файловой системы с помощью указанных параметров./// </summary>/// <param name="aggregate">Содержит методы для создания объекта-итератора.</param>/// <exception cref="ArgumentNullException">Интерфейс для создания объекта-итератора равен null!</exception>publicFileSystemNumerator(IFileNumerableaggregate){if(aggregate==null){thrownewArgumentNullException(nameof(aggregate),"Интерфейс для создания объекта-итератора равен null!");}_aggregate=aggregate;}/// <summary>/// Проверяет наличие следующего элемента./// </summary>/// <returns>Результат проверки.</returns>publicboolHasNext()=>_index<_aggregate.Count;/// <summary>/// Получение следующего элемента из файловой системы./// </summary>/// <returns>Следующий файл.</returns>/// <exception cref="ArgumentOutOfRangeException">Индекс вышел за границы!</exception>publicFileNext(){if(!HasNext()){thrownewArgumentOutOfRangeException("Индекс вышел за границы!");}return_aggregate[_index++];}}

Преимущества паттерна Iterator: достигается упрощение классов хранения данных.
Недостатки: Если вы работаете только с простыми коллекциями, то вам нет необходимости использовать данный паттерн.


Наблюдатель

Наблюдатель (Observer) - поведенческий шаблон проектирования. Определяет зависимость типа «один ко многим» таким образом, что при изменении объекта все, зависящие от него, получают сообщение об этом событии.
В dotnet есть три способа реализации данного паттерна:

1️⃣Через делегаты. Данный способ гарантирует наличие наблюдателя и подходит, когда нужно реализовать отношение: 1 поставщик – 1 наблюдатель. Также при данном подходе можно получить результат – ответ от подписчика.
2️⃣Через события. Любое число подписчиков. Нет гарантии наличия подписчиков. Не предусмотрено получение ответа от подписчика.
3️⃣Через набор интерфейсов IObserver (механизм для получения push-уведомлений)/IObservable (определяет поставщика push-уведомлений).

❌ Почему стоит использовать эти интерфейсы вместо событий: события плохо поддаются тестированию, данный паттерн универсален, он может использоваться и в других языках программирования. В C# есть события, а в других языках программирования их может и не быть.

❕ Таким образом, у нас естьIObservable – определяет наблюдаемый объект иIObserver – определяет наблюдателей.

Реализуем паттерннаблюдатель на примерекорпоративного портала для сотрудников. У нас будут пользователи корпоративного портала - сотрудники компании.
Сотрудники могут подписываться на уведомления о каких-либо новостях, событиях, которые будут публиковаться на корпоративном портале.
Соответственно, все пользователи, которые подписаны на уведомления корпоративного портала (подписчики) будут уведомлены о каком-либо событии. В данном случае корпоративный портал будетпоставщиком данных для наших подписчиков.

1️⃣ Создаем класс Message, который будет являться объектом сообщения (уведомления), которое будут получать подписчики от поставщика данных - в нашем случае корпоративного портала.

/// <summary>/// Сообщение./// </summary>publicclassMessage{/// <summary>/// Текст сообщения./// </summary>privatestring_text;/// <summary>/// Автор сообщения./// </summary>privatestring_author;/// <summary>/// Создание сообщения с помощью указанных данных./// </summary>/// <param name="text">Текст сообщения.</param>/// <param name="author">Автор сообщения.</param>publicMessage(stringtext,stringauthor){Validator.ValidateStringText(text);Validator.ValidateStringText(author);_text=text;_author=author;}/// <summary>/// Строковое представление объекта сообщения./// </summary>/// <returns>Данные сообщения в виде строки.</returns>publicoverridestringToString()=>$"Текст сообщения:{Environment.NewLine}{_text}"+$"{Environment.NewLine}Автор:{_author}";}

2️⃣ Далее мы создаем объект пользователя, реализующего интерфейс IObserver, поскольку он является наблюдателем. Параметром интерфейса выступаетMessage - тип данных уведопления, которое будут получать подписчики.
Реализуя данный интерфейс, нам необходимо реализовать логику работы методовOnCompleted, OnError, OnNext.

/// <summary>/// Пользователь./// </summary>publicclassUser:IObserver<Message>{/// <summary>/// Логин пользователя./// </summary>privatestring_login;/// <summary>/// Создает пользователя с помощью указанных параметров./// </summary>/// <param name="login">Логин пользователя.</param>/// <exception cref="ArgumentNullException">Логин равен null!</exception>publicUser(stringlogin){Validator.ValidateStringText(login);_login=login;}/// <summary>/// Обработчик события, когда от поставщика данных больше не будет поступать никаких уведомлений./// </summary>publicvoidOnCompleted(){}/// <summary>/// Обработчик события возникновения исключения у поставщика данных при отправке уведомлений./// </summary>/// <param name="error">Исключение, которое возникло у поставщика данных.</param>/// <exception cref="ArgumentNullException">Исключение равно null!</exception>publicvoidOnError(Exceptionerror){if(error==null){thrownewArgumentNullException(nameof(error),"Исключение равно null!");}Console.WriteLine($"Отправка уведомлений пользователю завершилась с ошибкой:{error.Message}"+$"{Environment.NewLine}Логин получателя:{_login}");}/// <summary>/// Обработчик события поступления уведомлений от поставщика данных./// </summary>/// <param name="value">Сообщение, поступившее от поставщика данных.</param>/// <exception cref="ArgumentNullException">Сообщение равно null!</exception>publicvoidOnNext(Messagevalue){if(value==null){thrownewArgumentNullException(nameof(value),"Сообщение равно null!");}Console.WriteLine($"Полученное уведомление:{Environment.NewLine}{value}{Environment.NewLine}"+$"Логин получателя:{_login}");}}

3️⃣ Теперь создадим класс нашего корпоративного портала, реализующий интерфейс IObservable (наблюдаемый объект). В качестве параметра также выступает тип данных Message - тип сообщения для подписчиков.

/// <summary>/// Корпоративный портал./// </summary>publicclassCorporatePortal:IObservable<Message>{/// <summary>/// Список подписчиков./// </summary>privatereadonlyList<IObserver<Message>>_observers;/// <summary>/// Создание корпоративного портала./// </summary>publicCorporatePortal(){_observers=newList<IObserver<Message>>();}/// <summary>/// Подписка на уведомления./// </summary>/// <param name="observer">Подписчик.</param>/// <returns>Объект с механизмом освобождения неуправляемых ресурсов.</returns>/// <exception cref="ArgumentNullException">Подписчик равен null!</exception>publicIDisposableSubscribe(IObserver<Message>observer){if(observer==null){thrownewArgumentNullException(nameof(observer),"Подписчик равен null!");}_observers.Add(observer);returnnewUnsubscriber<Message>(_observers,observer);// Данные класс будет реализован далее.}/// <summary>/// Отправляет уведомление всем подписчикам./// </summary>/// <param name="message">Сообщение, которое будет отправлено всем подписчикам.</param>/// <exception cref="ArgumentNullException">Сообщение равно null!</exception>publicvoidNotify(Messagemessage){if(message==null){thrownewArgumentNullException(nameof(message),"Сообщение равно null!");}foreach(varobserverin_observers){observer.OnNext(message);}}}

Несколько комментариев касательно Unsubscriber: нам необходимо, чтобы помимо подписки на событие, у пользователя была возможность и отписаться от события. В Unsubscriber должен храниться список всех подписчиков и конкретный подписчик, с которым будет происходить взаимодействие.

4️⃣ Теперь давайте реализуем данный класс.

📌Обратите внимание, что он должен реализовывать интерфейсIDisposable, в котором содержится метод Dispose - именно так будет происходить отписка пользователя от уведомлений корпоративного портала.

/// <summary>/// Работает с отписками от уведомлений./// </summary>/// <typeparam name="T">Тип подписчика.</typeparam>publicclassUnsubscriber<T>:IDisposable{/// <summary>/// Список подписчиков./// </summary>privatereadonlyList<IObserver<T>>_observers;/// <summary>/// Подписчик./// </summary>privatereadonlyIObserver<T>_observer;/// <summary>/// Создание экземпляра для отписок от уведомлений с помощью указанных данных./// </summary>/// <param name="observers">Подписчики.</param>/// <param name="observer">Подписчик.</param>/// <exception cref="ArgumentNullException">Подписчики равны null!</exception>publicUnsubscriber(List<IObserver<T>>observers,IObserver<T>observer){if(observers==null||observers.FindIndex(subscriber=>subscriber==null)!=-1){thrownewArgumentNullException(nameof(observers),"Список подписчиков или его элементы равны null!");}if(observer==null){thrownewArgumentNullException(nameof(observer),"Подписчик равен null!");}if(!observers.Contains(observer)){thrownewArgumentNullException(nameof(observer),"Подписчик не найден в списке подписчиков!");}_observer=observer;_observers=observers;}/// <summary>/// Отписка подписчика от уведомлений./// </summary>publicvoidDispose(){if(_observers.Contains(_observer)){_observers.Remove(_observer);}}}

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

Преимущества паттерна Observer: Можно создавать новые классы подписчиков. При этот класс наблюдаемого объекта как-то изменять не нужно.
Недостатки: Подписчики уведомляются в произвольном порядке.


Посредник

Посредник (Mediator) - это поведенческий паттерн проектирования, бгагодаря которому уменьшается связанность множества классов между собой. Это достигается за счет перемещения этих связей в один класс-посредник.
Самое популярное применение посредника в C# коде – это связь нескольких компонентовGUI одной программы.Аналогия из жизни: пилоты не общаются напрямую друг с другом, а через диспетчера.

Давайте попробуем реализовать данный паттерн наследующем примере: пусть у нас есть какая-то IT-компания, в которой есть программист и тимлид. Тимлид не скидывает лично (напрямую) программисту задачу, например, в социальных сетях. Он публикует ее вTFS (Team Foundation Server).

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

В нашем примере посредником (Mediator) будет является TFS между программистом и тимлидом.

Теперь реализуем это в коде.
1️⃣ Создадим интерфейсIMediator - он будет содержать методы, которыми будет обладать класс-посредник. С помощью методаNotify посредник будет уведомлять обе стороны о каком-либо событии.

/// <summary>/// Содержит методы для посредника./// </summary>publicinterfaceIMediator{/// <summary>/// Обрабатывает уведомления./// </summary>/// <param name="worker">Работник.</param>/// <param name="message">Сообщение.</param>voidNotify(Workerworker,stringmessage);}

2️⃣ Теперь реализуем абстрактный класс Worker - это будет наш сотрудник. Он будет хранить посредника - его можно будет установить в методеSetMediator:

/// <summary>/// Работник./// </summary>publicabstractclassWorker{/// <summary>/// Посредник./// </summary>protectedIMediator_mediator;/// <summary>///Устанавливает посредника для работника./// </summary>/// <param name="mediator">Посредник.</param>/// <exception cref="ArgumentNullException">Посредник равен null!</exception>publicvoidSetMediator(IMediatormediator){if(mediator==null){thrownewArgumentNullException(nameof(mediator),"Посредник равен null!");}_mediator=mediator;}}

3️⃣ Теперь создадим первый класс-наследник нашего Worker: это будет класс Programmer. У него будет два метода: начать работу и закончить работу. Причем программисту нельзя давать новую задачу, пока он не завершил предыдущую.

/// <summary>/// Программист./// </summary>publicclassProgrammer:Worker{/// <summary>/// Текст задачи,над которой работает программист./// </summary>privatestring_taskText=string.Empty;/// <summary>/// Получение текста задания./// </summary>publicstringTaskText=>_taskText;/// <summary>/// Начинает работу над задачей./// </summary>/// <param name="taskText">Текст задачи.</param>publicvoidStartWork(stringtaskText){Validator.ValidateStringText(taskText);if(_taskText.Length!=0){if(_mediator!=null){_mediator.Notify(this,"Программист уже работает над задачей!");}return;}_taskText=taskText;if(_mediator!=null){_mediator.Notify(this,$"Программист начал работу над задачей:{taskText}");}}/// <summary>/// Завершает работу над задачей./// </summary>publicvoidFinishWork(){if(_mediator!=null){_mediator.Notify(this,$"Программист завершил работу над задачей:{_taskText}");}_taskText=string.Empty;}}

4️⃣ Вторым классом-наследником будет наш Тимлид. У него будет методGiveTask - выдать задачу.

/// <summary>/// Тимлид./// </summary>publicclassTeamLead:Worker{/// <summary>/// Дать задание./// </summary>/// <param name="taskText">Текст задания.</param>publicvoidGiveTask(stringtaskText){Validator.ValidateStringText(taskText);if(_mediator!=null){_mediator.Notify(this,"Выдаю задачу программисту: "+taskText);}}}

5️⃣ Теперь мы можем написать класс TFS, который будет реализовывать интерфейс посредника IMediator. Класс внутри будет содержать объекты тимлида и программиста, посредниками которых он является. Также у нас будет список задач. С помощью метода AddTask мы можем добавить задачу в список. В конструкторе мы не только инициализируем тимлида и программиста, но и устанавливаем им текущего посредника.

Логика работы метода Notify следующая: если уведомление посреднику отправляет тимлид, то это означает, что он дает задачу сотруднику, значит сотрудник должен приступить к работе. Если уведомление приходит от сотрудника, то это означает, что он выполнил задачу и тимлид дает ему новую задачу: он будет давать новые задачи до тех пор, пока список задач не станет пустым.

/// <summary>/// TFS./// </summary>publicclassTFS:IMediator{/// <summary>/// Программист./// </summary>privateProgrammer_programmer;/// <summary>/// Тимлид./// </summary>privateTeamLead_teamLead;/// <summary>/// Задачи./// </summary>privateList<string>_tasks;/// <summary>/// Создает TFS с помощью указанных данных./// </summary>/// <param name="programmer">Программист.</param>/// <param name="teamLead">Тимлид.</param>/// <exception cref="ArgumentNullException">Программист или тимлид равен null!</exception>publicTFS(Programmerprogrammer,TeamLeadteamLead){if(programmer==null){thrownewArgumentNullException(nameof(programmer),"Программист равен null!");}if(teamLead==null){thrownewArgumentNullException(nameof(teamLead),"Тимлид равен null!");}_programmer=programmer;_teamLead=teamLead;programmer.SetMediator(this);teamLead.SetMediator(this);_tasks=newList<string>();}/// <summary>/// Обрабатывает уведомления./// </summary>/// <param name="worker">Работник.</param>/// <param name="message">Сообщение.</param>publicvoidNotify(Workerworker,stringmessage){if(worker==null){thrownewArgumentNullException(nameof(worker),"Работник равен null!");}Validator.ValidateStringText(message);Console.WriteLine(message);if(workerisProgrammer){if(message.StartsWith("Программист завершил работу над задачей")){if(_tasks.Count!=0){_teamLead.GiveTask(_tasks[0]);_tasks.RemoveAt(0);}}return;}if(workerisTeamLead){_programmer.StartWork(message);}}/// <summary>/// Добавить задачу./// </summary>/// <param name="taskText">Текст задачи.</param>publicvoidAddTask(stringtaskText){Validator.ValidateStringText(taskText);_tasks.Add(taskText);}}

Преимущества паттерна Mediator: Достигается устранение зависимости между компонентами, благодаря чему их можно повторно использовать, более удобным становится взаимодействие между компонентами, также управление компонентами централизовано.
Недостатки: Код посредника может быть очень большим.


Шаблонный метод

Шаблонный метод (Template Method) - это поведенческий паттерн проектирования, который определяет общий алгоритм поведения подклассов. При этом подклассы имеют возможность переопределять части этого алгоритма, не меняя при этом его общей структуры.

✅ Если бы мы не использовали данный паттерн, то нам приходилось бы явно прописывать реализацию алгоритма в каждой подклассе, несмотря на то, что алгоритмы в этих подклассах имеют небольшие различия.

Рассмотрим реализацию шаблонного метода на конкретном примере: пусть у нас будет бухгалтерия, в котором выдают зарплату для сотрудников.
1️⃣ Реализуем класс Worker, содержащий необходимые свойства, которыми будет обладать каждый сотрудник.

/// <summary>/// Сотрудник./// </summary>publicclassWorker{/// <summary>/// Идентификатор./// </summary>publicGuidId{get;set;}/// <summary>/// Имя./// </summary>publicstringFirstName{get;set;}/// <summary>/// Фамилия./// </summary>publicstringLastName{get;set;}/// <summary>/// Отчество./// </summary>publicstringPatronymic{get;set;}/// <summary>/// Должность./// </summary>publicstringPost{get;set;}/// <summary>/// Выплачена ли зарплата./// </summary>publicboolIsSalaryPaid{get;set;}}

2️⃣ Реализуем абстрактный класс бухгалтерии, в которой из полей будет список сотрудников типа Worker, которые работают в конкретной компании и словарь, в котором в зависимости от должности указана зарплата, которую должен получать сотрудник, занимая определенную должность.

Нам необходимо выдать зарплату сотруднику - за это отвечает методGetSalary. Алгоритм выдачи зарплаты следующий:

  1. Сначала вызывается методValidateWorkerId, проверяющий корректности идентификатора сотрудника, которому необходимо выдать зарплату - он не должен быть null.
  2. Далее вызывается методValidateWorkerExistence, проверяющий существование сотрудника с указанным идентификатором.
  3. Затем вызывается методValidatePaidSalary, проверяющий, получал ли работник зарплату ранее, если он ее уже получил, то повторно она выплачиваться не должна.
  4. Последний этап - вызывается методGetCalculationSalary, который расчитывает зарплату в зависимости от должности - он будет виртуальным. Это означает, что классы-наследники могут его переопределить, а могут и не переопределять, поскольку в базовом классе прописана его реализация по умолчанию. Результат метода GetCalculationSalary и будет возвращаться методом GetSalary.

Обратите внимание, что нам нет смысла переопределять методы из первых трех пунктов - неважно, какой компании является бухгалтерия. То есть в абсолютно любой бухгалтерии прежде чем выдать сотруднику зарплату, необходимо выполнить первые три пункта (возможно больше, в примере представлена упрощенная версия, чтобы было более понятно назначение паттерна.

✅ Благодаря шаблонному методу нам не нужно в бухгалтерии каждой компании прописывать выполнение первых трех пунктов, потому что они уже реализованы в базовом классеAccounting.

/// <summary>/// Бухгалтегия./// </summary>publicabstractclassAccounting{/// <summary>/// Список сотрудников для выплаты зарплаты./// </summary>protectedList<Worker>_workers=newList<Worker>();/// <summary>/// Зарплаты сотрудников./// </summary>protectedDictionary<string,decimal>_workersSalary=newDictionary<string,decimal>();/// <summary>/// Создание бухгалтерии./// </summary>publicAccounting(){_workers=newList<Worker>();_workersSalary=newDictionary<string,decimal>();}/// <summary>/// Проверка корректности идентификатора сотрудника./// </summary>/// <exception cref="ArgumentNullException">Идентификатор сотрудника равен null!</exception>protectedvoidValidateWorkerId(Guidid){if(id==null){thrownewArgumentNullException(nameof(id),"Идентификатор сотрудника равен null!");}}/// <summary>/// Проверка наличия сотрудника в базе./// </summary>/// <param name="id">Идентификатор сотрудника, которого необходимо проверить.</param>protectedvoidValidateWorkerExistence(Guidid){if(_workers.FirstOrDefault(worker=>worker.Id==id)==null){thrownewArgumentNullException(nameof(id),"Сотрудник с указанным идентификатором не найден!");}}/// <summary>/// Проверка того, была ли выплачена зарплата работнику ранее./// </summary>/// <param name="id">Идентификатор сотрудника.</param>/// <exception cref="ArgumentOutOfRangeException">Данному сотруднику уже выплачена зарплата!</exception>protectedvoidValidatePaidSalary(Guidid){if(_workers.FirstOrDefault(worker=>worker.Id==id).IsSalaryPaid){thrownewArgumentOutOfRangeException("Данному сотруднику уже выплачена зарплата!");}}/// <summary>/// Получение расчитанной зарплаты сотрудника./// </summary>/// <param name="id">Идентификатор сотрудника.</param>/// <returns>Расчитанная зарплата.</returns>protectedvirtualdecimalGetCalculationSalary(Guidid)=>_workersSalary[_workers.FirstOrDefault(worker=>worker.Id==id).Post];/// <summary>/// Выплата зарплаты сотруднику./// </summary>/// <param name="id">Идентификатор сотрудника, которого необходимо проверить.</param>publicdecimalGetSalary(Guidid){ValidateWorkerId(id);ValidateWorkerExistence(id);ValidatePaidSalary(id);_workers.FirstOrDefault(worker=>worker.Id==id).IsSalaryPaid=true;returnGetCalculationSalary(id);}/// <summary>/// Добавление сотрудника в базу бухгалтерии./// </summary>/// <param name="firstName">Имя.</param>/// <param name="lastName">Фамилия.</param>/// <param name="patronymic">Отчество.</param>/// <param name="post">Должность.</param>/// <exception cref="ArgumentOutOfRangeException">Сотрудник с указанными данными уже есть в базе!</exception>/// <exception cref="ArgumentNullException">Указанная должность в бухгалтерии не найдена!</exception>publicGuidAddWorker(stringfirstName,stringlastName,stringpatronymic,stringpost){Validator.ValidateStringText(firstName);Validator.ValidateStringText(lastName);Validator.ValidateStringText(patronymic);Validator.ValidateStringText(post);if(!_workersSalary.ContainsKey(post)){thrownewArgumentNullException("Указанная должность в бухгалтерии не найдена!");}varid=Guid.NewGuid();_workers.Add(newWorker{Id=id,FirstName=firstName,LastName=lastName,Patronymic=patronymic,Post=post,IsSalaryPaid=false});returnid;}/// <summary>/// Получение сотрудника по идентификатору./// </summary>/// <param name="id">Идентификатор сотрудника.</param>/// <returns>Сотрудника.</returns>publicWorkerGetWorker(Guidid){ValidateWorkerId(id);ValidateWorkerExistence(id);return_workers.First(worker=>worker.Id==id);}/// <summary>/// Удаляет сотрудника из базы./// </summary>/// <param name="id">Идентификатор сотрудника, которого необходимо удалить.</param>/// <exception cref="ArgumentNullException">Сотрудник с указанным идентификатором не найден в базе!</exception>publicvoidDeleteWorker(Guidid){ValidateWorkerExistence(id);_workers.Remove(_workers.FirstOrDefault(worker=>worker.Id==id));}}

3️⃣ Теперь реализуем первый класс-наследник базового класса Accounting - пусть это будет бухгалтерияСбера (SberAccounting). В Сбере сотрудникам помимо заработной платы выдается еще и фиксированная премия. Это значит, что нам необходимо изменить логику расчета зарплаты для сотрудников Сбера:

/// <summary>/// Бухгалтерия Сбера./// </summary>publicclassSberAccounting:Accounting{/// <summary>/// Премия./// </summary>privatedecimal_prize=5000;/// <summary>/// Создание бухгалтерии Сбера./// </summary>publicSberAccounting(){_workers=newList<Worker>();_workersSalary=newDictionary<string,decimal>{{"Менеджер",30000},{"Программист",100000}};}/// <summary>/// Получение расчитанной зарплаты сотрудника./// </summary>/// <param name="id">Идентификатор сотрудника.</param>/// <returns>Расчитанная зарплата.</returns>protectedoverridedecimalGetCalculationSalary(Guidid)=>base.GetCalculationSalary(id)+_prize;}

4️⃣ Реализуем второй класс-наследник: бухгалтерия Озона (OzonAccounting). У Озона у сотрудников нет премии. Все сотрудники добираются на работу на корпоративном такси, соответственно, ежемесячная плата за корпоративное такси вычитается из их заработной платы. Это означает, что нам также придется переопределить расчет зарплаты и здесь:

/// <summary>/// Бухгалтерия Ozon./// </summary>publicclassOzonAccounting:Accounting{/// <summary>/// Плата за корпоративное такси в месяц для поезди от дома до работы и обратно./// </summary>privatedecimal_taxiCostByMonth=15000;/// <summary>/// Создание бухгалтерии Озона./// </summary>publicOzonAccounting(){_workers=newList<Worker>();_workersSalary=newDictionary<string,decimal>{{"Менеджер",40000},{"Программист",90000}};}/// <summary>/// Получение расчитанной зарплаты сотрудника./// </summary>/// <param name="id">Идентификатор сотрудника.</param>/// <returns>Расчитанная зарплата.</returns>protectedoverridedecimalGetCalculationSalary(Guidid)=>base.GetCalculationSalary(id)-_taxiCostByMonth;}

Преимущества паттерна Template Method: Сокращение дублирования кода
Недостатки: По мере роста шагов в шаблонном методе возникают проблемы с его дальнейшей поддержкой.



[8]ページ先頭

©2009-2025 Movatter.jp