Должен ли я отделить интерфейс репозитория от модели домена

Допустим, у меня есть служба DDD, которая требует некоторого IEnumerable<Foo> для выполнения некоторых вычислений. Я придумал две схемы:

  1. Абстрагируйте доступ к данным с помощью интерфейса IFooRepository, что довольно типично.

    public class FooService
    {
        private readonly IFooRepository _fooRepository;
    
        public FooService(IFooRepository fooRepository)
            => _fooRepository = fooRepository;
    
    
        public int Calculate()
        {
            var fooModels = _fooRepository.GetAll();
            return fooModels.Sum(f => f.Bar);
        }
    }
    
  2. Не полагайтесь на абстракцию IFooRepository и внедряйте IEnumerable<Foo> напрямую.

    public class FooService
    {
        private readonly IEnumerable<Foo> _foos;
    
        public FooService(IEnumerable<Foo> foos)
            => _foos = foos;
    
    
        public int Calculate()
            => _foos.Sum(f => f.Bar);
    }    
    

На мой взгляд, этот второй дизайн кажется лучше, так как FooService теперь не важно, откуда поступают данные, а Calculate становится чистой логикой предметной области (игнорируя тот факт, что IEnumerable может поступать из нечистого источника).

Еще один аргумент в пользу использования второго варианта заключается в том, что когда IFooRepository выполняет асинхронный ввод-вывод по сети, обычно желательно использовать async-await, например:

public class AsyncDbFooRepository : IFooRepository
{
    public async Task<IEnumerable<Foo>> GetAll()
    {
        // Asynchronously fetch results from database
    }
}

Но так как вам нужно асинхронизировать до конца, FooService теперь вынужден изменить свою подпись на async Task<int> Calculate(). Похоже, это нарушает принцип инверсии зависимостей.

Однако есть проблемы и со вторым дизайном. Прежде всего, вы должны полагаться на контейнер DI (используя Simple Injector в качестве примера здесь) или корень композиции для разрешения кода доступа к данным, например:

public class CompositionRoot
{
    public void ComposeDependencies()
    {
        container.Register<IFooRepository, AsyncDbFooRepository>(Lifestyle.Scoped);

        // Not sure if the syntax is right, but it demonstrates the concept
        container.Register<FooService>(async () => new FooService(await GetFoos(container)));
    }

    private async Task<IEnumerable<Foo>> GetFoos(Container container)
    {
        var fooRepository = container.GetInstance<IFooRepository>();
        return await fooRepository.GetAll();
    }
}

Кроме того, в моем конкретном сценарии AsyncDbFooRepository требуется какой-то параметр времени выполнения для создания, а это означает, что вам нужна абстрактная фабрика для создания AsyncDbFooRepository.

С абстрактной фабрикой теперь мне приходится управлять жизненными циклами всех зависимостей под AsyncDbFooRepository (граф объекта под AsyncDbFooRepository не тривиален). У меня есть подозрение, что я неправильно использую DI, если выбираю второй дизайн.


Подводя итог, мои вопросы таковы:

  1. Я неправильно использую DI во втором дизайне?
  2. Как я могу удовлетворительно составить свои зависимости для моего второго проекта?

person rexcfnghk    schedule 29.06.2017    source источник
comment
Если Foo является данными времени выполнения, вы не должны вводить его в конструкторы ваших компонентов. Прочитайте это: cuttingedge.it/blogs/steven/pivot/entry.php ?id=99   -  person Steven    schedule 30.06.2017
comment
Спасибо за ссылку. Согласно вашему блогу, вместо этого я должен вводить AsyncDbFooRepository (первый дизайн), но не будет ли это также нарушением принципа инверсии зависимостей (как описано в вопросе)?   -  person rexcfnghk    schedule 30.06.2017
comment
Я бы порекомендовал вам использовать слой репозитория. У вас могут быть SqlFooRepository, FileSystemFooRepository, Neo4JFooRepository и т. д. Вы получаете преимущество изменения источника ваших данных. Если вы передаете IEnumerable‹Foo› в конструкторе, вы ограничиваете себя, например, добавляя данные обратно в репозиторий, удаляя и т. д.   -  person user1628733    schedule 05.07.2017
comment
@user1628733 user1628733 Мой второй дизайн по-прежнему позволяет переключать технологию доступа к данным при разрешении IFooRepository/IEnumerable<Foo>. Во-вторых, сервис не должен зависеть от абстракции (добавлять данные обратно в репозиторий), которую он не использует.   -  person rexcfnghk    schedule 07.07.2017


Ответы (2)


Одним из аспектов async/await является то, что он по определению должен применяться «до конца», как вы справедливо утверждаете. Однако вы не можете предотвратить использование Task<T> при введении IEnumerable<T>, как вы предлагаете во втором варианте. Вам нужно будет внедрить Task<IEnumerable<T>> в конструкторы, чтобы обеспечить асинхронное извлечение данных. При внедрении IEnumerable<T> это либо означает, что ваш поток блокируется при перечислении коллекции, либо все данные должны быть загружены во время построения графа объектов.

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

Я неправильно использую DI во втором дизайне?

Трудно сказать. IEnumerable<T> — это поток, поэтому вы можете считать его фабрикой, а это означает, что внедрение IEnumerable<T> не требует загрузки данных среды выполнения во время создания объекта. Пока это условие соблюдается, внедрение IEnumerable<T> может быть нормальным, но по-прежнему делает невозможным асинхронность системы.

Однако при введении IEnumerable<T> вы можете столкнуться с двусмысленностью, потому что может быть не очень ясно, что значит вводить IEnumerable<T>. Является ли эта коллекция потоком, который оценивается лениво или нет? Содержит ли он все элементы T. Является ли T данными среды выполнения или службой?

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

public interface IRepository<T> where T : Entity
{
    Task<IEnumerable<T>> GetAll();
}

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

Как я могу удовлетворительно составить свои зависимости для моего второго проекта?

Вы не можете. Для этого контейнер внедрения зависимостей должен иметь возможность асинхронно разрешать графы объектов. Например, для этого требуется следующий API:

Task<T> GetInstanceAsync<T>()

Но у Simple Injection нет такого API, как и у любого другого существующего DI Container, и это не зря. Причина в том, что конструкция объекта должна быть простой, быстрой и надежный, и вы теряете его при выполнении ввода-вывода во время построения графа объектов.

Таким образом, ваш второй дизайн не только нежелателен, его невозможно сделать, когда данные загружаются во время построения объекта, не нарушая асинхронность системы и не вызывая блокировку потоков при использовании контейнера внедрения зависимостей.

person Steven    schedule 05.07.2017
comment
Спасибо за подробное объяснение, если я добавлю предположение, что зависимость IEnumerable<T> всегда будет полной коллекцией (т. е. тип среды выполнения на самом деле является полностью перечисляемой коллекцией, ICollection<T>/ISet<T>), изменит ли это ваш ответ? - person rexcfnghk; 07.07.2017
comment
@rexcfnghk нет, не так. Пока вы вводите предварительно загруженные данные времени выполнения в компоненты вашего приложения, все будет довольно проблематично. Неважно, какую абстракцию вы используете для представления этих данных времени выполнения. - person Steven; 07.07.2017

Я стараюсь изо всех сил (до сих пор мне это удавалось каждый раз) не внедрять какие-либо службы, которые выполняют ввод-вывод, в мои модели предметной области, поскольку мне нравится, чтобы они оставались чистыми без побочных эффектов.

При этом второе решение кажется лучше, но есть проблема с сигнатурой метода public int Calculate(): он использует некоторые скрытые данные для выполнения вычислений, поэтому он не является явным. В таких случаях мне нравится передавать временные входные данные в качестве входного параметра непосредственно методу следующим образом:

public int Calculate(IEnumerable<Foo> foos)

Таким образом, очень ясно, что нужно методу и что он возвращает (на основе комбинации имени класса и имени метода).

person Constantin Galbenu    schedule 29.06.2017
comment
Я признаю, что метод Calculate не очень хороший пример, но, надеюсь, он демонстрирует мою проблему. - person rexcfnghk; 29.06.2017
comment
Также этот ответ, похоже, не отвечает на вопрос - person rexcfnghk; 29.06.2017
comment
Известны ли foos вызывающей стороне или они логически связаны с операцией Calculate? то есть как a и b в sum(a, b) - person Constantin Galbenu; 29.06.2017
comment
Да, foos известны вызывающему абоненту - person rexcfnghk; 30.06.2017
comment
Тогда я сохраняю свой ответ: их не следует вводить как зависимость, а явно отправлять как локальные параметры. Подумайте иначе: если бы вы каким-то образом заменили FooService на другой с таким же интерфейсом, вам все равно пришлось бы вводить foos или foos тоже можно было бы забыть/удалить из кода? - person Constantin Galbenu; 30.06.2017
comment
К сожалению, в моем случае это не сработает, поскольку FooService является декоратором для IFooService, а Calculate является одним из методов интерфейса. Извините, надо было уточнить об этом - person rexcfnghk; 30.06.2017
comment
Я пытаюсь сказать, что вы должны быть уверены, что foos не подходят в качестве входного параметра метода, и только после этого вы должны думать, как их внедрить. - person Constantin Galbenu; 30.06.2017
comment
Внедрение сервисов в агрегаты не является обычным явлением, но я думаю, что довольно часто внедрять сервисы в доменные сервисы, не так ли? - person plalx; 01.07.2017
comment
@plalx Я делаю это довольно часто на стороне чтения и в сагах, но никогда на стороне записи. - person Constantin Galbenu; 01.07.2017