繁体   English   中英

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

[英]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
    }
}

但是当你需要一直异步时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. 如何为我的第二个设计令人满意地编写我的依赖项?

async / await的一个方面是它根据定义需要在你正确陈述时“一直向下”应用。 但是,在注入IEnumerable<T> ,无法阻止使用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 Container,这是有充分理由的。 原因是对象构造必须简单 ,快速和可靠,并且在对象图构建期间进行I / O时会丢失。

因此,不仅您的第二个设计不合需要,在对象构造期间加载数据时也不可能这样做,而不会破坏系统的异步性并导致线程在使用DI容器时阻塞。

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

这就是说第二个解决方案似乎更好但是方法的签名有问题public int Calculate() :它使用一些隐藏数据来执行计算,因此它不是显式的。 在这种情况下,我喜欢将瞬态输入数据作为输入参数直接传递给这样的方法:

public int Calculate(IEnumerable<Foo> foos)

通过这种方式,非常清楚方法需要什么以及它返回什么(基于类名和方法名的组合)。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM