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

Допустим, у меня есть служба 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. Как я могу удовлетворительно составить свои зависимости для моего второго проекта?

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();
}

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

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

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

Task<T> GetInstanceAsync<T>()

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

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

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

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

public int Calculate(IEnumerable<Foo> foos)

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

Другие вопросы по тегам