Шаблон Data Dependency

в 20:08, , рубрики: .net, dependency injection, patterns, unity, Песочница, метки: , , , ,

Введение

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

Задача

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

    public interface ITeam
    {
        string Name { get; }
    }

Ранжировщик:

    public interface IRanker
    {
        int Rank(ITeam team);
    }

Запись в рейтинге:

    public interface ITopRecord
    {
        ITeam Team { get; }
        int Position { get; }
        double AverageRank { get; }
    }

Интерфейс компонента:

    public interface ITopBuilder
    {
        IEnumerable<ITopRecord> BuildTop(IEnumerable<ITeam> teams, int topCount);
    }

В зависимости от используемого контейнера, может дополнительно понадобиться примитив поставщика оценщиков:

    public interface IRankerProvider
    {
        IEnumerable<IRanker> GetRankers();
    }

Реализация

Можно приступать к реализации, а интерфейсы в отдельной сборке отдать разработчикам ранжировщиков (IRanker), чтобы они делали свою работу. Реализация интерфейса IRankerProvider лежит на пользователе, который знает, какие ранжировщики необходимо использовать, потому нам остается только ITopRecord и ITopBuilder. Можно ещё сделать из ранжировщиков композит. Делаем, сдаем и забываем.
Через некоторое время появляется неожиданная проблема. Одному из ранжировщиков для выставления оценки необходимо знать, сколько всего команд. Обычно на этом месте вспоминаются все недобрые, плохая архитектура и тому подобные вещи. Часто прибегают к некрасивому решению – изменению интерфейса ранжировщика:

    public interface IRanker
    {
        int Rank(ITeam team,int teamCount);
    }

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

    public interface IRanker
    {
        int Rank(ITeam team);
    }
    public interface IRankerWithCount
    {
        int Rank(ITeam team,int teamCount);
    }

или подобной схеме, которая неизбежно потребует изменений в реализации ITopBuilder.
Ад начинается тогда, когда приходит новое требование – ещё одному ранжировщику понадобилась дополнительная информация. Ему нужно знать, стоит ли оцениваемая команда в списке на четной позиции. А ещё одному обязательно надо знать, сколько человек будет в рейтинге.
На этом месте проще и правильнее всего сдаться, и передать в ранжировщики все параметры ITopBuilder:

    public interface IRanker
    {
        int Rank(ITeam team, IEnumerable<ITeam> teams, int topCount);
    }

Можно сделать это неявно, введя понятие контекста, и передавать контекст. В любом случае, ответственность за извлечение нужных данных полностью ложится на тех, кто реализует ранжировщики, а интерфейсы фиксируются. Кажется, что так и надо было сделать с самого начала.

Другой путь

Хотя решение, предложенное выше, и универсально, у него есть недостаток. Оно избыточно. Лишние параметры передаются всем ранжировщикам, даже когда они не нужны. Попробуем избавиться от лишних параметров совсем.
И, для начала, заметим, что лишние параметры являются зависимостями, для работы с которыми у нас есть паттерн Dependency Injection и контейнер. Класть в контейнер экземпляры IEnumerable и Int32 плохо. Обернем их в соответствующие интерфейсы:

    public interface ITeamCollection:IEnumerable<ITeam>
    {
    }
    public interface ITopCount
    {
        int Count { get; }
    }

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

    public interface ITop : IEnumerable<ITopRecord>
    {
    }

Теперь интерфейс нашего компонента ITopBuilder будет выглядеть следующим образом:

    public interface ITopBuilder
    {
        ITop BuildTop(ITeamCollection teams, ITopCount count);
    }

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

        public ITop BuildTop(ITeamCollection teams, ITopCount count)
        {
            using (var container = _container.CreateChildContainer())
            {
                container.RegisterInstance(teams);
                container.RegisterInstance(count);
                //do work
            }
        }

Разрешать ITeamCollection и ITopCount ранжировщики будут в конструкторе. А, следовательно, код не заработает. Всё потому, что ранжировщики разрешаются в конструкторе IRankerProvider, а он вызывается до того, как мы регистрируем наши экземпляры. Решить данную проблему можно делегировав работу на другой объект, который будет создаваться при каждом вызове BuildTop:

        public ITop BuildTop(ITeamCollection teams, ITopCount count)
        {
            using (var container = _container.CreateChildContainer())
            {
                container.RegisterInstance(teams);
                container.RegisterInstance(count);
                return container.Resolve<BuilerWorker>().DoWork();
            }
        }
   internal sealed class BuilerWorker
    {
        public BuilerWorker(
            ITeamCollection teams, 
            ITopCount count,
            IRankerProvider rankerProvider
            ...
            )
        {
            
        }
        public ITop DoWork()
        {
            ...
        }
    }

Таким образом мы сформировали шаблон, который достаточно универсален и хорошо применим на практике, чтобы иметь название – Data Dependency.

Автор: akrupa

Источник

Поделиться

* - обязательные к заполнению поля