我应该将存储库接口与域模型分离吗

Should I decouple the repository interface from the domain model

假设我有一些 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通过网络执行异步IO时,通常需要使用async-await,例如:

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

但是你需要async all the way down, FooService is now forced to change its signature to async Task<int> Calculate(). This seems to violate the dependency inversion principle

但是,第二个设计也有问题。首先,您必须依赖 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 需要某种运行时参数来构造,这意味着您需要一个 abstract factory 来构造 AsyncDbFooRepository

有了抽象工厂,现在我必须管理 AsyncDbFooRepository 下所有依赖项的生命周期(AsyncDbFooRepository 下的对象图并不简单)。我有一种预感,如果我选择第二种设计,我会错误地使用 DI。


总而言之,我的问题是:

  1. 我在第二个设计中是否错误地使用了 DI?
  2. 如何为我的第二个设计令人满意地组合我的依赖项?

我尽可能地尝试(直到现在我每次都成功)不在我的域模型中注入任何执行 IO 的服务,因为我希望保持它们的纯净而没有副作用。

话虽这么说,第二种解决方案似乎更好,但方法 public int Calculate() 的签名存在问题:它使用一些 hidden 数据来执行计算,所以它不是明确的。在这种情况下,我喜欢将瞬态输入数据作为输入参数直接传递给这样的方法:

public int Calculate(IEnumerable<Foo> foos)

这样就很清楚这个方法需要什么了returns(基于class名称和方法名称的组合)

async/await 的一个方面是 根据定义 需要应用 "all the way down" 正如您所说的那样。但是,正如您在第二个选项中所建议的那样,您不能在注入 IEnumerable<T> 时阻止使用 Task<T>。您必须将 Task<IEnumerable<T>> 注入构造函数以确保异步检索数据。注入 IEnumerable<T> 时,这意味着您的线程在枚举集合时被阻塞 - 或者 - 在对象图构造期间必须加载所有数据。

然而,由于我解释的原因 here,在对象图构造期间加载数据是有问题的。除此之外,由于我们在这里处理数据集合,这意味着必须在每次请求时从数据库中获取所有数据,即使可能不需要甚至使用所有数据。这可能会导致相当大的性能损失。

Am I using DI incorrectly in my second design?

这很难说。 IEnumerable<T> 是一个流,因此您可以将其视为一个工厂,这意味着注入 IEnumerable<T> 不需要在对象构造期间加载运行时数据。只要满足该条件,注入 IEnumerable<T> 可能没问题,但仍然无法使系统异步。

但是,当注入 IEnumerable<T> 时,您可能会产生歧义,因为注入 IEnumerable<T> 的含义可能不是很清楚。该集合是否是延迟评估的流?是否包含T的所有元素。 T 是运行时数据还是服务?

为防止这种混淆,将此运行时信息的加载移到抽象后面通常是最好的做法。为了让您的生活更轻松,您也可以使存储库抽象成为通用的:

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

这允许您拥有一个通用的实现并为系统中的所有实体进行一次注册。

How can I compose my dependencies satisfactorily for my second design?

你不能。为此,您的 DI 容器必须能够异步解析对象图。例如,它需要以下 API:

Task<T> GetInstanceAsync<T>()

但是 Simple Injection 没有这样的 API,任何其他 现有的 DI 容器也没有,这是有充分理由的。原因是对象构造必须是 simple, fast and reliable 并且在对象图构造期间执行 I/O 时会丢失它。

因此,不仅您的第二种设计不受欢迎,而且在对象构造期间加载数据时,也不可能在不破坏系统的异步性并导致线程在使用 DI 容器时阻塞的情况下这样做。