https://github.com/stacenko-developer/patterns
Паттерны проектирования с примерами на C#
https://github.com/stacenko-developer/patterns
abstract-factory abstract-factory-pattern adapter adapter-pattern csharp decorator decorator-pattern dependency-injection dependency-injection-pattern iterator iterator-pattern mediator mediator-pattern observer observer-pattern patterns singleton singleton-pattern
Last synced: 10 months ago
JSON representation
Паттерны проектирования с примерами на C#
- Host: GitHub
- URL: https://github.com/stacenko-developer/patterns
- Owner: stacenko-developer
- License: mit
- Created: 2023-06-12T13:38:04.000Z (almost 3 years ago)
- Default Branch: master
- Last Pushed: 2023-06-28T12:51:10.000Z (almost 3 years ago)
- Last Synced: 2025-04-09T16:14:00.932Z (about 1 year ago)
- Topics: abstract-factory, abstract-factory-pattern, adapter, adapter-pattern, csharp, decorator, decorator-pattern, dependency-injection, dependency-injection-pattern, iterator, iterator-pattern, mediator, mediator-pattern, observer, observer-pattern, patterns, singleton, singleton-pattern
- Language: C#
- Homepage:
- Size: 875 KB
- Stars: 18
- Watchers: 1
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Паттерны проектирования с примерами на C#
В данном репозитории содержатся реализации паттернов на языке программирования C#. Ниже вы можете ознакомиться с описанием паттернов, их назначением, а также преимуществами и недостатками.
После изучения теории можете переходить в код - он хорошо задокументирован, поэтому разобраться в нем не составит труда.
Помимо документации в данном файле будет пошагово описана реализация каждого паттерна, которыцй есть в репозитории.
### В папке каждого паттерна содержатся:
* его реализация в библиотеке классов;
* демонстрация работы в консольном приложении;
* [тестирование](https://github.com/stacenko-developer/UnitTests "тестирование") методов классов и проверка корректности реализации паттерна
## Оглавление
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).
:one: Для начала создадим два абстрактных класса: __WorkingCar__ (транспорт для нашего сотрудника) и __WorkingDevice__ (устройство, на котором наш сотрудник будет работать). Начнем с автомобиля: у него есть модель, цена и год выпуска. Помимо этого у нас будет абстрактный метод получения стоимости налога:
```C#
///
/// Рабочий автомобиль.
///
public abstract class WorkingCar
{
///
/// Модель автомобиля.
///
protected string _model;
///
/// Цена.
///
protected int _price;
///
/// Год выпуска.
///
protected int _releaseYear;
///
/// Получить стоимость налога.
///
/// Налог.
public abstract int GetTax();
}
```
Аналогично реализовываем абстрактный класс рабочего устройства, у коготого есть цена и модель. В данном классе тоже будет абстрактный метод, который будет расчитывать стоимость дополнительных аксессуаров (например, зарядка, мышка и так далее).
```C#
///
/// Рабочее устройство.
///
public abstract class WorkingDevice
{
///
/// Модель.
///
protected string _model;
///
/// Цена.
///
protected int _price;
///
/// Получить стоимость дополнительных аксессуаров.
///
/// Стоимость дополнительных аксессуаров.
public abstract int GetAccessoriesCost();
}
```
:two: После этого нам необходимо создать __классы наследники__: у WorkingCar классами-наследниками будут LADA и BMW, у WorkingDevice - Laptop и Computer. В данных классах наследникам мы реализуем методы, которые были в наших абстрактных классах.
:three: Теперь нам необходимо реализовать __интерфейс__ фабрики по созданию сотрудника, в котором будет 2 метода: создание объекта __рабочего устройства__ и создание объекта __рабочего автомобиля__.
```C#
///
/// Фабрика сотрудника.
///
public interface IWorkerFactory
{
///
/// Создание рабочего устройства для сотрудника.
///
/// Рабочее устройство.
WorkingDevice CreateWorkingDevice();
///
/// Создание рабочего автомобиля.
///
/// Рабочий автомобиль.
WorkingCar CreateWorkingCar();
}
```
Далее нам необходимо создать фабрику по созданию программиста и директора, реализующую интерфейс __IWorkerFactory__. Рассмотрим пример фабрики по созданию директора. У нас метод __CreateWorkingCar()__ возвращает объект автомобиля марки __BMW__ и метод __CreateWorkingDevice()__ объект __компьютера__ в качестве рабочего устройства.
```C#
///
/// Фабрика директора.
///
public class DirectorFactory : IWorkerFactory
{
///
/// Создание рабочего автомобиля.
///
/// Рабочий автомобиль.
public WorkingCar CreateWorkingCar() => new BMW();
///
/// Создание рабочего устройства.
///
/// Рабочее устройство.
public WorkingDevice CreateWorkingDevice() => new Computer();
}
```
> Аналогично реализована фабрика по созданию объекта программиста: метод __CreateWorkingCar()__ возвращает объект Lada и метод __CreateWorkingDevice()__ возвращает ноутбук.
:four: Теперь у нас есть все условия для того, чтобы создать класс самого сотрудника. Как было сказано ранее, у сотрудника есть служебный автомобиль и рабочее устройство. Добавим их в поля сотрудника.
В конструкторе в качестве параметра мы будем принимать интерфейс __IWorkerFactory__. Напомню, его у нас реализуют ProgrammerFactory и DirectorFactory.
__В итоге у нас получился следующий код:__
```C#
///
/// Сотрудник.
///
public class Worker
{
///
/// Рабочий автомобиль.
///
private WorkingCar _workingCar;
///
/// Рабочее устройство.
///
private WorkingDevice _workingDevice;
///
/// Создание сотрудника с помощью указанных параметров.
///
/// Фабрика сотрудника.
/// Фабрика сотрудника равна null!
public Worker(IWorkerFactory workerFactory)
{
if (workerFactory == null)
{
throw new ArgumentNullException(nameof(workerFactory),
"Фабрика для создания сотрудника равна null!");
}
_workingCar = workerFactory.CreateWorkingCar();
_workingDevice = workerFactory.CreateWorkingDevice();
}
///
/// Получить стоимость налога за автомобиль.
///
/// Стоимость налога.
public int GetTax() => _workingCar.GetTax();
///
/// Получить стоимость дополнительных аксессуаров для рабочего устройства.
///
/// Стоимость дополнительных аксессуаров.
public int GetAccessoriesCost() => _workingDevice.GetAccessoriesCost();
}
```
:white_check_mark: __Преимущества паттерна Abstract Factory__: упрощение добавления новых продуктов, их сочетаемость, а также избавление кода от привязки к конкретным классам продуктов.
:x: __Недостатки__: возможное усложнение кода из-за создания огромного количества вспомогательных классов.
____
### Одиночка
__Одиночка (Singleton)__ - это __паттерн__, который позволяет гарантировать, что класс имеет только один экземпляр, обеспечивая при этом глобальную точку доступа к этому экземпляру.
Модель Singleton решает две проблемы одновременно, __нарушая принцип единой ответственности__:
:one: Гарантия того, что класс имеет только один экземпляр. Это может пригодиться, когда необходимо контролировать доступ к какому-либо общему ресурсу, например, к базе данных или файлу.
:two: Предоставление глобальной точки доступа к этому экземпляру.
> Шаблон требует специальной обработки в __многопоточной среде__, чтобы несколько потоков не создавали экземпляр класса несколько раз.
Теперь перейдем к реализации данного __паттерна__. Пусть у нас есть какой-то сайт, в котором есть разделы. Раздел будет иметь следующие свойства: название и код (идентификатор).
```C#
///
/// Раздел.
///
public class Section
{
///
/// Название раздела.
///
public string Name { get; set; }
///
/// Код раздела.
///
public string Code { get; set; }
}
```
Теперь мы хотим создать экземпляр класса базы данных __SectionDatabase__, в которой будут храниться наши разделы.
:one: В данном классе создаем статическое поле, имеющее тот же тип, что и сам класс: SectionDatabase. По умолчанию он будет равен null, так как еще ни разу не был создан экземпляр данного класса.
:two: Создаем заблокированный объект, который мы будем использовать для синхронизации. Это означает, что в критическую область кода потоки будут заходить по очереди.
:three: Создаем список разделов, в который мы будем добавлять созданные разделы.
:four: Создаем защищенный конструктор. Это необходимо для того, чтобы у нас не было возможности вызвать публичный конструктор, так как в этом случае мы не сможем контролировать количество созданных экземпляров класса SectionDatabase.
:five: Добавляем публичный метод __Initialize__. Его назначение - инициализировать объект базы данных, а также проверять: если объект базы данных уже был создан, то необходимо возврать уже ранее созданный экземпляр. Также не забываем про использование синхронизации для критической секции.
Теперь посмотрим на получившийся результат (код представлен в упрощенном виде, полная реализация доступна в репозитории):
```C#
///
/// База данных разделов. Реализация паттерна Singleton.
///
public class SectionDatabase
{
///
/// База данных разделов.
///
private static SectionDatabase Database = null;
///
/// Заблокированный объект.
/// Служит для синхронизации потоков.
///
private static object LockObject = new object();
///
/// Список разделов.
///
private List _sectionsList;
///
/// Создает хранилище данных разделов.
///
protected SectionDatabase()
{
}
///
/// Инициализация хранилища данных разделов.
///
/// Хранилище данных разделов.
public static SectionDatabase Initialize()
{
if (Database == null)
{
lock (LockObject)
{
if (Database == null)
{
Database = new SectionDatabase();
}
}
}
return Database;
}
}
```
:white_check_mark: __Преимущества паттерна Singleton__: класс гарантированно имеет только один экземпляр и не более, у нас есть точка доступа к единственному экземпляру (в нашем случае это метод Initialize).
:x: __Недостатки__: нарушение принципа единой ответственности (Single Responsibility Principle), требуется особая обработка в многопоточной среде.
___
### Строитель
__Строитель (Builder)__ - это порождающий паттерн проектирования, который позволяет разделить создание экземпляра класса на несколько шагов. Данный паттерн может быть полезен, когда созданние какого-либо экземпляра класса требует много разных этапов и когда также важно, в каком порядке эти этапы будут выполняться.
> :x: Проблема заключается в том, что у нас может быть какой-то сложный объект и его создание может привести к огромному количеству кода в конструкторе
Паттерн Builder (Строитель) состоить из двух участников:
* __Строитель (Builder)__ – предоставляет методы для сборки частей экземпляра класса;
* __Распорядитель (Director)__ – определяет саму стратегию того, как будет происходить сборка: определяет, в каком порядке будут вызываться методы Строителя.
Реализуем данный паттерн на основе примера: у нас есть завод по производству компьютеров. У нас есть разработчики компьютеров и директор.
:one: Создадим класс компьютера, для простоты он будет содержать всего 4 характеристики:
```C#
///
/// Содержит методы для разработчика компьютеров.
///
public interface IComputerDeveloper
{
///
/// Установка процессора.
///
void SetProcessor();
///
/// Установка оперативной памяти.
///
void SetRandomAccessMemory();
///
/// Установка операционной системы.
///
void SetOperationSystem();
///
/// Получение компьютера.
///
/// Компьютер.
Computer GetComputer();
}
```
:two: Теперь создадим интерфейс __IComputerDeveloper__, которые будет содержать методы разработчика компьютеров:
```C#
///
/// Содержит методы для разработчика компьютеров.
///
public interface IComputerDeveloper
{
///
/// Установка процессора.
///
void SetProcessor();
///
/// Установка оперативной памяти.
///
void SetRandomAccessMemory();
///
/// Установка операционной системы.
///
void SetOperationSystem();
///
/// Получение компьютера.
///
/// Компьютер.
Computer GetComputer();
}
```
:three: Затем создадим классы HPComputerDeveloper (разработчик компьютеров HP) и DELLComputerDeveloper (Разработчик компьютеров DELL), в которых будет реализован интерфейс IComputerDeveloper. Рассмотрим реализация класса HPComputerDeveloper, класс DELLComputerDeveloper реализован аналогично.
```C#
///
/// Разработчик компьютеров HP.
///
public class HPComputerDeveloper : IComputerDeveloper
{
///
/// Компьютер.
///
private Computer _computer;
///
/// Модель.
///
private string _model = "HP";
///
/// Процессор.
///
private string _processor = "Intel Core i5-7400";
///
/// Количество оперативной памяти.
///
private int _randomAccessMemoryCount = 8;
///
/// Операционная система.
///
private string _operationSystem = "Windows 10 Pro";
///
/// Создание разработчика компьютеров HP.
///
public HPComputerDeveloper()
{
_computer = new Computer();
_computer.Model = _model;
}
///
/// Установка процессора.
///
public void SetProcessor()
{
_computer.Processor = _processor;
}
///
/// Установка оперативной памяти.
///
public void SetRandomAccessMemory()
{
_computer.RandomAccessMemory = _randomAccessMemoryCount;
}
///
/// Установка операционной системы.
///
public void SetOperationSystem()
{
_computer.OperationSystem = _operationSystem;
}
///
/// Получение компьютера.
///
/// Компьютер.
public Computer GetComputer() => _computer;
}
```
:four: Теперь создадим класс Director, который будет иметь поле IComputerDeveloper, то есть, он будет принимать в конструкторе одного из разработчиков компьютеров и в зависимости от разработчика создавать определенный компьютер.
```C#
///
/// Директор.
///
public class Director
{
///
/// Разработчик компьютеров.
///
private IComputerDeveloper _computerDeveloper;
///
/// Создание директора с помощью указанных параметров.
///
/// Разработчик компьютеров.
/// Разработчик компьютеров равен null!
public Director(IComputerDeveloper computerDeveloper)
{
if (computerDeveloper == null)
{
throw new ArgumentNullException(nameof(computerDeveloper), "Разработчик компьютеров равен null!");
}
_computerDeveloper = computerDeveloper;
}
///
/// Создание полноценного компьютера.
///
/// Созданный компьютер.
public Computer CreateFullComputer()
{
_computerDeveloper.SetProcessor();
_computerDeveloper.SetRandomAccessMemory();
_computerDeveloper.SetOperationSystem();
return _computerDeveloper.GetComputer();
}
///
/// Создание компьютера без операционной системы.
///
/// Созданный компьютер.
public Computer CreateComputerWithoutOperationSystem()
{
_computerDeveloper.SetProcessor();
_computerDeveloper.SetRandomAccessMemory();
return _computerDeveloper.GetComputer();
}
}
```
> За счет того, что мы разбили процесс создания компьютера на отдельный шаги, мы можем создавать разные объекты, например, полноценный компьютер или компьютер без операционной системы.
:white_check_mark: __Преимущества паттерна Builder__: контроль за этапами создания экземпляра класса, в зависимости от этапов можно получить различные объекты.
:x: __Недостатки__: жесткая связка конкретного Builder и продукта, который он создает.
___
### Прототип
__Прототип (Prototype)__ - это такой паттерн, который используется для создания новых объектов с помощью клонирования существующих. Для того, чтобы определение паттерна было более понятно, приведу конкретный пример.
Предположим, что у нас есть биржа фриланса, где есть заказчики и исполнители.
:one: Для начала создадим абстрактный класс User - пользователь биржи фриланса. У пользователя будут идентификатор, ФИО и абстрактный метод Clone. Это означает, что мы обязательно должны реализовать его в классе-наследнике.
```C#
///
/// Пользователь
///
public abstract class User
{
///
/// Идентификатор.
///
public int Id { get; set; }
///
/// Имя.
///
public string FirstName { get; set; }
///
/// Фамилия.
///
public string LastName { get; set; }
///
/// Отчество.
///
public string Patronymic { get; set; }
///
/// Клонирование пользователя.
///
/// Нового пользователя.
public abstract User Clone();
///
/// Строковое представления объекта пользователя.
///
/// Данные пользователя в виде строки.
public override string ToString() => $"Идентификатор - {Id} Имя - {FirstName} Фамилия - {LastName} Отчество - {Patronymic}";
}
```
:two: Теперь реализуем класс-наследник исполнителя: в него не будем добавлять дополнительные свойства. Реализация метода Clone будет выглядеть так:
```C#
///
/// Исполнитель.
///
public class Executor : User
{
///
/// Клонирование пользователя.
///
/// Нового пользователя.
public override User Clone() => (Executor)MemberwiseClone();
///
/// Строковое представления объекта исполнителя.
///
/// Данные исполнителя в виде строки.
public override string ToString() => $"Данные исполнителя: {base.ToString()}";
}
```
> В данном случае мы можем выполнить неполное (поверхностное) копирование. Это подходит тогда, когда у нас все поля Executor являются значимыми типами (исключение: string).
Теперь рассмотрим второй класс-наследник: Customer.
```C#
///
/// Заказчик
///
public class Customer : User
{
///
/// Паспорт.
///
public Passport Passport { get; set; }
///
/// Создание пользователя.
///
public Customer()
{
Passport = new Passport();
}
///
/// Клонирование пользователя.
///
/// Нового пользователя.
public override User Clone()
{
var customer = (Customer)MemberwiseClone();
customer.Passport = new Passport();
customer.Passport.Series = Passport.Series;
customer.Passport.Number = Passport.Number;
customer.Passport.ReceiptPlace = Passport.ReceiptPlace;
return customer;
}
///
/// Строковое представления объекта заказчика.
///
/// Данные заказчика в виде строки.
public override string ToString() => $"Данные заказчика: {base.ToString()} {Passport}";
}
```
> Здесь у нас уже присутствует класс Passport - это ссылочный тип, соответсвенно, мы не можем использовать неполное копирование. Если мы будем использовать неполное копирование, то у нас будет два заказчика ссылаться на один и тот же объект паспортных данных. Поэтому нам придется создать новый объект паспорта и вручную его проинициализировать.
Также, чтобы убедиться в том, что у нас создаются два абсолютно разных объекта, которые не ссылаются на одни и те же поля, рекомендую запустить тесты в проекте PrototypeTests, где происходит данная проверка.
:white_check_mark: __Преимущества паттерна Prototype__: клонирование объектов без привязки к конкретным классам, сокращение кода инициализации экземплятор классов.
:x: __Недостатки__: Проблемы с клонированием составных объектов, то есть, тех объектов, которые внутри содержат другие объекты.
___
### Фабричный метод
__Фабричный метод (Factory Method)__ - это порождающий паттерн проектирования, который определяет интерфейс для создания объектов определенного класса, но именно в подклассах принимается решение о типе объекта, который будет создан.
> :white_check_mark: Фабричный метод будет полезен, если мы заранее не знаем, объекты каких типов мы хотим создать.
У нас есть следующие участники:
* базовый класс какого-либо продукта;
* наследники базового класса продукта;
* базовый класс создателя этого продукта;
* создатели-наследники базового класса создателя, которые созданию продукты-наследники базового класса продукта
:one: Реализуем паттерн на примере создания телефонов. Пусть у нас будет базовый класс __Phone__, содержащий следующие свойства:
```C#
///
/// Телефон.
///
public abstract class Phone
{
///
/// Цена.
///
public decimal Price { get; set; }
///
/// Модель.
///
public string Model { get; set; } = string.Empty;
///
/// Процессор.
///
public string Processor { get; set; } = string.Empty;
///
/// Оперативная память.
///
public int RandomAccessMemory { get; set; }
///
/// Строковое представления объекта телефона.
///
/// Данные телефона в виде строки.
public override string ToString() => $"Цена = {Price} Модель = {Model} Процессор = {Processor} Оперативная память = {RandomAccessMemory}";
}
```
:two: Создадим два класса наследника: Nokia и Samsung, в них добавим по одному дополнительному свойству.
```C#
///
/// Нокиа.
///
public class Nokia : Phone
{
///
/// Работает ли батарея.
///
public bool IsBatteryWork { get; set; }
///
/// Строковое представления объекта Нокиа.
///
/// Данные Нокиа в виде строки.
public override string ToString() => $"Нокиа: {base.ToString()} Работает ли батарея = {IsBatteryWork}";
}
///
/// Самсунг.
///
public class Samsung : Phone
{
///
/// Влючена ли сейчас фронтальная камера.
///
public bool IsFrontCamera { get; set; }
///
/// Строковое представления объекта Самсунга.
///
/// Данные Самсунга в виде строки.
public override string ToString() => $"Самсунг: {base.ToString()} Включена ли фронтальная камера = {IsFrontCamera}";
}
```
:three: Создадим интерфейс создателя телефонов IPhoneDeveloper:
```C#
///
/// Содержит методы для рабработчика телефонов.
///
public interface IPhoneDeveloper
{
///
/// Создание телефона.
///
/// Телефон.
Phone CreatePhone();
}
```
:four: Создадим разработчиков телефонов Нокиа и Самсунга, реализующих интерфейсы __IPhoneDeveloper__:
```C#
///
/// Разработчик телефонов фирмы Нокиа.
///
public class NokiaDeveloper : IPhoneDeveloper
{
///
/// Создание телефона.
///
/// Телефон.
public Phone CreatePhone() => new Nokia();
}
///
/// Разработчик телефонов фирмы Самсунг.
///
public class SamsungDeveloper
{
///
/// Создание телефона.
///
/// Телефон.
public Phone CreatePhone() => new Samsung();
}
```
:white_check_mark: __Преимущества паттерна Factory Method__: упрощение поддержки кода, так как продукт создается в отдельном классе.
:x: __Недостатки__: Значительное увеличение кода, так как для каждого класса продукта необходимо будет добавлять класс-создатель, который будет создавать данный продукт.
___
## Структурные паттерны
__Структурные паттерны__ (Structural) - цель их применения заключается в том, что благодаря им вы можете совмещать и сочетать сущности вместе.
___
### Адаптер
Адаптер (Adapter) - это структурный шаблон проектирования, который используется для организации использования методов объекта, недоступного для модификации, через специально созданный интерфейс.
> __Проблема__: у нас уже есть конкретный класс и нужно, чтобы этот класс реализовывал определенный интерфейс, при этом сам класс менять нельзя.
:white_check_mark: Решение: мы пишем класс, который будет реализовывать необходимый интерфейс, затем мы наследуем наш класс от того класса, который нам нужно изменить без прямого вмешательства в тот класс.
Предположим, что у нас есть офис, в котором работают сотрудники. У офиса есть название, адрес:
```C#
///
/// Офис.
///
public class Office
{
///
/// Название.
///
protected string _name;
///
/// Адрес.
///
protected string _address;
///
/// Создает офис с помощью указанных параметров.
///
/// Название офиса.
/// Адрес офиса.
public Office(string name, string address)
{
Validator.ValidateStringText(name);
Validator.ValidateStringText(address);
_name = name;
_address = address;
}
}
```
Теперь нам необходимо добавить метод "переезда офиса". То есть метод, который устанавливает новое значение для адреса. Не будем забывать, что существующий класс нельзя модифицировать. Решим проблему с помощью паттерна Адаптер:
:one: Создадим интерфейс IMovable, в котором будет один метод Move, принимающий новый адрес офиса.
```C#
///
/// Содержит методы, связанные с переездом офиса.
///
public interface IMovable
{
///
/// Изменение адреса офиса.
///
/// Новый адрес.
void Move(string newAddress);
}
```
:two: Создадим класс-наследник нашего базового класса Office, пусть это будет офис какой-то конкретной компании (например, Норбит). В данном классе мы реализуем интерфейс __IMovable__.
```C#
///
/// Офис Норбит. Реализация паттерна Адаптер.
///
public class NrbOffice : Office, IMovable
{
///
/// Создает офис Норбит с помощью указанных параметров.
///
/// Название офиса.
/// Адрес офиса.
public NrbOffice(string name, string address)
: base(name, address)
{
}
///
/// Изменение адреса офиса.
///
/// Новый адрес.
public void Move(string newAddress)
{
Validator.ValidateStringText(newAddress);
_address = newAddress;
}
}
```
:white_check_mark: __Преимущества паттерна Adapter__: возможность отделения интерфейса или кода преобразования данных от основной бизнес-логики программы.
:x: __Недостатки__: общая сложность кода увеличивается.
___
### Декоратор
__Декоратор (Decorator)__ - это паттерн, который позволяет динамически подключать к объекту дополнительную функциональность, оборачивая объект в обертки.
Для того, чтобы определить какой-либо новый функционал, как правило, мы прибегаем к наследованию. Декораторы в отличие от наследования позволяют динамически в процессе выполнения определять новые возможности у объектов.
> Можно использовать несколько разных обёрток одновременно и вы получите бъединённое поведение сразу всех обёрток.
Давайте теперь реализуем данный паттерн. Пусть у нас также будут сотрудники какой-то компании. У нас есть __задача__: отфильтровать сотрудника по определенному признаку.
:one: Для начала создадим абстрактный класс __WorkersFilter__, в котором будет метод __GetFiltratedList()__, который будет возвращать нам отфильтрованный список сотрудников.
```C#
///
/// Фильтр.
///
public abstract class WorkersFilter
{
///
/// Получение отфильтрованного списка.
///
/// Отфильтрованный список.
public abstract List GetFiltratedList();
}
```
:two: Создаем класс-наследник NorbitWorkersFilter, базовый класс у которого WorkersFilter. В наследнике мы реализуем логику метода GetFiltratedList(). То есть текущий фильтр будет возвращать коллекцию, у всех сотрудников которой значение свойства __Organization__ равно __Norbit__.
```C#
///
/// Фильтр сотрудников.
///
public class NorbitWorkersFilter : WorkersFilter
{
///
/// Сотрудники.
///
protected static List Workers;
///
/// Название организации по умолчанию.
///
private string _defaultOrganization = "Норбит";
///
/// Создание фильтра сотрудников с помощью указанных параметров.
///
/// Список сотрудников.
/// Список сотрудников или его элементы равны null!
public NorbitWorkersFilter(List workers)
{
if (workers == null || workers.FindIndex(worker => worker == null) != -1)
{
throw new ArgumentNullException(nameof(workers), "Список сотрудников или его элементы равны null!");
}
Workers = workers;
}
///
/// Получение отфильтрованного списка сотрудников.
///
/// Отфильтрованный список сотрудников.
public override List GetFiltratedList() => Workers
.Where(worker => worker.Organization == _defaultOrganization)
.ToList();
}
```
:three: Создаем класс дополнительного условия фильтрации, который будет классом-наследником класс NorbitWorkersFilter. Он будет создаваться с помощью конструктора, который будет приниматт объект типа NorbitWorkersFilter.
```C#
///
/// Дополнительное условие фильтрации.
///
public class AdditionalFilteringCondition : NorbitWorkersFilter
{
///
/// Фильтр сотрудников Норбит.
///
protected NorbitWorkersFilter _filter;
///
/// Создает дополнительное условие фильтрации с помощью указанных параметров.
///
/// Фильтр сотрудников Норбит.
public AdditionalFilteringCondition(NorbitWorkersFilter filter)
: base(Workers)
{
if (filter == null)
{
throw new ArgumentNullException(nameof(filter), "Фильтр равен null!");
}
_filter = filter;
Workers = base.GetFiltratedList();
}
}
```
> Перед тем, как список будет отфильтрован дополнительным условием, он сначала будет отфильтрован фильтров базового метода с помощью __base.GetFiltratedList()__
:four: Добавим классы, которые будут фильтровать сотрудников по возрасту и должности.
```C#
///
/// Фильтр сотрудников по возрасту.
///
public class AgeWorkersFilter : AdditionalFilteringCondition
{
///
/// Минимальное допустимое значение возраста.
///
private int _defaultMinCorrectAge = 25;
///
/// Создание фильтра сотрудников по возрасту с помощью указанных параметров.
///
/// Базовый фильтр сотрудников Норбит.
public AgeWorkersFilter(NorbitWorkersFilter filter)
: base(filter)
{
Workers = filter.GetFiltratedList();
}
///
/// Получение отфильтрованного списка сотрудников.
///
/// Отфильтрованный список сотрудников.
public override List GetFiltratedList() => Workers
.Where(worker => worker.Age >= _defaultMinCorrectAge)
.ToList();
}
```
> По такому же принципу реализован класс PostWorkersFilter. Для подробного ознакомления рекомендую перейти в репозиторий. Также с помощью консольного приложения вы можете наблюдать снижения количества сотрудников в коллекции по мере добавления к базовому фильтру сотрудников дополнительный оберток.
:white_check_mark: __Преимущества паттерна Decorator__: возможность добавлять или удалять функционал из экземпляра класса во время выполнения, благодаря оберткам объединить несколько возможных вариантов поведения объекта.
:x: __Недостатки__: в результате получается большое число мелких объектов, которые друг на друга похожи и отличаются способом взаимосвязи.
___
### Компоновщик
__Компоновщик (Composite)__ - это структурный паттерн проектирования, который используется, когда объекты должны быть реализованы в виде древовидной структуры и когда клиенты аналогично управляют как целыми объектами, так и составными частями.
> :white_check_mark: Реализуем данный паттерн на примере файловой системы.
:one: Создадим абстрактный класс компонента файловой системы, единственное поле у которого будет название компонента. Мы также можем добавлять в компонент другие компоненты и аналогично удалять их.
```C#
///
/// Компонент файловой системы.
///
public abstract class FileSystemComponent
{
///
/// Название.
///
protected string _name;
///
/// Создание компонента файловой системы с помощью указанных параметров.
///
/// Название компонента файловой системы.
public FileSystemComponent(string name)
{
Validator.ValidateStringText(name);
_name = name;
}
///
/// Проверка корректности компонента.
///
/// Компонент, который необходимо проверить.
/// Компонент равен null!
protected void ValidateComponent(FileSystemComponent component)
{
if (component == null)
{
throw new ArgumentNullException(nameof(component), "Компонент равен null!");
}
}
///
/// Добавление компонента.
///
/// Компонент, который необходимо добавить.
public virtual void Add(FileSystemComponent component)
{
ValidateComponent(component);
}
///
/// Добавление компонента.
///
/// Компонент, который необходимо добавить.
public virtual void Remove(FileSystemComponent component)
{
ValidateComponent(component);
}
///
/// Строковое преставление объекта компонента файловой системы.
///
/// Данные объекта компонента файловой системы в виде строки.
public override string ToString() => _name;
}
```
:two: Создадим первый класс-наследник компонента файловой системы: файл. У файла не будут переопределены методы добавления и удаления - базовая реализация нас вполне устраивает, поскольку мы не может добавлять в файл другие файлы.
```C#
///
/// Файл.
///
public class File : FileSystemComponent
{
///
/// Создание файла с помощью указанных параметров.
///
/// Название файла.
public File(string name)
: base(name)
{
}
}
```
:three: Добавим второй класс-наследник компонента файловой системы: папка. У папки будет список компонентов файловой системы, поскольку в папку мы уже можем добавить как другие файлы, так и другие папки. Также будут переопределены методы добавления и удаления компонентов файловой системы из директории.
```C#
///
/// Папка.
///
public class Directory : FileSystemComponent
{
///
/// Список компонентов файловой системы, которые находятся в папке.
///
private List _components = new List();
///
/// Создает папку с помощью указанных параметров.
///
/// Название папки.
public Directory(string name)
: base(name)
{
}
///
/// Добавление компонента.
///
/// Компонент, который необходимо добавить.
public override void Add(FileSystemComponent component)
{
ValidateComponent(component);
_components.Add(component);
}
///
/// Добавление компонента.
///
/// Компонент, который необходимо добавить.
/// Указанный компонент файловой системы отсутствует в директории!
public override void Remove(FileSystemComponent component)
{
ValidateComponent(component);
if (!_components.Contains(component))
{
throw new ArgumentNullException("Указанный компонент файловой системы отсутствует в директории!");
}
_components.Remove(component);
}
///
/// Строковое преставление объекта компонента файловой системы.
///
/// Данные объекта компонента файловой системы в виде строки.
public override string ToString() => $"{_name}: {string.Join("=>", _components)}{Environment.NewLine}";
}
```
:white_check_mark: __Преимущества паттерна Composite__: упрощение работы с деревом компонентов, также более удобно добавлять в программу новые компоненты.
:x: __Недостатки__: слишком общий интерфейс для классов, функциональность которых может сильно отличаться.
___
### Фасад
__Фасад (Facade)__ -
## Поведенческие паттерны
__Поведенческие паттерны__ (Behavioral) описывают способы реализации взаимодействия между объектами с отличающимися типами. При таком взаимодействии объекты могут решать более трудные задачи, чем если бы они решали их по-отдельности.
___
### Итератор
__Итератор (Iterator)__ - это поведенческий паттерн проектирования, благодаря которому у нас есть возможность последовательно обходить элементы составных объектов, при этом не раскрывая их внутреннего представления.
Идея паттерна в том, чтобы вынести поведение обхода коллекции из самой коллекции в __отдельный класс__.
> :white_check_mark: Зная эту информацию, давайте теперь его реализуем.
Пусть у нас будет файловая система, которая будет хранить файлы. У каждого файла есть следующие свойства:
```C#
///
/// Файл.
///
public class File
{
///
/// Идентификатор.
///
public Guid Id { get; set; }
///
/// Название.
///
public string Name { get; set; }
///
/// Тип.
///
public string Type { get; set; }
///
/// Строковое преставление объекта файла.
///
/// Данные объекта файла в виде строки.
public override string ToString() => $"Идентификатор: {Id} Название: {Name} Тип: {Type}";
}
```
:one: Создадим интерфейс IFileIterator итератора для файловой системы:
```C#
///
/// Содержит методы для итератора файловой системы.
///
public interface IFileIterator
{
///
/// Проверяет, есть ли в коллекции следующий элемент.
///
/// Результат проверки.
bool HasNext();
///
/// Получает следующий элемент.
///
/// Следующий элемент.
File Next();
}
```
:two: Создадим интерфейс IFileNumerator, содержащий методы получения итератора из коллекции:
```C#
///
/// Содержит методы получения итератора из коллекции.
///
public interface IFileNumerable
{
///
/// Создание итератора.
///
/// Созданный итератор.
IFileIterator CreateNumerator();
///
/// Количество элементов в коллекции.
///
int Count { get; }
///
/// Получение элемента из коллекции по индексу.
///
/// Индекс элемента, который необходимо получить.
/// Элемент по индексу.
File this[int index] { get; }
}
```
:three: Теперь мы можем создать конкретную файловую систему, реализующую интерфейс IFileNumerable:
```C#
///
/// Файловая система.
///
public class FileSystem : IFileNumerable
{
///
/// Файлы, хранящиеся в файловой системе.
///
private List _files;
///
/// Количество файлов в файловой системе.
///
public int Count => _files.Count;
///
/// Создание файловой системы.
///
public FileSystem()
{
_files = new List();
}
///
/// Проверяет выход индекса за границы списка файлов файловой системы.
///
/// Порядковый номер элемента.
/// Индекс вышел за границы!
///
private void ValidateIndex(int index)
{
if (index < 0 || index >= _files.Count)
{
throw new ArgumentOutOfRangeException("Индекс вышел за границы массива!");
}
}
///
/// Создание итератора.
///
/// Итератор.
public IFileIterator CreateNumerator() => new FileSystemNumerator(this); // Данный класс мы создадим далее.
///
/// Доступ к элементам файловой системы.
///
/// Позиция элемента, к которому необходим доступ.
/// Индекс вышел за границы!
public File this[int index]
{
get
{
ValidateIndex(index);
return _files[index];
}
}
}
```
:four: Теперь последний шаг - реализуем класс-алгоритм обхода файловой системы, реализующий интерфейс IFileIterator.
```C#
///
/// Реализует алгоритм обхода файловой системы.
///
public class FileSystemNumerator : IFileIterator
{
///
/// Содержит методы для создания объекта-итератора.
///
private IFileNumerable _aggregate;
///
/// Индекс текущего элемента.
///
private int _index = 0;
///
/// Создание итератора файловой системы с помощью указанных параметров.
///
/// Содержит методы для создания объекта-итератора.
/// Интерфейс для создания объекта-итератора равен null!
public FileSystemNumerator(IFileNumerable aggregate)
{
if (aggregate == null)
{
throw new ArgumentNullException(nameof(aggregate),
"Интерфейс для создания объекта-итератора равен null!");
}
_aggregate = aggregate;
}
///
/// Проверяет наличие следующего элемента.
///
/// Результат проверки.
public bool HasNext() => _index < _aggregate.Count;
///
/// Получение следующего элемента из файловой системы.
///
/// Следующий файл.
/// Индекс вышел за границы!
public File Next()
{
if (!HasNext())
{
throw new ArgumentOutOfRangeException("Индекс вышел за границы!");
}
return _aggregate[_index++];
}
}
```
:white_check_mark: __Преимущества паттерна Iterator__: достигается упрощение классов хранения данных.
:x: __Недостатки__: Если вы работаете только с простыми коллекциями, то вам нет необходимости использовать данный паттерн.
___
### Наблюдатель
__Наблюдатель (Observer)__ - поведенческий шаблон проектирования. Определяет зависимость типа «один ко многим» таким образом, что при изменении объекта все, зависящие от него, получают сообщение об этом событии.
В dotnet есть три способа реализации данного паттерна:
:one: __Через делегаты.__ Данный способ гарантирует наличие наблюдателя и подходит, когда нужно реализовать отношение: 1 поставщик – 1 наблюдатель. Также при данном подходе можно получить результат – ответ от подписчика.
:two: __Через события.__ Любое число подписчиков. Нет гарантии наличия подписчиков. Не предусмотрено получение ответа от подписчика.
:three: __Через набор интерфейсов IObserver__ (механизм для получения push-уведомлений)/IObservable (определяет поставщика push-уведомлений).
> :x: Почему стоит использовать эти интерфейсы вместо событий: события плохо поддаются тестированию, данный паттерн универсален, он может использоваться и в других языках программирования. В C# есть события, а в других языках программирования их может и не быть.
:grey_exclamation: Таким образом, у нас есть __IObservable__ – определяет наблюдаемый объект и __IObserver__ – определяет наблюдателей.
Реализуем паттерн __наблюдатель__ на примере __корпоративного портала для сотрудников__. У нас будут пользователи корпоративного портала - сотрудники компании.
Сотрудники могут подписываться на уведомления о каких-либо новостях, событиях, которые будут публиковаться на корпоративном портале.
Соответственно, все пользователи, которые подписаны на уведомления корпоративного портала (подписчики) будут уведомлены о каком-либо событии. В данном случае корпоративный портал будет __поставщиком данных__ для наших подписчиков.
:one: Создаем класс Message, который будет являться объектом сообщения (уведомления), которое будут получать подписчики от поставщика данных - в нашем случае корпоративного портала.
```C#
///
/// Сообщение.
///
public class Message
{
///
/// Текст сообщения.
///
private string _text;
///
/// Автор сообщения.
///
private string _author;
///
/// Создание сообщения с помощью указанных данных.
///
/// Текст сообщения.
/// Автор сообщения.
public Message(string text, string author)
{
Validator.ValidateStringText(text);
Validator.ValidateStringText(author);
_text = text;
_author = author;
}
///
/// Строковое представление объекта сообщения.
///
/// Данные сообщения в виде строки.
public override string ToString() => $"Текст сообщения: {Environment.NewLine}{_text}" +
$"{Environment.NewLine}Автор: {_author}";
}
```
:two: Далее мы создаем объект пользователя, реализующего интерфейс IObserver, поскольку он является наблюдателем. Параметром интерфейса выступает __Message__ - тип данных уведопления, которое будут получать подписчики.
Реализуя данный интерфейс, нам необходимо реализовать логику работы методов __OnCompleted, OnError, OnNext__.
```C#
///
/// Пользователь.
///
public class User : IObserver
{
///
/// Логин пользователя.
///
private string _login;
///
/// Создает пользователя с помощью указанных параметров.
///
/// Логин пользователя.
/// Логин равен null!
public User(string login)
{
Validator.ValidateStringText(login);
_login = login;
}
///
/// Обработчик события, когда от поставщика данных больше не будет поступать никаких уведомлений.
///
public void OnCompleted()
{
}
///
/// Обработчик события возникновения исключения у поставщика данных при отправке уведомлений.
///
/// Исключение, которое возникло у поставщика данных.
/// Исключение равно null!
public void OnError(Exception error)
{
if (error == null)
{
throw new ArgumentNullException(nameof(error), "Исключение равно null!");
}
Console.WriteLine($"Отправка уведомлений пользователю завершилась с ошибкой: {error.Message}" +
$"{Environment.NewLine}Логин получателя: {_login}");
}
///
/// Обработчик события поступления уведомлений от поставщика данных.
///
/// Сообщение, поступившее от поставщика данных.
/// Сообщение равно null!
public void OnNext(Message value)
{
if (value == null)
{
throw new ArgumentNullException(nameof(value), "Сообщение равно null!");
}
Console.WriteLine($"Полученное уведомление: {Environment.NewLine}{value}{Environment.NewLine}" +
$"Логин получателя: {_login}");
}
}
```
:three: Теперь создадим класс нашего корпоративного портала, реализующий интерфейс IObservable (наблюдаемый объект). В качестве параметра также выступает тип данных Message - тип сообщения для подписчиков.
```C#
///
/// Корпоративный портал.
///
public class CorporatePortal : IObservable
{
///
/// Список подписчиков.
///
private readonly List> _observers;
///
/// Создание корпоративного портала.
///
public CorporatePortal()
{
_observers = new List>();
}
///
/// Подписка на уведомления.
///
/// Подписчик.
/// Объект с механизмом освобождения неуправляемых ресурсов.
/// Подписчик равен null!
public IDisposable Subscribe(IObserver observer)
{
if (observer == null)
{
throw new ArgumentNullException(nameof(observer), "Подписчик равен null!");
}
_observers.Add(observer);
return new Unsubscriber(_observers, observer); // Данные класс будет реализован далее.
}
///
/// Отправляет уведомление всем подписчикам.
///
/// Сообщение, которое будет отправлено всем подписчикам.
/// Сообщение равно null!
public void Notify(Message message)
{
if (message == null)
{
throw new ArgumentNullException(nameof(message), "Сообщение равно null!");
}
foreach (var observer in _observers)
{
observer.OnNext(message);
}
}
}
```
> Несколько комментариев касательно Unsubscriber: нам необходимо, чтобы помимо подписки на событие, у пользователя была возможность и отписаться от события. В Unsubscriber должен храниться список всех подписчиков и конкретный подписчик, с которым будет происходить взаимодействие.
:four: Теперь давайте реализуем данный класс.
:pushpin: __Обратите внимание__, что он должен реализовывать интерфейс __IDisposable__, в котором содержится метод Dispose - именно так будет происходить отписка пользователя от уведомлений корпоративного портала.
```C#
///
/// Работает с отписками от уведомлений.
///
/// Тип подписчика.
public class Unsubscriber : IDisposable
{
///
/// Список подписчиков.
///
private readonly List> _observers;
///
/// Подписчик.
///
private readonly IObserver _observer;
///
/// Создание экземпляра для отписок от уведомлений с помощью указанных данных.
///
/// Подписчики.
/// Подписчик.
/// Подписчики равны null!
public Unsubscriber(List> observers, IObserver observer)
{
if (observers == null || observers.FindIndex(subscriber => subscriber == null) != -1)
{
throw new ArgumentNullException(nameof(observers),
"Список подписчиков или его элементы равны null!");
}
if (observer == null)
{
throw new ArgumentNullException(nameof(observer), "Подписчик равен null!");
}
if (!observers.Contains(observer))
{
throw new ArgumentNullException(nameof(observer),
"Подписчик не найден в списке подписчиков!");
}
_observer = observer;
_observers = observers;
}
///
/// Отписка подписчика от уведомлений.
///
public void Dispose()
{
if (_observers.Contains(_observer))
{
_observers.Remove(_observer);
}
}
}
```
Отписка у нас происходит следующим образом: мы удаляем подписчика из нашей коллекции подписчиков, соответственно, ему больше не будут приходить уведомления корпоративного портала.
:white_check_mark: __Преимущества паттерна Observer__: Можно создавать новые классы подписчиков. При этот класс наблюдаемого объекта как-то изменять не нужно.
:x: __Недостатки__: Подписчики уведомляются в произвольном порядке.
___
### Посредник
__Посредник (Mediator)__ - это поведенческий паттерн проектирования, бгагодаря которому уменьшается связанность множества классов между собой. Это достигается за счет перемещения этих связей в один класс-посредник.
Самое популярное применение посредника в C# коде – это связь нескольких компонентов __GUI__ одной программы. __Аналогия из жизни__: пилоты не общаются напрямую друг с другом, а через диспетчера.
Давайте попробуем реализовать данный паттерн на __следующем примере__: пусть у нас есть какая-то IT-компания, в которой есть программист и тимлид. Тимлид не скидывает лично (напрямую) программисту задачу, например, в социальных сетях. Он публикует ее в __TFS__ (Team Foundation Server).
Программист заходит на TFS, чтобы проверить, не появилось ли для него задач. Если задачи есть - берет их в работу. После выполнения задачи, программист переводит ее в "ожидающие проверки". Тимлид также заходит на TFS, чтобы проверить, выполнил ли программист задачи, которые он выдал. Если сделал - проверяет их. Если есть недочеты - отправляет на доработку, если недочетов нет - закрывает задачу. Также тимлид может дать программисту новые задачи для выполнения, если таковые имеются.
> В нашем примере посредником (Mediator) будет является TFS между программистом и тимлидом.
Теперь реализуем это в коде.
:one: Создадим интерфейс __IMediator__ - он будет содержать методы, которыми будет обладать класс-посредник. С помощью метода __Notify__ посредник будет уведомлять обе стороны о каком-либо событии.
```C#
///
/// Содержит методы для посредника.
///
public interface IMediator
{
///
/// Обрабатывает уведомления.
///
/// Работник.
/// Сообщение.
void Notify(Worker worker, string message);
}
```
:two: Теперь реализуем абстрактный класс Worker - это будет наш сотрудник. Он будет хранить посредника - его можно будет установить в методе __SetMediator__:
```C#
///
/// Работник.
///
public abstract class Worker
{
///
/// Посредник.
///
protected IMediator _mediator;
///
///Устанавливает посредника для работника.
///
/// Посредник.
/// Посредник равен null!
public void SetMediator(IMediator mediator)
{
if (mediator == null)
{
throw new ArgumentNullException(nameof(mediator), "Посредник равен null!");
}
_mediator = mediator;
}
}
```
:three: Теперь создадим первый класс-наследник нашего Worker: это будет класс Programmer. У него будет два метода: начать работу и закончить работу. Причем программисту нельзя давать новую задачу, пока он не завершил предыдущую.
```C#
///
/// Программист.
///
public class Programmer : Worker
{
///
/// Текст задачи,над которой работает программист.
///
private string _taskText = string.Empty;
///
/// Получение текста задания.
///
public string TaskText => _taskText;
///
/// Начинает работу над задачей.
///
/// Текст задачи.
public void StartWork(string taskText)
{
Validator.ValidateStringText(taskText);
if (_taskText.Length != 0)
{
if (_mediator != null)
{
_mediator.Notify(this, "Программист уже работает над задачей!");
}
return;
}
_taskText = taskText;
if (_mediator != null)
{
_mediator.Notify(this, $"Программист начал работу над задачей: {taskText}");
}
}
///
/// Завершает работу над задачей.
///
public void FinishWork()
{
if (_mediator != null)
{
_mediator.Notify(this, $"Программист завершил работу над задачей: {_taskText}");
}
_taskText = string.Empty;
}
}
```
:four: Вторым классом-наследником будет наш Тимлид. У него будет метод __GiveTask__ - выдать задачу.
```C#
///
/// Тимлид.
///
public class TeamLead : Worker
{
///
/// Дать задание.
///
/// Текст задания.
public void GiveTask(string taskText)
{
Validator.ValidateStringText(taskText);
if (_mediator != null)
{
_mediator.Notify(this, "Выдаю задачу программисту: " + taskText);
}
}
}
```
:five: Теперь мы можем написать класс TFS, который будет реализовывать интерфейс посредника IMediator. Класс внутри будет содержать объекты тимлида и программиста, посредниками которых он является. Также у нас будет список задач. С помощью метода AddTask мы можем добавить задачу в список. В конструкторе мы не только инициализируем тимлида и программиста, но и устанавливаем им текущего посредника.
Логика работы метода Notify следующая: если уведомление посреднику отправляет тимлид, то это означает, что он дает задачу сотруднику, значит сотрудник должен приступить к работе. Если уведомление приходит от сотрудника, то это означает, что он выполнил задачу и тимлид дает ему новую задачу: он будет давать новые задачи до тех пор, пока список задач не станет пустым.
```C#
///
/// TFS.
///
public class TFS : IMediator
{
///
/// Программист.
///
private Programmer _programmer;
///
/// Тимлид.
///
private TeamLead _teamLead;
///
/// Задачи.
///
private List _tasks;
///
/// Создает TFS с помощью указанных данных.
///
/// Программист.
/// Тимлид.
/// Программист или тимлид равен null!
public TFS(Programmer programmer, TeamLead teamLead)
{
if (programmer == null)
{
throw new ArgumentNullException(nameof(programmer), "Программист равен null!");
}
if (teamLead == null)
{
throw new ArgumentNullException(nameof(teamLead), "Тимлид равен null!");
}
_programmer = programmer;
_teamLead = teamLead;
programmer.SetMediator(this);
teamLead.SetMediator(this);
_tasks = new List();
}
///
/// Обрабатывает уведомления.
///
/// Работник.
/// Сообщение.
public void Notify(Worker worker, string message)
{
if (worker == null)
{
throw new ArgumentNullException(nameof(worker), "Работник равен null!");
}
Validator.ValidateStringText(message);
Console.WriteLine(message);
if (worker is Programmer)
{
if (message.StartsWith("Программист завершил работу над задачей"))
{
if (_tasks.Count != 0)
{
_teamLead.GiveTask(_tasks[0]);
_tasks.RemoveAt(0);
}
}
return;
}
if (worker is TeamLead)
{
_programmer.StartWork(message);
}
}
///
/// Добавить задачу.
///
/// Текст задачи.
public void AddTask(string taskText)
{
Validator.ValidateStringText(taskText);
_tasks.Add(taskText);
}
}
```
:white_check_mark: __Преимущества паттерна Mediator__: Достигается устранение зависимости между компонентами, благодаря чему их можно повторно использовать, более удобным становится взаимодействие между компонентами, также управление компонентами централизовано.
:x: __Недостатки__: Код посредника может быть очень большим.
___
### Шаблонный метод
__Шаблонный метод__ (Template Method) - это поведенческий паттерн проектирования, который определяет общий алгоритм поведения подклассов. При этом подклассы имеют возможность переопределять части этого алгоритма, не меняя при этом его общей структуры.
> :white_check_mark: Если бы мы не использовали данный паттерн, то нам приходилось бы явно прописывать реализацию алгоритма в каждой подклассе, несмотря на то, что алгоритмы в этих подклассах имеют небольшие различия.
Рассмотрим реализацию шаблонного метода на конкретном примере: пусть у нас будет бухгалтерия, в котором выдают зарплату для сотрудников.
:one: Реализуем класс Worker, содержащий необходимые свойства, которыми будет обладать каждый сотрудник.
```C#
///
/// Сотрудник.
///
public class Worker
{
///
/// Идентификатор.
///
public Guid Id { get; set; }
///
/// Имя.
///
public string FirstName { get; set; }
///
/// Фамилия.
///
public string LastName { get; set; }
///
/// Отчество.
///
public string Patronymic { get; set; }
///
/// Должность.
///
public string Post { get; set; }
///
/// Выплачена ли зарплата.
///
public bool IsSalaryPaid { get; set; }
}
```
:two: Реализуем абстрактный класс бухгалтерии, в которой из полей будет список сотрудников типа Worker, которые работают в конкретной компании и словарь, в котором в зависимости от должности указана зарплата, которую должен получать сотрудник, занимая определенную должность.
Нам необходимо выдать зарплату сотруднику - за это отвечает метод __GetSalary__. Алгоритм выдачи зарплаты следующий:
1. Сначала вызывается метод __ValidateWorkerId__, проверяющий корректности идентификатора сотрудника, которому необходимо выдать зарплату - он не должен быть null.
2. Далее вызывается метод __ValidateWorkerExistence__, проверяющий существование сотрудника с указанным идентификатором.
3. Затем вызывается метод __ValidatePaidSalary__, проверяющий, получал ли работник зарплату ранее, если он ее уже получил, то повторно она выплачиваться не должна.
4. Последний этап - вызывается метод __GetCalculationSalary__, который расчитывает зарплату в зависимости от должности - он будет виртуальным. Это означает, что классы-наследники могут его переопределить, а могут и не переопределять, поскольку в базовом классе прописана его реализация по умолчанию. Результат метода GetCalculationSalary и будет возвращаться методом GetSalary.
> __Обратите внимание__, что нам нет смысла переопределять методы из первых трех пунктов - неважно, какой компании является бухгалтерия. То есть в абсолютно любой бухгалтерии прежде чем выдать сотруднику зарплату, необходимо выполнить первые три пункта (возможно больше, в примере представлена упрощенная версия, чтобы было более понятно назначение паттерна.
:white_check_mark: Благодаря шаблонному методу нам не нужно в бухгалтерии каждой компании прописывать выполнение первых трех пунктов, потому что они уже реализованы в базовом классе __Accounting__.
```C#
///
/// Бухгалтегия.
///
public abstract class Accounting
{
///
/// Список сотрудников для выплаты зарплаты.
///
protected List _workers = new List();
///
/// Зарплаты сотрудников.
///
protected Dictionary _workersSalary = new Dictionary();
///
/// Создание бухгалтерии.
///
public Accounting()
{
_workers = new List();
_workersSalary = new Dictionary();
}
///
/// Проверка корректности идентификатора сотрудника.
///
/// Идентификатор сотрудника равен null!
protected void ValidateWorkerId(Guid id)
{
if (id == null)
{
throw new ArgumentNullException(nameof(id), "Идентификатор сотрудника равен null!");
}
}
///
/// Проверка наличия сотрудника в базе.
///
/// Идентификатор сотрудника, которого необходимо проверить.
protected void ValidateWorkerExistence(Guid id)
{
if (_workers.FirstOrDefault(worker => worker.Id == id) == null)
{
throw new ArgumentNullException(nameof(id), "Сотрудник с указанным идентификатором не найден!");
}
}
///
/// Проверка того, была ли выплачена зарплата работнику ранее.
///
/// Идентификатор сотрудника.
/// Данному сотруднику уже выплачена зарплата!
protected void ValidatePaidSalary(Guid id)
{
if (_workers.FirstOrDefault(worker => worker.Id == id).IsSalaryPaid)
{
throw new ArgumentOutOfRangeException("Данному сотруднику уже выплачена зарплата!");
}
}
///
/// Получение расчитанной зарплаты сотрудника.
///
/// Идентификатор сотрудника.
/// Расчитанная зарплата.
protected virtual decimal GetCalculationSalary(Guid id)
=> _workersSalary[_workers.FirstOrDefault(worker => worker.Id == id).Post];
///
/// Выплата зарплаты сотруднику.
///
/// Идентификатор сотрудника, которого необходимо проверить.
public decimal GetSalary(Guid id)
{
ValidateWorkerId(id);
ValidateWorkerExistence(id);
ValidatePaidSalary(id);
_workers.FirstOrDefault(worker => worker.Id == id).IsSalaryPaid = true;
return GetCalculationSalary(id);
}
///
/// Добавление сотрудника в базу бухгалтерии.
///
/// Имя.
/// Фамилия.
/// Отчество.
/// Должность.
/// Сотрудник с указанными данными уже есть в базе!
/// Указанная должность в бухгалтерии не найдена!
public Guid AddWorker(string firstName, string lastName, string patronymic, string post)
{
Validator.ValidateStringText(firstName);
Validator.ValidateStringText(lastName);
Validator.ValidateStringText(patronymic);
Validator.ValidateStringText(post);
if (!_workersSalary.ContainsKey(post))
{
throw new ArgumentNullException("Указанная должность в бухгалтерии не найдена!");
}
var id = Guid.NewGuid();
_workers.Add(new Worker
{