На этой лекции разберем структурные паттерны (Structural Patterns), которые помогают в проектировании проектной модели
Адаптер (Adapter) - промежуточный тип, использующий объект одного типа, для реализации интерфейса другого типа
Для применения паттерна “Адаптер” обычно есть цель (Target), которая представляет целевой интерфейс, через который мы хотим взаимодействовать с объектом, изначально его не реализующий. Далее должен быть адаптируемый тип (Adaptee) с другим интерфейсом, который мы хотим использовать через целевой интерфейс с помощью адаптера - обертки, реализующей целевой интерфейс, содержащей объект адаптируемого типа и перенаправляющей в него вызовы поведений целевого интерфейса
Яркий пример применения адаптера - это различие форматов розетки и вилки во всем мире. Так, например, британский стандарт использует три прямоугольных штыря - один для фазы, другой для нейтрали, а третий для заземления. Чтобы подключить британскую вилку (она является адаптируемым типом) к европейской розетке, которая принимает европейские вилки, состоящей из двух цилиндрических штырей (это целевой интерфейс), нужен адаптер - кусок пластика с металлическим контактами, которые изогнуты так, чтобы подключать британскую вилку к европейской розетке
С помощью адаптера можно достичь полиморфизма между несовместимыми объектами. Например: есть два типа логгеров, которые имеют разный интерфейс, но нужно к ним обращаться одинаково:
public class PostgresLogStorage
{
public void Save(
string message,
DateTime timeStamp,
int severity)
{
...
}
}
public class ElasticSearchLogStorage
{
public void Save(ElasticLogMessage message)
{
...
}
}
public interface ILogStorage
{
void Save(LogMessage message);
}
public class PostgresLogStorageAdapter : ILogStorage
{
private readonly PostgresLogStorage _storage;
public void Save(LogMessage message)
{
_storage.Save(
message.Message,
message.DateTime,
message.Severity.AsInteger());
}
}
public class ElasticLogStorageAdapter : ILogStorage
{
private readonly ElasticSearchLogStorage _storage;
public void Save(LogMessage message)
{
_storage.Save(message.AsElasticLogMessage());
}
}

И вся прелесть адаптеров раскрывается в том случае, если эти логгеры из разных библиотек - естественно, мы не можем менять их исходный код и исправлять интерфейсы друг под друга
Помимо этого адаптеры это:
Помимо этого с помощью адаптеров можно проводить адаптивный рефакторинг. Допустим, что есть такая ситуация: все долгие годы мы использовали в проекте старый синхронный логгер, теперь все процессы в коде асинхронные, и нам нужен асинхронный логгер. Тогда сделаем все в 2 шага:
Таким образом, мы получаем два этапа, которые легче тестировать по отдельности
Допустим, что у нас есть абстракции сложные (низкоуровневые) и простые (верхнеуровневые). Тогда, чтобы через простую абстракцию использовать сложную, создадим мост
Мост (Bridge) - это объект, который разделяет один или несколько классов на две отдельные иерархии, позволяя изменять их независимо друг от друга
Пусть у нас будет сложное устройство “Телевизор”:
public interface IDevice
{
public bool IsEnabled { get; set; }
public int Channel { get; set; }
public int Volume { get; set; }
}
И простое устройство “Пульт управления”:
public interface IControl
{
void ToggleEnabled();
void ChannelForward();
void ChannelBackward();
void VolumeUp();
void VolumeDown();
}
Мост между ними будет выглядеть так:
public class Control : IControl
{
private readonly IDevice _device;
public void ToggleEnabled()
=> _device.IsEnabled = !_device.IsEnabled;
public void ChannelForward()
=> _device.Channel += 1;
public void ChannelBackward()
=> _device.Channel -= 1;
public void VolumeUp()
=> _device.Volume += 10;
public void VolumeDown()
=> _device.Volume -= 10;
}
При помощи моста мы можем разделить объектную модель на две иерархии - иерархию пультов и телевизоров

Можем заметить, что мост похож на адаптер тем, что тоже соблюдает принципы открытости/закрытости и инверсии зависимостей. Отличие моста от адаптера в том, что мост разделяет абстракцию и реализацию, а адаптер меняет интерфейс. Также как правило мост проектируется изначально
Компоновщик (Composite) - это представление древовидной структуры объектов в виде одного композитного объекта
Допустим, что у нас есть куча объектов, реализующих один интерфейс, и мы хотим сделать со всеми ними какое-то действие:
public interface IGraphicComponent
{
void MoveBy(int x, int y);
void Draw();
}
public class Circle : IGraphicComponent
{
public void MoveBy(int x, int y) { ... }
public void Draw() { ... }
}
public class Line : IGraphicComponent
{
public void MoveBy(int x, int y) { ... }
public void Draw() { ... }
}
Сделаем из них объект-_компоновщик_ GraphicComponentGroup, который циклом проходится и выполняет это действие у всех объектов:
public class GraphicComponentGroup : IGraphicComponent
{
private readonly IReadOnlyCollection<IGraphicComponent> _components;
public void MoveBy(int x, int y)
{
foreach (var component in _components)
component.MoveBy(x, y);
}
public void Draw()
{
foreach (var component in _components)
component.Draw();
}
}

Декоратор (Decorator) - это тип-обёртка над объектом абстракции, которую он реализует, добавляя к поведению объекта новую логику
Допустим у нас есть абстракция какого-то абстрактного сервиса:
public interface IService
{
void DoStuff(DoStuffArgs args);
}
public class Service : IService
{
public void DoStuff(DoStuffArgs args) { }
}
В последствии возникла потребность журналировать все, что делает сервис. Тогда для нашего декорируемого типа (Decoratee), сделаем декоратор LoggingServiceDecorator, реализующий наш интерфейс и расширяющий функционал:
public class LoggingServiceDecorator : IService
{
private readonly IService _decoratee;
private readonly ILogger _logger;
public void DoStuff(DoStuffArgs args)
{
_logger.Log(ArgsToLogMessage(args));
_decoratee.DoStuff(args);
}
private static string ArgsToLogMessage(DoStuffArgs args) { ... }
}

Заместитель (или прокси, Proxy) - тип-обёртка, реализующий логику контроля доступа к объекту, реализующему абстракцию, которую реализует он сам
Возьмем для примера обычный сервис, реализующий интерфейс:
public interface IService
{
void DoOperation(OperationArgs args);
}
public class Service : IService
{
public void DoOperation(OperationArgs args) { }
}
Рассмотрим несколько видов заместителей:
Виртуальный прокси (Virtual proxy)
Если у нас есть какой-то прям тяжелый объект, который мы хотим инициализировать тогда, когда он нам прям нужен, то нам поможет виртуальный прокси:
public class VirtualServiceProxy : IService
{
private readonly Lazy<IService> _service =
new Lazy<IService>(() => new Service());
public void DoOperation(OperationArgs args)
{
_service.Value.DoOperation(args);
}
}
Защищающий прокси (Defensive proxy)
Используется, если нужно ограничить доступ к объекту:
public class ServiceAuthorizationProxy : IService
{
private readonly IService _service;
private readonly IUserInfoProvider _userInfoProvider;
public void DoOperation(OperationArgs args)
{
if (_userInfoProvider.GetUserInfo().IsAuthenticated)
_service.DoOperation(args);
}
}
Кеширующий прокси (Caching proxy)
Используется, если выполнение операции обходится дорого, и мы не хотим каждый раз заново вызывать ее:
public class CachingServiceProxy : IService
{
private readonly IService _service;
private readonly Dictionary<OperationArgs, OperationResult> _cache;
public OperationResult DoOperation(OperationArgs args)
{
if (_cache.TryGetValue(args, out var result))
return result;
return _cache[args] = _service.DoOperation(args);
}
}
Удаленный прокси (Remote proxy)
Используется, если нужно работать с интерфейсом, который не лежит в программе (это может быть классом, который оборачивает HTTP-вызовы)

Как можно заметить, прокси очень подозрительно похож на декоратор, однако:
Фасад (Facade) - оркестрация одной или несколько сложных операций в каком-либо типе
Фасад рассматривался как контроллер в шаблонах GRASP. Фасад лучше не использовать для описания бизнес-логики, потому что существуют:
Но фасад может быть полезен в модели “запрос-ответ”, например, как объектная обертка вызовов API (Application Programming Interface, интерфейс программирования приложения)

Легковес (Flyweight) - декомпозиция объектов, выделенные тяжелых и повторяющихся данных в отдельные модели для дальнейшего переиспользования
При помощи легковеса мы можем отделить тяжелый объект, чтобы каждый раз пользоваться им по ссылке и не создавать новый
Допустим, что есть программа, моделирующая поведение частиц Particle, которые имеют определенные данные:
public record Particle(int X, int Y, byte[] Model);
public class ParticleFactory
{
private readonly IAssetLoader _assetLoader;
public Particle Create(string modelName)
{
var model = _assetLoader.Load(modelName);
return new Particle(0, 0, model);
}
}
Вместо создания множества копий значения Model можно всего лишь хранить ссылки на них в кеше легковеса ParticleFactory:
public record ModelData(byte[] Value);
public record Particle(int X, int Y, ModelData Model);
public class ParticleFactory
{
private readonly IAssetLoader _assetLoader;
private readonly Dictionary<string, ModelData> _cache;
public Particle Create(string modelName)
{
var model = _cache.TryGetValue(modelName, out var data)
? data
: _cache[modelName] =
new ModelData(_assetLoader.Load(modelName));
return new Particle(0, 0, model);
}
}
