域代码是否认为它需要所需的数据是一种不好的做法?

时间:2012-10-04 18:58:22

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

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

域模型 DM 感兴趣的数据可通过域实体&传递到 DM #39; s 导航属性或上层(即 UI层服务层)。

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

如果此存储库持久层完全分离,那么我们的实体不会违反 PI ,因为它仍然不知道它是如何获取数据的,它只知道它通过从存储库请求数据来获取数据:

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

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

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

回复Sebastian Good:

1)

  

我们的想法是,您的域名模型不应该关注细节   关于数据的来源。

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

  

2)你仍然需要决定如何加载这些数据,但是你做了   您的应用服务" (通常)担心它。

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

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

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

回复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

因此,如果最好放置逻辑来确定域中所需的确切数据实例,我应该封装对服务 <内存储库的访问权限strong> S 然后将 S 作为参数传递给域实体的方法?因此,在我们的示例中,我应封装对OrderRepository服务中ordersSelectorService的访问权限,然后将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)通过&#34; 特定行为&#34;你指的是域实体(即Customer)?!

b)我并不完全确定&#34; 应用服务的含义是什么,提供该服务的抽象作为参数&#34;。也许这不是提供服务 S 本身(即OrderRepository)作为方法的参数(即Customer.InterestedWhatOtherCustomerOrdered),我们应该有一些 class C (即OrdersSelectorService)封装 S ,然后将 C 作为参数传递给方法?

c)我假设 C (封装 S &lt; - 见 b)问题的类))应始终为应用程序服务 S 应始终由 C 封装(除非 S 已经是应用程序服务)?如果是,为什么?

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 而不是传递给OrdersSelectorServiceCustomer.InterestedWhatOtherCustomerOrdered。 em>) Customer.InterestedWhatOtherCustomerOrdered ?但据我所知,这会违反 Persistence Ignorance 规则(参见我之前的thread

2)

  

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

啊哈,我错误地认为您建议 C 应该是应用服务。无论如何, C 应该在哪里生活?是应该在 Application Services程序集中还是在域模型程序集中打包?

3)

  

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

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

4)我不是故意造成任何麻烦或暗示你们其中一个人是错的(你们两个人都已经推理过你们为什么喜欢你的设计),但我只是想确定我完全了解你的帖子。事实上,你建议与 nwang0 建议的相反(即,如果你们两个人都推荐相同的东西,那么我的理解能力需要一些修复:o)?!< / p>

谢谢

3 个答案:

答案 0 :(得分:2)

我们的想法是,您的域模型不应该关注数据来自何处的详细信息。您仍然需要决定如何加载这些数据,但是您的“应用程序服务”(通常)会担心它。通过这种方式,他们可以管理数据持久性,缓存,安全性等的无数复杂性,同时您的域对象担心其域逻辑。

或者,另一个令人信服的论点是它违反了单一责任原则。现在,您的域对象负责确定自己的逻辑,并确定如何请求其数据。

答案 1 :(得分:1)

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

相反,正如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 :(得分:1)

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

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

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

在您的示例中,您有:

var orders = repository.Find...;

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

var orders = repository.FindByDate(productIdThisCustomerLike);

我假设productIdThisCustomerLike是Customer的私有字段。

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

您的意见回答:

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

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