【问题标题】:Is it considered a bad practice for domain code to request data it needs?域代码请求它需要的数据是否被认为是一种不好的做法?
【发布时间】:2012-10-04 18:58:21
【问题描述】:

域实体不应包含与持久性相关的代码,因此它们应该是持久性无知 PI

域模型DM感兴趣的数据可以通过域实体'传递给DM s 导航属性或由上层(即UI层或服务层)。

但我也假设在特定的域实体必须动态决定它需要什么数据的情况下,该实体通过组件请求数据是完全可以接受的,例如作为存储库。

如果这个Repository与持久层完全解耦,那么我们的entity没有违反PI,因为它仍然不知道它是如何获取数据的,它只知道它通过从 Repository 请求数据来获取数据:

class Customer
{
       public string InterestedWhatOtherCustomerOrdered( ... )
       {
                ...
                var orders = repository.Find...;
                ...
        }
       ...
}

因此,为什么 域代码 也能够从 Repository 请求它需要的数据而不是仅仅从任一上层接收它被认为是一种不好的做法图层或导航属性?

即,即使根据 Fowler(PEAA 关于 Data Mapper 的章节),也可以从 Data Mapper 中提取所需的任何方法将域代码转换成一个接口类,然后域代码可以使用。

回复塞巴斯蒂安·古德:

1)

这个想法是你的域模型不应该关心细节 关于这些数据的来源。

但如果域实体遵守 PI 规则,那么我们可以说他们不知道数据实际来自何处的详细信息。

2) 您仍然需要决定如何加载该数据,但是您需要 您的“应用程序服务”(通常)会担心它。

a) 假设 现实世界的实体 确实具有搜索特定数据的功能,您是否仍然认为 域实体 请求数据存在问题(我很抱歉,我知道很难回答这些一般性问题)?

b) 最重要的是,我很难理解应用程序服务层 是如何预测域实体 可能需要处理的所有不同类型的数据。

也就是说,不让应用层服务单独负责加载数据意味着我们随时更改域实体的内部逻辑(这样现在entity 需要不同类型的数据)也意味着我们必须相应地更改 应用程序服务,以便它们现在向 entity 提供新的数据类型而不是旧数据类型?!

回复 Eulerfx:

1)

a)The application service can provide not only data, but a mechanism for retrieving data as well, in cases where it is better to place logic for determining the exact instance of data needed in the domain

因此,如果最好放置逻辑来确定域中所需的确切数据实例,我应该将对 repository 的访问封装在 service S,然后将 S 作为参数传递给 域实体 的方法?因此,在我们的示例中,我应该在ordersSelectorService 服务中封装对OrderRepository 的访问,然后将ordersSelectorService 作为参数传递给Customer.InterestedWhatOtherCustomerOrdered:

class Customer
{
       public string InterestedWhatOtherCustomerOrdered(OrdersSelectorService ordersSelectorService)
       {
                ...
                var orders = ordersSelectorService.Select...;
                ...
        }
        ...
}



class CustomerService
{
  OrdersSelectorService ordersSelectorService;
  CustomerRepository customerRepository;

  public void ()
  {
        var customer = this.customerRepository.Get...;
                ...

        customer.InterestedWhatOtherCustomerOrdered(ordersSelectorService);
                ...

  }
}

b) 如果这确实是您的建议,那么除了简单地将OrderRepository 作为参数传递给Customer.InterestedWhatOtherCustomerOrdered,还有其他好处(除了您已经提到的那些):

class Customer
{
       public string InterestedWhatOtherCustomerOrdered(CustomerRepository orderRepository)
       {
                ...
                var orders = orderRepository.Select...;
                ...
       }
       ...
}

2) 以下问题只是为了确保我完全正确理解了您的帖子:

So if a specific behavior requires access to some service, have the application service provide an abstraction of that service as an argument to the corresponding behavior method. This way, the dependency upon the service is explicitly stated in the method signature.

a) “特定行为”您指的是域实体(即Customer)?!

b) 我不确定您所说的“应用服务提供该服务的抽象作为参数”是什么意思。也许我们不应该提供 service S 本身(即OrderRepository)作为方法的参数(即Customer.InterestedWhatOtherCustomerOrdered),而是应该有一些类 C(即OrdersSelectorService)封装S,然后将C作为参数传递给方法?

c) 我假设 C (封装 S b) 问题的类)应该始终是一个 application service 和 S 应始终由 C 封装(除非 S 已经是 application service )?如果是,为什么?

d)

这样,对服务的依赖在 方法签名。

通过在方法签名中明确声明对服务的依赖,我们可以获得什么好处?只有我们可以立即知道方法在做什么而不需要检查方法的代码?

3) 有点离题,但是当我们将行为 B 注入到类 C 作为方法 M 的参数时,它就会出现( C.M(B b); ),那么我们就不叫它依赖注入,而是通过构造函数B注入C /em> 或 setter (B b=new B();C c=new C(b);),那么我们称之为依赖注入。这是为什么呢?

第二次回复 Eulerfx:

1)

1ab) ... 另一种选择是使用 lambda 而不是 OrdersSelectorService。

我假设您的意思是,我们应该使用 Linq-to-Entities (严重依赖 lambda )而不是传递给 OrdersSelectorService 到 Customer.InterestedWhatOtherCustomerOrdered Customer.InterestedWhatOtherCustomerOrdered?但据我所知,这将违反 Persistence Ignorance 规则(参见我之前的 thread)

2)

2c) 不,C 应该只是一个包含所需的接口 方法。服务 S 可以实现该接口,或者 可以即时提供实现。

啊哈,我误以为你在建议 C 应该是一个应用程序服务。无论如何,C应该住在哪里?它应该打包在 Application Services 程序集 中还是域模型程序集 中?

3)

2d) ... 在方法签名中声明依赖关系的好处 与类本身的构造函数相反的是......另一个 好处是您的域类不需要成为其中的一部分 来自 IoC 容器的依赖图 - 让事情变得更简单。

还不太了解IoC,因此我必须问一下域类究竟是如何成为IoC的依赖图的一部分 ?换句话说,这个域类必须在IoC的配置层中指定(我以为这个层只是用来指定一个依赖的接口之间的映射 和一个实际的依赖项的实现,因此我假设依赖类 甚至没有在这一层中提及)或者...?

4) 我并不是要引起任何麻烦或暗示你们中的一个人是错的(你们俩都已经解释了为什么你更喜欢你的设计),但我想确保我完全理解你的帖子.实际上,您的推荐与 nwang0 的建议正好相反(即,如果你们两个都推荐相同的东西,那么我的理解能力需要一些修复:o)?!

谢谢

【问题讨论】:

  • 嗨,我认为让您的一个实体使用存储库是完全有效的,毕竟两者都是域模型的一部分。关于您在问题中所说的内容,我看不到任何部分说实体不应使用存储库。调用存储库的实体是 Persistence Ignorant,因为它不知道如何存储实体或使用哪种技术。
  • @Yves Reynhout:您能否详细说明“明确依赖关系”是什么意思?
  • 隐式依赖在类外是不可发现的。 IE。它们将在类中实例化。从类外部可见显式依赖关系。通常这是使用依赖注入 (DI) 来处理的,它通常只是简单地设计接口并通过类构造函数传递您的依赖项。在您的情况下,您可能会通过构造函数传递一个 ICustomerRepository 实例。如果您使用 IoC 容器,这将负责自动更新和传递这些依赖项。
  • 或者只是将它传递给方法,或者让域服务承担这种依赖关系并与其他对象协作。
  • 我个人不赞成从实体/域对象访问我认为是“技术资源”的东西,即使它们隐藏在接口后面。接下来你会做什么,从域模型发送一封电子邮件?写文件?毕竟,它在一个接口的后面。无论如何,您提到“导航属性”。我假设您的意思是遍历对象的依赖关系图。我只是想知道您是否可能对数据库中从实体 A 到相关事物 B 的关系进行建模。如果你可以这样做,那么你可以使用你的持久性框架来解决关联。

标签: design-patterns domain-driven-design repository-pattern


【解决方案1】:

这个想法是,您的域模型不应该关心有关数据来自何处的详细信息。您仍然必须决定如何加载该数据,但是您让“应用程序服务”(通常)担心它。通过这种方式,他们可以管理数据持久性、缓存、安全性等无数复杂问题,而您的域对象则担心它们的域逻辑。

或者,另一个令人信服的论点是它违反了单一责任原则。现在你的领域对象负责搞清楚它自己的逻辑,以及搞清楚如何请求它的数据。

【讨论】:

    【解决方案2】:

    域对象请求他们需要的数据并不是坏习惯,但是将存储库依赖项直接注入实体通常被认为是不好的做法。造成这种情况的一个原因是,现在您的域对象成为依赖关系图的一部分,这是一种不必要的复杂性。此外,存储库通常带有环境依赖项,例如事务和工作单元。这增加了复杂性并使关于域逻辑的推理更加困难。

    相反,正如 Sebastian Good 所指出的,最好让应用程序服务提供实体所需的数据。应用程序服务是注入存储库和其他gateways 的好地方。应用程序服务不仅可以提供数据,而且还可以提供一种检索数据的机制,以便更好地放置逻辑以确定域中所需的确切数据实例。例如,看看这个question。因此,如果特定行为需要访问某些服务,则让应用程序服务提供该服务的抽象作为相应行为方法的参数。这样,对服务的依赖就会在方法签名中明确说明。

    更新

    1ab) 是的,这是正确的。另一种选择是使用 lambda 而不是 OrdersSelectorService。如果 lambda 在您的语言中不可用,那么它应该是一个接口。通过OrderRepository 的好处是基于interface segregation principle,其目标是减少不必要的耦合。 Customer 上的行为不太可能需要 OrderRepository 上的所有方法,而是需要特定的函数,因此请明确说明。

    2a) 是的,我所指的行为是 Customer 实体上的行为,它只是类上的方法之一。

    2b) 是的,原因见 1ab。

    2c) 不,C 应该只是一个包含所需方法的接口。服务 S 可以实现该接口,也可以即时提供实现。

    2d) 是的。这是支持依赖注入而不是服务位置的论点的一部分。相对于类本身的构造函数而言,在方法签名中声明依赖关系的一个好处是,该服务通常只需要一个方法,并且使其成为类的成员是一种浪费。另一个好处是您的域类不需要成为 IoC 容器的依赖关系图的一部分 - 让事情变得更简单。

    3) 我将两者都称为依赖注入 (DI)。 DI 旨在对比服务定位,其中类构造函数或方法将负责通过服务定位器获取所需的服务。

    更新 2

    1) 这是一个 C# 代码示例:

    // this is is the repository, but it doesn't have to be an interface, just some class encapsulating data access
    interface IOrderRepository
    {
      Order Get(string id);
      void Add(Order order);
      IEnumerable<Order> GetOrdersBySomeCriteria(SomeCriteria criteria);
    }
    
    class Customer
    {
       // the selector parameter is a lambda.
       public string InterestedWhatOtherCustomerOrdered(Func<SomeCriteria, IEnumerable<Order>> selector)
       {
          // do stuff with selector lambda
       }
    }
    
    // this is the app service
    class CustomerApplicationService
    {
      readonly IOrderRepository orderRepository;
    
      public void DoSomething()
      {
         var customer = this.customerRepository.Get ...;
    
         // the app service passes lambda which in turn points to repository.
         var result = customer.InterestedWhatOtherCustomerOrdered(criteria => this.orderRepository.GetOrdersBySomeCriteria(criteria));
    
      }
    }
    

    这不违反持久性无知并且非常解耦。 InterestedWhatOtherCustomerOrdered 方法上的 lambda 参数准确地指定了该方法需要什么——仅此而已。它并不关心该功能是如何提供的,它就是这样。

    2) 对于 lamda,C 并不真正存在于任何地方,因为它是由 lambda 完整指定的。但是,如果您要使用接口,例如IOrderSelector,则需要在存在 Customer 聚合的位置声明该接口。它可以由OrderRepository 直接实现,也可以有一个adapter 类。

    3) 我提到 IoC 的原因是因为另一种方法是在 Customer 类的构造函数中声明对顺序选择器的依赖关系。然后,每当创建该类的新实例时,就需要注入该依赖项(顺序选择器)。一种方法是在 Customer 类被实例化的地方使用 IoC 容器。这是有问题的原因是因为现在您必须确保无论您在哪里实例化 Customer 类都可以访问 IoC 容器。这也是责任错位,因为创建客户与订单选择器无关,只有一种行为需要它。

    4) 我想这是哲学的不同。由于上述原因以及其他原因,我不喜欢让域对象引用存储库。总体而言,如果您浏览 SO 或博客等,通常会不赞成。存储库接口确实在域中声明,但这并不意味着它们应该直接从域实体中引用。

    【讨论】:

    • 我很抱歉,但如果你愿意帮助我更多,你能看到我最后的编辑吗?
    • 明天无论如何我都会标记你的帖子,因为你已经给了我很大的帮助,但是如果你有时间,你能看到我最后的编辑吗(之后不会有更多的编辑了) ?
    • 2)"但是,如果您要使用一个接口,例如 IOrderSelector,则需要在存在 Customer 聚合的地方声明该接口。它可以由 OrderRepository 直接实现,或者您可以有一个适配器类。”
    • a) 我认为 C 应该驻留在客户聚合存在的地方(比如域程序集)而不是服务层(即应用程序服务程序集)存在的地方的原因是因为服务层应该依赖于域层,但是如果 C 存在于 Application Service 组件中,那么域层将依赖于服务层?! b)我假设适配器类也应该驻留在域程序集中,原因如上所述?
    • a) 是的。 b) 适配器类实际上可以在应用层中,因为应用层将是实例化它的那个。但是,适配器类必须实现在域层中声明的接口,原因与 a) 相同。
    【解决方案3】:

    域对象依赖存储库对象并没有错。实际上,Repository 对象属于领域模型,Repository 接口应该与其他领域对象打包在一起。

    但是,保持存储库接口抽象而不与它们的具体实现方式耦合是至关重要的。 IE。您的 OrderRepository 应该有一个类似语义的集合并使用规范。这篇文章有一些构建/使用存储库的好例子。 http://thinkinginobjects.com/2012/08/26/dont-use-dao-use-repository/

    另一方面,我认为从上层接收值是一个不太好的解决方案,假设上层是指应用服务层。

    在您的示例中,您有:

    var orders = repository.Find...;
    

    在现实生活中,您需要将一些信息传递到存储库中以查找相关订单。我在这里举个例子:

    var orders = repository.FindByDate(productIdThisCustomerLike);
    

    我假设 productIdThisCustomerLike 是客户的私有字段。

    在 Customer 对象中创建 Repository.Find 并传入一些本地信息是很自然的。如果我们选择在应用服务层调用repository.Find,我们需要从客户那里提取产品ID信息。它会破坏封装,因此是一个邪恶的解决方案。

    对您的 cmets 的回答:

    1. 无需使用服务包装存储库。我认为让域对象依赖于服务对象是一种不好的做法,因为服务层依赖于域模型层,而不是相反。如果您需要对返回的订单列表进行一些后期处理(如过滤、分组或合并),请在您的 Customer 和 OrderRepository 之间引入另一个域对象,并将其命名为域对象,而不是服务。

    2. 这取决于您的用例。如果 Customer.InterestedWhatOtherCustomerOrdered 由您的服务层直接调用,则可以从服务层传入 Repository 引用。但是,如果它被另一个域对象(例如 ShoppingCart)调用,相同的方法将强制 ShoppingCart 知道 OrderRepository 只是为了给它 Customer。一般来说,我更喜欢让域对象保留对他们需要的存储库的引用。

    【讨论】:

    • 1 - 我现在有点困惑,因为这似乎违背了 eulerfx,它建议(如果我理解正确的话)将存储库包装在服务中,然后将服务作为参数传递给客户的方法?!我错过了什么? 2 - 您对将存储库作为参数传递给 Customer.InterestedWhatOtherCustomerOrdered 的应用程序服务是否满意?
    • 感谢一百万帮助我更好地理解这一点
    猜你喜欢
    • 2015-10-29
    • 1970-01-01
    • 1970-01-01
    • 2017-12-18
    • 1970-01-01
    • 2022-12-22
    • 2017-07-28
    • 1970-01-01
    相关资源
    最近更新 更多