0

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

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

但我还假设,在特定域实体必须动态决定它需要什么数据的情况下,该实体通过诸如Repository之类的组件请求该数据是完全可以接受的。

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

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

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

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

回复塞巴斯蒂安·古德:

1)

这个想法是你的域模型不应该关心数据来自哪里的细节。

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

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

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

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

也就是说,不让应用层服务单独负责加载数据意味着我们随时更改域实体的内部逻辑(例如,现在该实体 需要不同类型的数据)也意味着我们必须更改应用程序服务因此,他们现在将向实体提供新类型的数据而不是旧数据?!

回复 Eulerfx:

1)

一个)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

因此,如果最好放置逻辑以确定域中所需的确切数据实例,我应该封装对服务S内的存储库的访问,然后将S作为参数传递给域实体的方法?因此,在我们的示例中,我应该封装对内部服务的访问,然后作为参数传递给: OrderRepositoryordersSelectorServiceordersSelectorServiceCustomer.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);
                ...

  }
}

OrderRepositoryb)如果这确实是您的建议,那么除了简单地作为参数传递给之外,还有其他好处(除了您已经提到的那些)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)我不确定您所说的“应用服务提供该服务的抽象作为参数”是什么意思。也许我们不应该提供服务 S本身(即OrderRepository)作为方法(即Customer.InterestedWhatOtherCustomerOrdered)的参数,而应该让一些 C(即OrdersSelectorService)封装S,然后将C作为参数传递给方法?

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

d)

这样,对服务的依赖在方法签名中明确说明。

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

3) 有点跑题了,但是当我们将行为B作为方法M ( ) 的参数注入到类C中时就会出现这种情况,那么我们不称它为依赖注入,而是通过构造函数设置器将B注入到C中( ),那么我们称之为依赖注入。这是为什么?C.M(B b);B b=new B();C c=new C(b);

对 Eulerfx 的第二次回复:

1)

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

我假设您的意思是,我们应该在Customer.InterestedWhatOtherCustomerOrdered中使用Linq-to-Entities(严重依赖lambda)而不是传递OrdersSelectorService给?但据我所知,这将违反持久性无知规则(请参阅我以前的帖子Customer.InterestedWhatOtherCustomerOrdered

2)

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

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

3)

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

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

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

谢谢你

4

3 回答 3

2

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

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

于 2012-10-04T20:32:28.997 回答
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 对象中创建 Repository.Find 并传入一些本地信息是很自然的。如果我们选择在应用服务层调用repository.Find,我们需要从客户那里提取产品ID信息。它会破坏封装,因此是一个邪恶的解决方案。

回复您的评论:

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

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

于 2012-10-07T22:24:31.547 回答
1

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

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

更新

1ab) 是的,这是正确的。另一种选择是使用 lambda 而不是OrdersSelectorService. 如果 lambda 在您的语言中不可用,那么它应该是一个接口。优于传递的好处OrderRepository是基于接口隔离原则,其目标是减少不必要的耦合。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));

  }
}

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

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

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

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

于 2012-10-06T00:44:23.633 回答