Шаблон декоратора сочетает в себе полезность и низкую стоимость, что незаменимо при разработке программного обеспечения. Это позволяет вам прозрачно разделять сквозные проблемы для пользователя интерфейса. Иногда его используют как более гибкую замену наследования. Шаблон декоратора имеет тенденцию дополнять другие шаблоны проектирования, которые помогают при конструировании, например, Factory Pattern или Builder Pattern. Для наших целей мы создадим простой конструктор, который будет работать с нашим декоратором.

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

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

public interface IRepository<TModel> 
{
    void Add(TModel model);
    void Delete(TModel model);
    void Update(TModel model);
    TModel GetById(int id);
}

Я знаю, что это довольно анемично для доступа к данным, но основное внимание в этом посте уделяется шаблону проектирования, поэтому мы будем придерживаться простого интерфейса. Теперь предположим, что у вас есть два разработчика интерфейса. Один использует структурированную базу данных в качестве хранилища, а другой использует хранилище в памяти. Для любого из этих методов у вас может быть много чего. Конечно, вы хотите сохранить данные, но тогда вы, вероятно, также захотите выполнять все виды протоколирования, проверки и, возможно, даже авторизации. Разработчику SQL для простоты можно использовать Entity Framework.

public class EFRepository<TModel> : Interfaces.IRepository<TModel>
    where TModel : BaseEntity
{
    DbContext context;
    public EFRepository(DbContext context)
    {
        this.context = context;
    }
    public void Add(TModel model)
    {
        context.Set<TModel>().Add(model);
        context.SaveChanges();
    }
    public void Delete(TModel model)
    {
        context.Set<TModel>().Remove(model);
        context.SaveChanges();
    }
    public TModel GetById(int id)
    {
        return context.Set<TModel>()
            .FirstOrDefault(m => m.Id == id);
    }
    public void Update(TModel model)
    {
        var set = context.Set<TModel>();
        set.Attach(model);
        context.SaveChanges();
    }
}

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

public class InMemoryRepository<TModel> :
    Interfaces.IRepository<TModel>
    where TModel : BaseEntity
{
    Dictionary<int, TModel> datastore = 
        new Dictionary<int, TModel>();
    public void Add(TModel model)
    {
        if (datastore.ContainsKey(model.Id))
        {
            throw new InvalidOperationException("A model with this key already exists in the store");
        }
        datastore.Add(model.Id, model);
    }
    public void Delete(TModel model)
    {
        if (!datastore.ContainsKey(model.Id))
        {
            throw new InvalidOperationException("A model with this key doesnt exist in the store");
        }
        datastore.Remove(model.Id);
    }
    public TModel GetById(int id)
    {
        if (!datastore.ContainsKey(id))
        {
            throw new InvalidOperationException("A model with this key doesnt exist in the store");
        }
        return datastore[id];
    }
    public void Update(TModel model)
    {
        if (!datastore.ContainsKey(model.Id))
        {
            throw new InvalidOperationException("A model with this key doesnt exist in the store");
        }
        datastore[model.Id] = model;
    }
}

Первое, что вы можете заметить, это то, что обе реализации довольно анемичны, ни одна из них не реализует никаких функций, кроме фактического сохранения и загрузки данных. Здесь на помощь приходит декоратор. Декоратор реализует интерфейс и требует реализации интерфейса. Затем он обращается к базовой реализации для каждого метода или функции, добавляя при этом немного «украшения». Посмотрим, например, на ведение журнала.

В первую очередь декоратору нужно чем-то украсить. Таким образом, конструктор должен принимать интерфейс IRepository ‹TModel›. Для наших целей наш декоратор ведения журнала также принимает интерфейс для ведения журнала. Итак, конструктор будет выглядеть так:

public LoggingDecorator(
    IRepository<TModel> repository, 
    ILogger logger)
{
    this.repository = repository;
    this.logger = logger;
}

Теперь давайте реализуем метод «Добавить». Допустим, в начале метода, который мы хотим вызвать в журнал, что что-то добавляется: this.logger.LogMessage(“Adding a model”); после этого мы можем вызвать метод добавления нашего переданного в репозиторий this.repository.Add(model), но давайте обернем это в try catch и запишем все исключения:

try
{
    this.repository.Add(model);
}
catch (Exception ex)
{
    logger.LogError(ex.ToString());
    throw;
}

одна из ключевых особенностей декоратора в том, что интерфейс не меняется

Обратите внимание, что мы обязательно повторно генерируем исключение. Это важно, чтобы любые другие декораторы также имели возможность работать с исключениями. Возможность исключения является частью интерфейса, и одной из ключевых особенностей декоратора является то, что интерфейс не изменяется. Если вы хотите получить более детальную информацию, вы можете выделить ведение журнала исключений в другой декоратор.

Проделайте это для каждого из методов, и у вас будет декоратор! Причина, по которой это так мощно, заключается в том, что теперь мы абстрагируем ведение журнала от реализации конкретного репозитория. Независимо от того, используем ли мы EFRepository или InMemoryRepository, мы можем обеспечить ведение журнала, заключив его в декоратор ведения журнала. Это также дает нам возможность легко включать и выключать вход, просто используя декоратор. Использование шаблона Builder для создания и комбинирования декораторов может быть хорошим способом справиться с дополнительной сложностью комбинирования и упорядочивания декораторов. Подумайте о том, чтобы создать свой репозиторий следующим образом:

var repository = Builders.RepositoryBuilder<SomeModel>.Create()
    .WithDataStore()
        .InMemory()
    .WithValidator(validator)
    .WithAuthorization(authorizer, currentUser)
    .WithLogging(logger)
    .CreateRepository();

Несколько заключительных замечаний по декораторам: Порядок важен! Обратите внимание, что наш декоратор ведения журнала указан последним. Это потому, что он улавливает исключения и регистрирует их. Если вы хотите, чтобы исключение, созданное валидатором, регистрировалось декоратором ведения журнала, декоратор ведения журнала должен обернуть декоратор валидатора. Также учтите, что декоратор должен принимать все, что ему нужно для выполнения своей задачи, чтобы быть внедренным конструктором.