8应用服务与领域服务

本系列包含以下文章:

  1. DDD入门
  2. DDD概念大白话
  3. 战略设计
  4. 代码工程结构
  5. 请求处理流程
  6. 聚合根与资源库
  7. 实体与值对象
  8. 应用服务与领域服务(本文)
  9. 领域事件
  10. 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只需要领域模型就够了(毕竟领域驱动嘛)。但是,软件运行于计算机这种基础设施之上,显然不止于领域模型这么简单,至少还应该包含以下方面:

  1. 数据的网络传输
  2. 应用协议的解析
  3. 对业务用例的协调
  4. 事务处理
  5. 业务数据的持久化
  6. 日志
  7. 认证授权等非业务逻辑类关注点

以Spring MVC为例,在编写代码时我们直接面对的是Controller。在Controller背后,Spring框架和Servlet容器已经为我们处理好了数据的网络传输以及HTTP协议解析等底层设施,此时的Controller似乎已经是一个比较高级的编程对象了。咋一看,我们得到了这么一个场景:一边是Controller,一边是领域模型,何不直接使用Controller调用领域模型完成上述的第3点到7点呢?并非完全不可以,但是直接在Controller中调用领域模型的缺点也非常明显:

  1. Controller属于Spring框架,依然是一个非常技术性的存在,而上述的第3到7点大多与具体的框架无关,因此更应该作为一个单独的关注点来处理,以达到与具体框架解耦的目的
  2. 对用例的协调是可以复用的,比如以后需要通过桌面GUI(比如JavaFx)来实现的话,其协调逻辑和此时的Controller是相同的,总不至于再拷一份源代码过去吧

由此可以看出,在技术性的Controller和业务性的领域模型之间,还应该有一个值得被当做单独关注点的存在。而另一方面,从领域模型本身来说,它只是业务知识在软件中的表达,并不负责直接处理外界请求,而是需要有一个门面性的存在来协助它。综合起来,在DDD中我们将这个“存在”称之为应用服务

先来看个关于应用服务的例子,在码如云中,租户的管理员可以对成员(Member)进行启用或禁用操作,以启用成员为例,此时的Controller代码如下:

//MemberController@PutMapping(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)代码如下:

//MemberCommandService@Transactional
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

从上面两段代码中,我们可以总结出以下几点:

  1. Controller的作用非常简单,就一行代码,即调用应用服务,这么做的目的是希望程序尽量早地从技术框架解耦;
  2. 应用服务MemberCommandService遵循DDD中的业务请求处理三部曲原则,即先加载Member,再调用Member上的业务方法activate(),最后调用资源库memberRepository.save(member)保存Member,整个过程中,应用服务主要起组织协调作用,并不负责实际的业务逻辑;
  3. MemberCommandService方法上标注了@Transactional,也即应用服务负责处理事务边界;
  4. 在完成协调工作之前,MemberCommandService通过调用user.checkIsTenantAdmin()来检查操作用户是否为租户管理员,也即应用服务也会负责协调对权限的处理;
  5. 打日志,一个应用服务对应一个独立的业务用例,用例处理完后需要日志记录;
  6. 从整个上看,应用服务与其所在的Spring框架是解耦的。

应用服务是领域模型的门面 #

在DDD中,领域模型并不直接接收外界的请求,而是通过应用服务向外提供业务功能。此时的应用服务就像酒店的前台一样,对外面对客户,对内则将客户的请求代理派发给内部的领域模型。应用服务将核心的领域模型和外界隔离开来,可以说应用服务是在“呵护”着领域模型。

既然应用服务只是起协调代理的作用,也意味着应用服务不应该包含过多的逻辑,而应该是很薄的一层。另外,应用服务是以业务用例为粒度接收外部请求的,也即应用服务类中的每一个共有方法即对应一个业务用例,进而意味着应用服务也负责处理事务边界,使得对一个业务用例的处理要么全部成功,要么全部失败。对应到实际编码过程中,@Tranactional注解并不是想怎么打就怎么打的,而是主要应该打到应用服务上。

应用服务应该与框架无关 #

应用服务要做到与技术框架无关,因为应用服务向外代表着业务用例,而业务用例不因框架的变化而变化,当我们把应用服务放到诸如Spring MVC这种Web框架中,它能正常工作,当我们将它迁移到桌面GUI程序中,它也应该可以正常工作。从这个角度,可以将应用服务比作电子元器件,比如CPU,一个CPU在华硕的主板上可以正常使用,将其转插到微星主板中也是可以的。

这里有个需要讨论的点是@Transactional,这个注解是属于Spring框架的,将其打在了到应用服务上,这不违背了“应用服务与框架无关”的原则吗?严格上来讲,的确如此,但是这个妥协我们认为是可以做的,原因如下:(1)@Transational注解是打在应用服务方法之上的,并不直接侵入应用服务的方法实现内部,因此这种侵入性并不会导致应用服务中逻辑的混乱,替换的成本也不高;(2)@Transational本是通过Spring的AOP实现,如果的确不想使用,可以在Controller中调用应用服务的地方使用Spring的TranactionalTemplate类完成,或者另行封装一个TransactionWrapper之类的东西供Controller调用,这样一来咱们的应用服务就的确和Spring框架没任何关系了,但是从务实的角度考虑,这种做法有些得不偿失。就上例而言,如果的确有一天你需要像电脑更换CPU那样将系统从Spring迁移到Guice框架,通过简单的适配便达到目的了。

领域服务 #

领域服务虽然和应用服务都有“服务”二字,但是它们并没有多少联系,分别在不同的DDD岗位上各司其职,并且源自于两种完全不同的逻辑推演。

在本系列的前几篇文章中,我们知道了领域模型中最重要的概念是聚合根对象,理想情况下我们希望所有的业务逻辑都发生在聚合根之中,在实际编码中我们也是朝着这个目标行进的。但是,理想和现实始终是存在差距的,在有些情况下将业务逻辑放到聚合根中并不合适,于是我们做个妥协,将这部分业务逻辑放到另外的地方——领域服务

还是来看个实际的例子,在码如云中,成员(Member)可以修改自己的手机号,在修改手机号时,需要判断新手机号是否已经被他人占用。这里的“检查手机号是否被占用”是一种跨聚合根的业务逻辑,单单凭当事的Member自身是否无法完成的,因为该Member无法感知到其他Member的状态。另外,“手机号不能重复”这种逻辑恰恰是一种业务逻辑,应该属于领域模型的一部分。

让我们将思考问题的方式反过来,通过自底向上的方式再看看,要实现跨聚合根的检查,无论如何是需要访问数据库的,这落入了资源库(Repository)的职责范畴,为此我们在Member对应的资源库MemberRepository中实existsByMobile(mobileNumber)方法用于检查一个手机号mobileNumber是否已经被占用。接下来的问题在于,对该方法的调用应该由谁完成?此时至少有3种方式:

  1. 在应用服务中调用:这种调用不再是简单的协调式调用,而是感知到了业务逻辑的调用,这违背了应用服务的基本原则,因此不应该使用这种方式;
  2. MemberRepository作为参数传入Member,这的确是一种方式,但是这种方式使得聚合根Member接受了与业务数据无关的方法参数,是一种API污染,因此我们也不推荐;
  3. 作为一个单独的关注点,另立门户:将这部分逻辑放到一个单独的类中,这个类依然属于领域模型,此时的“另立门户”便是一个领域服务了。

在使用了领域服务后,整个请求的流程稍微有些变化。首先在应用服务MemberCommandService中, 我们不再遵循经典的请求处理三部曲,而是通过调用领域服务MemberDomainService来更新Member的状态:

//MemberCommandService@Transactional
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中的应用服务和领域服务分别解决了两个完全不同的问题,他们主要的区别在于:

  1. 应用服务处于领域模型的外侧,是领域模型的客户(调用方),其作用是协调各方完成业务用例;而领域服务则是属于领域模型的一部分;
  2. 应用服务不处理业务逻辑,领域服务里全是业务逻辑;
  3. 每一个业务用例都需要经过应用服务,而领域服务则是一种迫不得已而为之的妥协。

到这里,再去看看自己代码中的那些Service类,是不是可以尝试着对它们归个类了?

总结 #

在本文中,我们分别对应用服务和领域服务做了展开讲解,包含它们各自产生的逻辑以及它们之间的区别。在实际编码中,通常的编码方式是:从Controller中调用应用服务,应用服务协调各方完成对业务用例的处理,业务逻辑优先放入聚合根中,如果不合适才考虑使用领域服务。在下一篇领域事件中,我们将讲到领域事件在DDD中的应用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/86946.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

微信小程序demo 调用支付jsapi缺少参数 total_fee,支付签名验证失败 究极解决方案

坑一:调用支付jsapi缺少参数 total_fee 修正后的uniapp代码如下: uni.requestPayment({provider: wxpay,timeStamp: String(data.timestamp),nonceStr: data.nonceStr,package: prepay_id data.prepayId,signType: HMAC-SHA256,paySign: data.sign,su…

ElasticSearch - 索引库和文档相关命令操作

目录 一、ElasticSearch 索引库操作 1.1、mapping 属性 1.2、索引库相关操作 1.2.1、创建索引库 1.2.2、增加和删除索引库 1.2.3、修改索引库 1.3、文档操作 1.3.1、添加文档 1.3.2、文档的查询和删除 1.3.3、修改文档 1.全量修改:会先删除旧文档&#xf…

rust泛型

泛型&#xff0c;英文是generic。 泛型是一种参数化多态。就是把类型作为参数&#xff0c;使用时才指定具体类型。 这样一套代码可以应用于多种类型。比如Vec<T>&#xff0c;可以是整型向量Vec<i32>&#xff0c;也可以是浮点型向量Vec<f64>。 Rust中的泛型属…

Vue 07 Vue中的数据代理

通过数据代理&#xff0c;我可以方便的使用vm.属性&#xff0c;修改data中的属性 什么是数据代理 数据代理&#xff1a;通过一个对象代理对另一个对象中属性的操作&#xff08;读/写&#xff09; 我们修改obj2的x属性&#xff0c;其实修改的是obj的x属性 <!DOCTYPE html&…

基于springboot消防员招录系统

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战 主要内容&#xff1a;毕业设计(Javaweb项目|小程序等)、简历模板、学习资料、面试题库、技术咨询 文末联系获取 项目介绍…

设计模式:观察者模式(C++实现)

观察者模式&#xff08;Observer Pattern&#xff09;是一种设计模式&#xff0c;用于定义对象之间的一对多依赖关系&#xff0c;当一个对象&#xff08;称为主题或可观察者&#xff09;的状态发生变化时&#xff0c;它的所有依赖对象&#xff08;称为观察者&#xff09;都会收…

vue-cli创建项目、vue项目目录结(运行vue项目)、ES6导入导出语法、vue项目编写规范

vue-cli创建项目、vue项目目录结构、 ES6导入导出语法、vue项目编写规范 1 vue-cli创建项目 1.1 vue-cli 命令行创建项目 1.2 使用vue-cli-ui创建 2 vue项目目录结构 2.1 运行vue项目 2.2 vue项目的目录结构 3 es6导入导出语法 4 vue项目编写规范 4.1 修改项目 4.2 以后…

DDL、DML

文章目录 一、字段1.1 添加字段1.2 给列添加默认值1.3 修改列的属性1.4 更新1.5 删除1.6 同时修改多列 二、索引2.1 普通索引2.2 唯一键 三、创建表四、字段json不是json类型&#xff0c;但是也可以按照json存储内容补充 default 0 和 default 0没区别 一、字段 1.1 添加字段 …

【深度学习推荐系统 工程篇】三、浅析FastTransFormer看 GPU推理优化 思路

前言 在搜索/推荐场景中&#xff08;一般是CTR/CVR预估&#xff09;Serving的模型一般是稀疏参数占比比较大&#xff0c;工程落地方面会遇到两方面的困难&#xff1a; 稀疏参数的存储/IO网络结构的优化 对于稀疏参数的存储/IO&#xff0c;在上一篇【深度学习推荐系统 工程篇…

电子信息工程专业课复习知识点总结:(五)通信原理

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 第一章通信系统概述——通信系统的构成、各部分性质、性能指标1.通信系统的组成&#xff1f;2.通信系统的分类&#xff1f;3.调制、解调是什么&#xff1f;有什么用…

详解MySQL存储引擎

前言: 📕作者简介:热爱编程的小七,致力于C、Java、Python等多编程语言,热爱编程和长板的运动少年! 📘相关专栏Java基础语法,JavaEE初阶,数据库,数据结构和算法系列等,大家有兴趣的可以看一看。 😇😇😇有兴趣的话关注博主一起学习,一起进步吧! 一、MySQL存…

【Excel函数】Vlookup的函数的使用

VLOOKUP(垂直查找)是Microsoft Excel中一种非常有用的函数,用于查找并返回一个特定值所在列的相关信息。它通常用于在大型数据表中查找数据。 以下是VLOOKUP函数的基本语法: (lookup_value, table_array, col_index_num, [range_lookup]) lookup_value:这是您要查找的值或…

rust枚举

一、定义枚举 1.使用enum关键字定义枚举。 语法格式如下 enum enum_name {variant1,variant2,variant3 }例如 enum Fruits {Banana, // 香蕉Pear, // 梨Mandarin, // 橘子Eggplant // 茄子 }2.可以为枚举成员添加属性 enum Book {Papery(u32),Electronic(String), } let bo…

【CNN-FPGA开源项目解析】卷积层01--floatMult16模块

文章目录 (基础)半精度浮点数的表示和乘运算16位半精度浮点数浮点数的乘运算 floatMult16完整代码floatMult16代码逐步解析符号位sign判断指数exponent计算尾数fraction计算尾数fraction的标准化和舍位整合为最后的16位浮点数结果[sign,exponent,fraction] 其他变量宽度表alway…

系统运维工程师

文章目录 引言I 任职要求II Sentinel2.1 安装2.2 开放防火墙端口:III zipkinsee also引言 云效devops部署解决方案: Java应用构建并部署ECS、K8s、SAE、EDAS。 部署微服务的服务器选择。域名劫持:网站被拦截到特定网址,如何解决。你认为初级运维工程师和高级运维工程师的区…

管理公司和管理工作室

有一段时间在写小游戏&#xff0c;最近unity3d 闹出一则荒唐事情&#xff0c;其实说白一点。老贼就要收费这个事情不会妥协。好不容易做了一次坏人。不过说到这一点我们不得不说管理公司这个事情。unity3d在一些新闻公布有7000多员工&#xff0c;这样看起来似乎是不是有点多了&…

HJ90 合法ip 判断合法字符串

题目链接&#xff1a;https://www.nowcoder.com/practice/995b8a548827494699dc38c3e2a54ee9 IPV4地址可以用一个32位无符号整数来表示&#xff0c;一般用点分方式来显示&#xff0c;点将IP地址分成4个部分&#xff0c;每个部分为8位&#xff0c;表示成一个无符号整数&#xff…

Aspose转pdf乱码问题

一、问题描述 ​ 在centos服务器使用aspose.word转换word文件为pdf的时候显示中文乱码(如图)&#xff0c;但是在win服务器上使用可以正常转换 二、问题原因 由于linux服务器缺少对应的字库导致文件转换出现乱码的 三、解决方式 1.将window中字体(c:\windows\fonts)放到linux…

Spring Authorization Server优化篇:Redis值序列化器添加Jackson Mixin,解决Redis反序列化失败问题

前言 在授权码模式的前后端分离的那篇文章中使用了Redis来保存用户的认证信息&#xff0c;在Redis的配置文件中配置的值序列化器是默认的Jdk序列化器&#xff0c;虽然这样也可以使用&#xff0c;但是在Redis客户端中查看时是乱码的(看起来是)&#xff0c;如果切换为Jackson提供…

leetcode刷题 二维数组 八方向

题目描述 输入&#xff1a;board [[0,1,0],[0,0,1],[1,1,1],[0,0,0]] 输出&#xff1a;[[0,0,0],[1,0,1],[0,1,1],[0,1,0]] 题目分析:就是以二维数组某个元素为中心&#xff0c;寻找周围八个方向的元素&#xff0c;按照题目要求修改二维数组元素返回&#xff1b; 拷贝一份二…