itmo_conspects

Лекция 7. Структурные паттерны

На этой лекции разберем структурные паттерны (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 шага:

  1. Меняем абстракцию - создаем крутой адаптер, интерфейс которого поддерживает и старую, и новую реализации, и используем этот адаптер в нашем коде
  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) { }
}

Рассмотрим несколько видов заместителей:

Заместитель

Как можно заметить, прокси очень подозрительно похож на декоратор, однако:

Фасад

Фасад (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);
    }
}

Легковес