假设我有一些DDD服务需要一些IEnumerable<Foo>
来执行一些计算。我想出了两个设计:
使用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);
}
}
不要依赖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
现在被迫将其签名更改为async Task<int> Calculate()
。这似乎违反了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。
总之,我的问题是:
答案 0 :(得分:2)
async / await的一个方面是按照定义需要在你正确陈述时“一直向下”应用。但是,在注入Task<T>
时,您无法阻止使用IEnumerable<T>
,正如您在第二个选项中所建议的那样。您必须向构造函数注入Task<IEnumerable<T>>
以确保异步检索数据。注入IEnumerable<T>
时,或者意味着在枚举集合时线程被阻塞 - 或者 - 在对象图构建期间必须加载所有数据。
然而,在对象图构造过程中加载数据是有问题的,因为我解释here的原因。除此之外,由于我们在这里处理数据集合,这意味着必须在每个请求中从数据库中提取所有数据,即使并非所有数据都可能需要甚至使用。这可能会导致相当大的性能损失。
我在第二次设计时是否错误地使用了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也没有,这是有充分理由的。原因是对象构造必须是simple,快速和reliable,并且在对象图构建期间进行I / O时会丢失它。
因此,不仅您的第二个设计不合适,在对象构造期间加载数据时也不可能这样做,而不会破坏系统的异步性并导致线程在使用DI容器时阻塞。
答案 1 :(得分:0)
我尽可能地尝试(直到现在我每次都成功)不注入任何在我的域模型中执行IO的服务,因为我喜欢保持它们纯净而没有副作用。
据说第二个解决方案似乎更好但是方法public int Calculate()
的签名存在问题:它使用一些隐藏的数据来执行计算,因此它不明确。在这种情况下,我喜欢将瞬态输入数据作为输入参数直接传递给这样的方法:
public int Calculate(IEnumerable<Foo> foos)
通过这种方式,非常清楚方法需要什么以及它返回什么(基于类名和方法名的组合)。