做网站十大公司哪家好,十大猎头公司,中山做网站推广公司,wordpress首页静态化生成本系列包含以下文章#xff1a;
DDD入门DDD概念大白话战略设计代码工程结构请求处理流程聚合根与资源库实体与值对象应用服务与领域服务#xff08;本文#xff09;领域事件CQRS
案例项目介绍 #
既然DDD是“领域”驱动#xff0c;那么我们便不能抛开业务而只讲技术…本系列包含以下文章
DDD入门DDD概念大白话战略设计代码工程结构请求处理流程聚合根与资源库实体与值对象应用服务与领域服务本文领域事件CQRS
案例项目介绍 #
既然DDD是“领域”驱动那么我们便不能抛开业务而只讲技术为此让我们先从业务上了解一下贯穿本文章系列的案例项目 —— 码如云不是马云也不是码云。如你已经在本系列的其他文章中了解过该案例可跳过。
码如云是一个基于二维码的一物一码管理平台可以为每一件“物品”生成一个二维码并以该二维码为入口展开对“物品”的相关操作典型的应用场景包括固定资产管理、设备巡检以及物品标签等。
在使用码如云时首先需要创建一个应用(App)一个应用包含了多个页面(Page)也可称为表单一个页面又可以包含多个控件(Control)比如单选框控件。应用创建好后可在应用下创建多个实例(QR)用于表示被管理的对象比如机器设备。每个实例均对应一个二维码手机扫码便可对实例进行相应操作比如查看实例相关信息或者填写页面表单等对表单的一次填写称为提交(Submission)更多概念请参考码如云术语。
在技术上码如云是一个无代码平台包含了表单引擎、审批流程和数据报表等多个功能模块。码如云全程采用DDD完成开发其后端技术栈主要有Java、Spring Boot和MongoDB等。
码如云的源代码是开源的可以通过以下方式访问 码如云源代码GitHub - mryqr-com/mry-backend: 本代码库为码如云后端代码。码如云是一个基于二维码的一物一码管理平台可以为每一件“物品”生成一个二维码手机扫码即可查看物品信息并发起相关业务操作操作内容可由你自己定义典型的应用场景包括固定资产管理、设备巡检以及物品标签等。在技术上码如云是一个无代码平台全程采用DDD、整洁架构和事件驱动架构思想完成开发。 应用服务和领域服务 #
对于服务类代码中的各种Service类想必程序员们都不会陌生比如在做Spring项目时在Controller层的后面通常会有一个XxxService存在。如果对代码职责划分得好一点呢那么该Service还会协调其他各方完成对请求的处理而如果代码设计得不那么好呢估计就是一个Service负责从头到尾的所有了。在DDD中也有类似的服务类即应用服务Application Service和领域服务Domain Service不过DDD对于这些服务类的职责做出了明确的界定在本文中我们将对此做出详细地讲解。
应用服务 #
在本系列的前几篇文章中我们讲到在DDD中领域模型主要包含聚合根实体值对象工厂等是软件系统的核心所有的业务逻辑都发生在其中。在理想情况下DDD只需要领域模型就够了毕竟领域驱动嘛。但是软件运行于计算机这种基础设施之上显然不止于领域模型这么简单至少还应该包含以下方面
数据的网络传输应用协议的解析对业务用例的协调事务处理业务数据的持久化日志认证授权等非业务逻辑类关注点
以Spring MVC为例在编写代码时我们直接面对的是Controller。在Controller背后Spring框架和Servlet容器已经为我们处理好了数据的网络传输以及HTTP协议解析等底层设施此时的Controller似乎已经是一个比较高级的编程对象了。咋一看我们得到了这么一个场景一边是Controller一边是领域模型何不直接使用Controller调用领域模型完成上述的第3点到7点呢并非完全不可以但是直接在Controller中调用领域模型的缺点也非常明显
Controller属于Spring框架依然是一个非常技术性的存在而上述的第3到7点大多与具体的框架无关因此更应该作为一个单独的关注点来处理以达到与具体框架解耦的目的对用例的协调是可以复用的比如以后需要通过桌面GUI比如JavaFx来实现的话其协调逻辑和此时的Controller是相同的总不至于再拷一份源代码过去吧
由此可以看出在技术性的Controller和业务性的领域模型之间还应该有一个值得被当做单独关注点的存在。而另一方面从领域模型本身来说它只是业务知识在软件中的表达并不负责直接处理外界请求而是需要有一个门面性的存在来协助它。综合起来在DDD中我们将这个“存在”称之为应用服务。
先来看个关于应用服务的例子在码如云中租户的管理员可以对成员(Member)进行启用或禁用操作以启用成员为例此时的Controller代码如下
//MemberControllerPutMapping(value /{memberId}/activation)
public void activateMember(PathVariable(memberId) NotBlank MemberId String memberId,AuthenticationPrincipal User user) {memberCommandService.activateMember(memberId, user);
}源码出处com/mryqr/core/member/MemberController.java 对应的应用服务(MemberCommandService)代码如下
//MemberCommandServiceTransactional
public void activateMember(String memberId, User user) {user.checkIsTenantAdmin();Member member memberRepository.byIdAndCheckTenantShip(memberId, user);member.activate(user);memberRepository.save(member);log.info(Activated member[{}]., memberId);
}源码出处com/mryqr/core/member/command/MemberCommandService.java 从上面两段代码中我们可以总结出以下几点
Controller的作用非常简单就一行代码即调用应用服务这么做的目的是希望程序尽量早地从技术框架解耦应用服务MemberCommandService遵循DDD中的业务请求处理三部曲原则即先加载Member再调用Member上的业务方法activate()最后调用资源库memberRepository.save(member)保存Member整个过程中应用服务主要起组织协调作用并不负责实际的业务逻辑MemberCommandService方法上标注了Transactional也即应用服务负责处理事务边界在完成协调工作之前MemberCommandService通过调用user.checkIsTenantAdmin()来检查操作用户是否为租户管理员也即应用服务也会负责协调对权限的处理打日志一个应用服务对应一个独立的业务用例用例处理完后需要日志记录从整个上看应用服务与其所在的Spring框架是解耦的。
应用服务是领域模型的门面 #
在DDD中领域模型并不直接接收外界的请求而是通过应用服务向外提供业务功能。此时的应用服务就像酒店的前台一样对外面对客户对内则将客户的请求代理派发给内部的领域模型。应用服务将核心的领域模型和外界隔离开来可以说应用服务是在“呵护”着领域模型。 既然应用服务只是起协调代理的作用也意味着应用服务不应该包含过多的逻辑而应该是很薄的一层。另外应用服务是以业务用例为粒度接收外部请求的也即应用服务类中的每一个共有方法即对应一个业务用例进而意味着应用服务也负责处理事务边界使得对一个业务用例的处理要么全部成功要么全部失败。对应到实际编码过程中Tranactional注解并不是想怎么打就怎么打的而是主要应该打到应用服务上。
应用服务应该与框架无关 #
应用服务要做到与技术框架无关因为应用服务向外代表着业务用例而业务用例不因框架的变化而变化当我们把应用服务放到诸如Spring MVC这种Web框架中它能正常工作当我们将它迁移到桌面GUI程序中它也应该可以正常工作。从这个角度可以将应用服务比作电子元器件比如CPU一个CPU在华硕的主板上可以正常使用将其转插到微星主板中也是可以的。
这里有个需要讨论的点是Transactional这个注解是属于Spring框架的将其打在了到应用服务上这不违背了“应用服务与框架无关”的原则吗严格上来讲的确如此但是这个妥协我们认为是可以做的原因如下1Transational注解是打在应用服务方法之上的并不直接侵入应用服务的方法实现内部因此这种侵入性并不会导致应用服务中逻辑的混乱替换的成本也不高2Transational本是通过Spring的AOP实现如果的确不想使用可以在Controller中调用应用服务的地方使用Spring的TranactionalTemplate类完成或者另行封装一个TransactionWrapper之类的东西供Controller调用这样一来咱们的应用服务就的确和Spring框架没任何关系了但是从务实的角度考虑这种做法有些得不偿失。就上例而言如果的确有一天你需要像电脑更换CPU那样将系统从Spring迁移到Guice框架通过简单的适配便达到目的了。
领域服务 #
领域服务虽然和应用服务都有“服务”二字但是它们并没有多少联系分别在不同的DDD岗位上各司其职并且源自于两种完全不同的逻辑推演。
在本系列的前几篇文章中我们知道了领域模型中最重要的概念是聚合根对象理想情况下我们希望所有的业务逻辑都发生在聚合根之中在实际编码中我们也是朝着这个目标行进的。但是理想和现实始终是存在差距的在有些情况下将业务逻辑放到聚合根中并不合适于是我们做个妥协将这部分业务逻辑放到另外的地方——领域服务。
还是来看个实际的例子在码如云中成员(Member)可以修改自己的手机号在修改手机号时需要判断新手机号是否已经被他人占用。这里的“检查手机号是否被占用”是一种跨聚合根的业务逻辑单单凭当事的Member自身是否无法完成的因为该Member无法感知到其他Member的状态。另外“手机号不能重复”这种逻辑恰恰是一种业务逻辑应该属于领域模型的一部分。
让我们将思考问题的方式反过来通过自底向上的方式再看看要实现跨聚合根的检查无论如何是需要访问数据库的这落入了资源库(Repository)的职责范畴为此我们在Member对应的资源库MemberRepository中实existsByMobile(mobileNumber)方法用于检查一个手机号mobileNumber是否已经被占用。接下来的问题在于对该方法的调用应该由谁完成此时至少有3种方式
在应用服务中调用这种调用不再是简单的协调式调用而是感知到了业务逻辑的调用这违背了应用服务的基本原则因此不应该使用这种方式将MemberRepository作为参数传入Member中这的确是一种方式但是这种方式使得聚合根Member接受了与业务数据无关的方法参数是一种API污染因此我们也不推荐作为一个单独的关注点另立门户将这部分逻辑放到一个单独的类中这个类依然属于领域模型此时的“另立门户”便是一个领域服务了。
在使用了领域服务后整个请求的流程稍微有些变化。首先在应用服务MemberCommandService中 我们不再遵循经典的请求处理三部曲而是通过调用领域服务MemberDomainService来更新Member的状态
//MemberCommandServiceTransactional
public void changeMobile(ChangeMyMobileCommand command, User user) {Member member memberRepository.byId(user.getMemberId());memberDomainService.changeMobile(member, command.getMobile(), command.getPassword());memberRepository.save(member);log.info(Mobile changed by member[{}]., member.getId());
}源码出处com/mryqr/core/member/command/MemberCommandService.java 这里应用服务MemberCommandService在加载到对应的Member对象后将该Member传递给了领域服务MemberDomainService.changeMobile()并期待着这个领域服务会干正确的事情即更新Member的手机号。最后应用服务再调用memberRepository.save()将更新后的Member对象保存到数据库中。在这个过程中应用服务的“将请求代理给领域模型”这种结构并没有发生变化并且也无需关心领域服务的内部细节。事实上此时对请求的处理依然是三部曲只是其中的第2步从“调用聚合根上的业务方法”变成了“调用领域服务上的业务方法”。
领域服务MemberDomainService的实现如下
//MemberDomainServicepublic void changeMobile(Member member, String newMobile) {if (Objects.equals(member.getMobile(), newMobile)) {return;}if (memberRepository.existsByMobile(newMobile)) {throw new MryException(MEMBER_WITH_MOBILE_ALREADY_EXISTS,修改手机号失败手机号对应成员已存在。,mapOf(mobile, newMobile, memberId, member.getId()));}member.changeMobile(newMobile, member.toUser());
}源码出处com/mryqr/core/member/domain/MemberDomainService.java MemberDomainService先调用memberRepository.existsByMobile()判断手机号是否被占用如被占用则抛出异常反之才调用Member.changeMobile()完成实际的状态更新。
在码如云我们发现多数情况下领域服务的存在都是为了解决类似于本例中的“检查某个值是否重复”这样的场景比如检查成员邮箱是否被占用检查分组名称是否重复等。事实上这类问题被业界广泛讨论过有兴趣的读者可以参考这里和这里。当然领域服务远不止处理此类场景比如有时生成ID是通过某些复杂的算法或者调用第三方完成此时便可以将其封装在领域服务中。此外DDD中的工厂可以被认为是一种特殊类型的领域服务。
可以看到DDD中的应用服务和领域服务分别解决了两个完全不同的问题他们主要的区别在于
应用服务处于领域模型的外侧是领域模型的客户调用方其作用是协调各方完成业务用例而领域服务则是属于领域模型的一部分应用服务不处理业务逻辑领域服务里全是业务逻辑每一个业务用例都需要经过应用服务而领域服务则是一种迫不得已而为之的妥协。
到这里再去看看自己代码中的那些Service类是不是可以尝试着对它们归个类了
总结 #
在本文中我们分别对应用服务和领域服务做了展开讲解包含它们各自产生的逻辑以及它们之间的区别。在实际编码中通常的编码方式是从Controller中调用应用服务应用服务协调各方完成对业务用例的处理业务逻辑优先放入聚合根中如果不合适才考虑使用领域服务。在下一篇领域事件中我们将讲到领域事件在DDD中的应用。