领域驱动设计:领域服务,应用服务。

380

请问有人可以通过提供一些示例来解释域和应用程序服务之间的区别吗? 如果一个服务是域服务,我是否应该将该服务的实际实现放在域程序集中,如果是这样,我是否还应该将存储库注入到该域服务中? 一些信息会非常有帮助。

能不能请您解释一下什么是“注入”和“程序集”,这样我才能更好地进行翻译呢?

请随意查看此链接:youtu.be/MfEpw2WXXyk - Alireza Rahmani Khalili
8个回答

522

服务分为三种类型:域服务应用服务基础设施服务

  • 域服务:封装了不适合在域对象中自然存在的业务逻辑,并且不是典型的CRUD操作——这些操作将属于存储库
  • 应用服务:供外部消费者使用以与您的系统通信(考虑Web服务)。如果消费者需要访问CRUD操作,则会在此处公开。
  • 基础设施服务:用于抽象技术问题(例如 MSMQ、电子邮件提供程序等)。

将域服务与域对象一起保留是明智的——它们都专注于领域逻辑。是的,您可以将存储库注入到您的服务中。

应用服务通常会同时使用域服务存储库来处理外部请求。


6
在CQRS中,您会将命令和查询放在哪里?哪个服务生成它们,哪个服务处理它们? - inf3rno
19
我认为应用程序服务应该独立于技术细节,比如“Web服务”,它们被这样的服务所使用。请参阅《领域驱动设计中的服务》(http://gorodinski.com/blog/2012/04/14/services-in-domain-driven-design-ddd/)。 - deamon
3
在使用任何类型的服务类之前,请三思而后行。很有可能你可以不用它们,这样你的代码就会变得更加清晰和可维护。参考链接:https://hackernoon.com/you-dont-need-a-domain-service-class-in-ddd-9ecd3140782 - Vadim Samokhin
2
这是一个很好的答案,但它需要引用书籍和作者(使其变得更好)。 - carloswm85

141

(如果您不想阅读,底部有摘要 :-))

我也曾经苦恼于应用程序服务的精确定义。虽然一个月前Vijay的答案对我的思考过程非常有帮助,但我现在不同意其中的一部分。

其他资源

关于应用程序服务的信息很少。像聚合根、仓储和领域服务这样的主题被广泛讨论,但是应用程序服务只是简单地提到或者根本没有提到。

MSDN Magazine文章《领域驱动设计入门》将应用程序服务描述为将您的领域模型转换和/或公开给外部客户端的一种方式,例如作为WCF服务。这也是Vijay描述应用程序服务的方式。从这个角度来看,应用程序服务是您领域的接口。

Jeffrey Palermo关于洋葱架构的文章(第123部分)是不错的阅读材料。他将应用程序服务视为应用程序级别的概念,例如用户会话。虽然这更接近我的应用程序服务理解,但仍与我的思考不一致。

我的想法

我认为应用程序服务是应用程序提供的依赖项。在这种情况下,应用程序可以是桌面应用程序或WCF服务。

领域

现在是时候举个例子了。首先从您的领域开始。所有实体以及不依赖外部资源的任何领域服务都在此处实现。任何依赖于外部资源的领域概念都由一个接口定义。以下是可能的解决方案布局(粗体表示项目名称):

My Solution
- My.Product.Core (My.Product.dll)
  - DomainServices
      IExchangeRateService
    Product
    ProductFactory
    IProductRepository

ProductProductFactory 类已经在核心程序集中实现了。 IProductRepository 是由数据库支持的东西。其实现不是领域的问题,因此由一个接口定义。

现在,我们将关注 IExchangeRateService。这个服务的业务逻辑是由外部Web服务实现的。但是,它的概念仍然是领域的一部分,并由此接口表示。

基础设施

外部依赖的实现是应用程序基础设施的一部分:

My Solution
+ My.Product.Core (My.Product.dll)
- My.Product.Infrastructure (My.Product.Infrastructure.dll)
  - DomainServices
      XEExchangeRateService
    SqlServerProductRepository

XEExchangeRateService 通过与xe.com通信实现了IExchangeRateService 领域服务。此实现可以被使用您的领域模

public class CachingExchangeRateService : IExchangeRateService
{
    private IExchangeRateService service;
    private ICache cache;

    public CachingExchangeRateService(IExchangeRateService service, ICache cache)
    {
        this.service = service;
        this.cache = cache;
    }

    // Implementation that utilizes the provided service and cache.
}

注意到ICache参数了吗?这个概念不属于我们的领域,因此它不是一个领域服务。它是一个应用服务,是我们基础设施的依赖项,可能由应用程序提供。让我们引入一个演示这个概念的应用程序:

我的解决方案
- My.Product.Core (My.Product.dll)
  - 领域服务
      IExchangeRateService
    产品
    ProductFactory
    IProductRepository
- My.Product.Infrastructure (My.Product.Infrastructure.dll)
  - 应用服务
      ICache
  - 领域服务
      CachingExchangeRateService
      XEExchangeRateService
    SqlServerProductRepository
- My.Product.WcfService (My.Product.WcfService.dll)
  - 应用服务
      MemcachedCache
    IMyWcfService.cs
  + MyWcfService.svc
  + Web.config

所有这些在应用程序中汇集在一起:

// Set up all the dependencies and register them in the IoC container.
var service = new XEExchangeRateService();
var cache = new MemcachedCache();
var cachingService = new CachingExchangeRateService(service, cache);

ServiceLocator.For<IExchangeRateService>().Use(cachingService);

概述

一个完整的应用程序由三个主要层组成:

  • 领域层
  • 基础设施层
  • 应用层

领域层包含领域实体和独立的领域服务。任何依赖于外部资源的领域概念(包括领域服务和存储库)都是通过接口定义的。

基础设施层包含来自领域层的接口实现。这些实现可能引入新的非领域依赖项,需要应用程序来提供。这些是应用程序服务,并由接口表示。

应用层包含应用程序服务的实现。如果基础设施层提供的实现不足够,应用层还可以包含领域接口的附加实现。

尽管这种观点可能与DDD中服务的一般定义不匹配,但它确实将领域与应用程序分开,并允许您在多个应用程序之间共享领域(和基础架构)程序集。


3
您需要根据请求模型重建或填充您的领域模型,并将领域模型传递给领域服务。此问题 可能会为您提供一些想法。如果不行,请告诉我,我会看看是否有时间回答其他问题。 - Niels van der Rest
@dario-g:抱歉,我可能忽略了你的问题。你能否创建一个新的问题,并用示例清楚地展示你的问题? - Niels van der Rest
1
@Tiendq:你是指 IExchangeRateService 接口吗?这是一个领域概念,也就是包含在客户通用语言中的内容。你的领域的其他部分可能依赖于此服务,因此它的接口定义在领域层中。但由于其实现涉及外部 Web 服务,因此实现类位于基础设施层。这样,领域层只关注业务逻辑。 - Niels van der Rest
4
传统的分层架构通常是领域无关的基础架构。但在洋葱架构中(请参见我的答案中的链接),基础架构实现了领域的外部依赖关系。但我不会说基础架构依赖于领域,它只是引用它。我从洋葱架构中采用了“基础架构”一词,但“外部依赖项”可能更合适。 - Niels van der Rest
14
我不同意你对Vijay持相反观点的部分,原因在于:CachingExchangeRateService是基础设施问题。尽管您通常接受ICache,但该ICache的实现取决于所涉及的技术(例如Web、Windows)。仅仅因为它是通用的并不意味着它是应用程序服务。应用程序服务是您域的API。如果您想向编写应用程序的其他人公开您的域,他们将使用什么?应用程序服务,而他们可能不需要缓存,因此您的缓存实现对他们毫无用处(这就是为什么它是基础设施) 。 - Aaron Hawkins
显示剩余5条评论

70
我理解最深入的关于应用服务和领域服务的区别的资源是Eric Evans的cargo示例的Java实现,可以在这里找到。如果你下载它,你可以查看RoutingService(一个领域服务)以及BookingService、CargoInspectionService(这些是应用服务)的内部实现。
我的“咦”时刻是由以下两点触发的:
  • 阅读上述链接中有关服务描述的部分,更确切地说,是这句话:

领域服务使用通用语言和领域类型来表达,即方法参数和返回值都是适当的领域类。

我发现在将应用程序流程与领域模型分离方面非常有帮助。涉及应用程序流程的所有逻辑通常最终成为应用程序服务,并分解到应用程序层中,而来自领域的不适合作为模型对象的概念最终形成一个或多个领域服务。


5
我同意,这正是我定义应用服务的方式,并且适用于我到目前为止遇到的所有情况。领域服务处理与域对象相关的一切,但超出了单个实体的范围。例如:BookReferencesService.GetNextAvailableUniqueTrackingNumber(),重点显然是业务规则*。关于应用服务,它正是你所描述的,大多数时候我会从我的控制器操作开始将这个业务工作流程放入其中,当我注意到这一点时,我会将这个逻辑重构到应用服务层中。我们可以说这个层次是针对用例的。 - tobiak777
1
这些领域服务接口被领域实体所使用。 - tobiak777
1
博客文章的链接已经失效,请问有人找到可用的链接吗? - ahong
我们如何强制使用域服务而不是域实体?这个知识需要所有开发人员都意识到吗? - Ayyappa

65

这是我对《实现领域驱动设计》(作者:Vaughn Vernon)中一些概念的理解:

领域对象(包括实体值对象)封装了子域所需的行为,使其自然、表达力强、易于理解。

领域服务封装了不适合于一个单独的领域对象的行为。例如,借书馆将Book借给Client(与相应的Inventory更改),可能就需要使用领域服务。

应用服务处理用例的流程,包括在领域之上所需的任何其他问题。它通常通过API公开这些方法,供外部客户端使用。继续上面的例子,我们的应用服务可以公开一个方法LendBookToClient(Guid bookGuid, Guid clientGuid),它:

  • 检索Client
  • 确认其权限。(注意我们如何将领域模型从安全/用户管理问题中解放出来。这种污染可能会导致许多问题。相反,我们在这里在应用程序服务中满足此技术要求。)
  • 检索Book
  • 调用领域服务(传递ClientBook)来处理借给客户的实际领域逻辑。例如,我认为确认书籍的可用性肯定是领域逻辑的一部分。

应用程序服务通常应该具有非常简单的流程。复杂的应用程序服务流往往表明领域逻辑已经泄露出去了。

希望您能够看到,这种方式下领域模型非常清晰简洁,易于理解和与领域专家进行讨论,因为它只包含自己的业务关注点。另一方面,应用程序流程因为不再承担领域相关的责任,变得更加简洁和直接,也更容易管理。


3
我认为“应用程序服务”也是解决依赖关系的关键点。它的方法是一个用例,一个单一的流程,因此它可以对具体实现做出明智的决策。数据库事务也适用于此处。 - Timo
2
个人认为,“权限”通常比人们想象的更多涉及到领域问题。例如,“只向信誉良好的客户借书”听起来像是一条业务规则。当然,你可能需要一些应用级别的逻辑将原始权限转换为“客户”实体的领域级别“信誉”状态。 - TheRubberDuck
1
@Ali.Rashidi BookClient将是领域对象。领域模型是用于在业务领域实现功能的整个模型。由于将书借给客户可能很复杂,因此可能涉及领域服务,因为该过程的责任涉及多个实体:书是否可用?客户是否有空间借更多书?客户是否有权首先借书(活动订阅,无债务等)?这些都是需要在模型中某处执行的领域规则。 - Timo
1
@timo那应用程序服务会做什么?我们将所有业务逻辑放在域服务中。 - Ali.Rashidi
2
@Ali.Rashidi 实现 LendBook 的应用程序服务可能会:执行身份验证和授权。解释和验证输入的合同模型(例如 LendBookRequest)。加载 ClientBook。确认它们存在或使请求无效。调用领域服务执行领域逻辑。将结果适配为输出合同模型(例如 LendBookResponse)。 (请记住,虽然领域模型可以自由更改,但合同模型很难更改。它需要稳定,成为您的 API 的一部分,并被外部上下文使用。) - Timo
显示剩余2条评论

50

领域服务是域的扩展,只在域的范围内看到。这不像“关闭账户”之类的某些用户操作。如果存在状态,则应该是一个领域对象。领域服务执行的操作只有与其他协作者(领域对象或其他服务)一起使用时才有意义,并且“有意义”的责任属于另一层。

应用程序服务是初始化和监督领域对象和服务之间交互的那一层。通常的流程是:从存储库获取领域对象(或多个对象),执行操作,然后将它(它们)放回去(或不放回)。它也可以做更多的事情,例如检查领域对象是否存在,根据情况抛出异常。因此,它使用户可以通过操纵领域对象和服务与应用程序互动(这可能是其名称的起源所在)。应用程序服务通常代表所有可能的用例。在考虑领域之前,最好创建应用程序服务接口,这将让您更好地了解自己真正想要做什么。拥有这样的知识能够让您专注于领域。

存储库通常可以注入到领域服务中,但这种情况相对较少。大多数情况下,是应用程序层这样做。


20
“域服务适用于没有状态的情况,否则它将成为一个域对象。”这句话让我恍然大悟。谢谢。 - Nick
@Nick,你能解释一下吗? - Ali.Rashidi
我读到的关于这个困难区分的最好的事情之一。谢谢。 - groovedigga

18
领域服务视为实现业务逻辑或与领域对象相关的业务规则的对象,这种逻辑很难适应于相同的领域对象,并且也不会导致领域服务(领域服务是一个没有“状态”或更好地说是没有具有业务含义的状态的对象)的状态发生变化,但最终仅会更改其操作的领域对象的状态。

应用服务实现应用级别的逻辑,例如用户交互、输入验证和与业务无关的逻辑,例如身份验证、安全、电子邮件等,仅限于简单地使用领域对象公开的服务。

以下是一个仅用于解释目的的示例场景:我们必须实现一个非常小的domotic实用程序应用程序,执行一个简单的操作,即“当有人打开进入房间的门时,打开灯,当关闭房间的门时关闭灯”。

简化后,我们只考虑两个不属于同一个聚合的领域实体:,它们各自有两种状态:打开/关闭开/关,并且具有特定的方法来操作它们的状态变化。这些实体需要成为不同的聚合部分,以便以下逻辑无法在聚合根中实现。 在这种情况下,我们需要一个领域服务,在有人从外面打开门进入房间时执行打开灯光的特定操作,因为门和灯对象不能以我们认为适合它们的业务性质的方式实现此逻辑。这个新的领域服务需要封装一些应该始终发生的业务流程,由某些领域事件/方法触发。 我们可以把我们的领域服务称为DomoticDomainService,并实现两个方法:OpenTheDoorAndTurnOnTheLightCloseTheDoorAndTurnOffTheLight,这两个方法分别将DoorLamp对象的状态更改为打开/开关闭/关。进入或离开房间的状态既不在领域服务对象中,也不在领域对象中,而将通过一个应用服务实现简单的用户交互。我们可以称之为 HouseService 的应用服务,该服务实现一些事件处理程序,如 onOpenRoom1DoorToEnteronCloseRoom1DoorToExit,对于每个房间(这只是为了说明的一个例子),分别涉及调用领域服务方法以执行所需的行为。(由于这只是一个示例,我们没有考虑实体Room)。
此示例远非一个设计良好的真实应用程序,它的唯一目的(正如多次提到的)是解释什么是领域服务以及它与应用服务的区别,希望它清晰且有用。
此外,上述示例领域服务容易被领域事件替换,后者用于显式地在一个或多个聚合之间实现副作用。但由于这不是本问题的主题,我在这里只是提到它们,以使读者意识到它们的存在,并稍后决定哪种方法更适合他们。

7
嗨Morteza,你能具体说明一下吗?否则你的话只是一个没有实际论据的“判断”。谢谢。 - Ciro Corvino

14

域服务:不适合单个实体或需要访问存储库的方法包含在域服务中。域服务层也可以包含自身的域逻辑,与实体和值对象一样是域模型的一部分。

应用服务:应用程序服务是位于域模型之上的薄层,协调应用程序活动。它不包含业务逻辑,也不保存任何实体的状态;但是,它可以存储业��工作流事务的状态。您可以使用应用程序服务使用请求-响应消息模式将API提供给域模型。

出处:Millett,C (2010). Professional ASP.NET Design Patterns. Wiley Publishing. 92.


10

域服务:表达不属于任何聚合根的业务逻辑的服务。

  • 你有两个聚合根:

    • Product 包含名称和价格。
    • Purchase 包含购买日期,订单中所订购的产品列表、数量和当时的产品价格,以及支付方式。
  • Checkout 不属于这两个模型的任何一个,是你业务中的一个概念。

  • Checkout 可以作为一个域服务创建,它获取所有产品并计算总价,通过调用另一个基础设施实现的域服务 PaymentService 进行支付,并将其转换为 Purchase

应用服务:一个“编排”或执行域方法的服务。这可以只是你的控制器。

这通常是你完成以下工作的地方:

public String createProduct(...some attributes) {
  if (productRepo.getByName(name) != null) {
    throw new Exception();
  }

  productId = productRepository.nextIdentity();

  product = new Product(productId, ...some attributes);

  productRepository.save(product);

  return productId.value();
  // or Product itself
  // or just void if you dont care about result
}

public void renameProduct(productId, newName) {
  product = productRepo.getById(productId);

  product.rename(newName);

  productRepo.save(product);
}

在这里,您可以进行验证,例如检查产品是否唯一。除非产品的唯一性是不变量,否则应该作为域服务的一部分,可能被称为UniqueProductChecker,因为它不能成为Product类的一部分,并且与多个聚合进行交互。

以下是DDD项目的完整示例:https://github.com/VaughnVernon/IDDD_Samples

您可以找到很多应用程序服务和几个领域服务的示例。


在应用服务中验证和保存实体是强制性的吗?如果我有实体A、B和C,它们之间都有关联(A->B->C),对A进行操作应该通过从一个域服务调用另一个域服务来导致对B和C的更改,那该怎么做呢? - MrNVK
在应用服务中,是验证并仅保存实体是强制性的吗? 如果必须,则是的。大多数情况下,您必须检查ID是否存在,否则您将处理空变量。 - captainskippah
1
如果我有实体A、B和C,它们之间都有关联(A->B->C),并且对A的操作应该通过从一个域服务调用另一个域服务来导致B和C发生变化,那么该怎么做呢?我不确定你所说的“从一个域服务调用另一个域服务”的意思是什么,但是对于实体的更改反应,您可以使用事件或者只是通过应用程序服务进行编排,例如:aggregateA.doOperation(),aggregateB.doAnother()。搜索:Orchestration vs Choreography。 - captainskippah
1
谢谢您的回复!“从另一个域服务调用一个域服务” - 我的意思是,如果我对实体A进行复杂操作,则必须使用ADomainService。但是,除了实体A之外,此操作还会影响实体B。在ADomainService中必须执行BDomainService上的实体B的操作也很复杂。现在我对这种方法表示怀疑 :) 但是,如果我将此逻辑放入ApplicationService中,它是否会破坏应该仅在域层而不是应用程序层中的业务流程的封装? - MrNVK
如果您认为应该将事件放在域服务中而不是应用服务中,那么您可以直接从域服务中发出事件。 - captainskippah

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接