4

假设我有一些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); 
    }  
    

这第二个设计s在我看来,更好的是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 downFooService现在不得不将其签名更改为async Task<int> Calculate()。这似乎违反了dependency inversion principle

但是,第二个设计也存在问题。首先,你必须依靠的DI容器(使用简单注射器在这里举例),或组成根源上解决,如数据访问代码:

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

随着抽象工厂,现在我必须管理所有依赖AsyncDbFooRepositoryAsyncDbFooRepository下的对象图是不平凡的)的生命周期。我有一种预感,如果我选择第二种设计,我会错误地使用DI。


总之,我的问题是:

  1. 我在我的第二个设计不当使用DI?
  2. 我该如何为我的第二个设计令人满意地构建我的依赖关系?
+0

如果Foo是运行时数据,则不应将其注入到组件构造函数中。阅读:https://cuttingedge.it/blogs/steven/pivot/entry.php?id = 99 – Steven

+0

感谢您的链接。根据你的博客,我应该注入'AsyncDbFooRepository'而不是(首次设计),但是这不会违反依赖倒置原则(如问题所述)吗? – rexcfnghk

+0

我会建议你使用存储库层。您可以拥有SqlFooRepository,FileSystemFooRepository,Neo4JFooRepository等。您可以从数据更改位置获取好处。如果您在构造函数中通过IEnumerable ,则会限制自己,例如将数据添加回存储库,删除等。 – user1628733

回答

2

async/await的一个方面是,它的根据定义需要应用“一路下来”,因为你正确地说。但是,在注射IEnumerable<T>时,您仍然无法阻止使用Task<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>() 

,但简单的注射却没有这样的API,而且也不现有的DI容器任何其他,这就是很好的理由。原因在于对象构造必须是simple,fast和reliable,并且在对象图构造期间执行I/O时会丢失该对象。

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

+0

感谢您的详细解释,如果我添加一个假设,IEnumerable 依赖将永远是一个完整的集合(即运行时类型实际上是一个完全枚举的集合,'ICollection '/'ISet ')是否会改变你的答案? – rexcfnghk

+1

@rexcfnghk不,它不。只要您将预加载的运行时数据注入到应用程序组件中,事情就会变得更加棘手。用什么抽象表示这个运行时数据并不重要。 – Steven

0

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

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

public int Calculate(IEnumerable<Foo> foos) 

通过这种方式,很清楚什么方法需要和返回结果是什么(基于组合类名称和方法名称)。

+0

我承认'Calculate'方法不是一个很好的例子,但希望它能证明我的问题。 – rexcfnghk

+0

此外,这个答案似乎并没有回答问题 – rexcfnghk

+0

调用者已知'foos'还是逻辑上绑定到'Calculate'操作?也就是说,在'sum(a,b)'中有'a'和'b'' –

相关问题