DDD全网最通俗易懂讲解(二)

领域事件相关案例

我来给你介绍一个保险承保业务过程中有关领域事件的案例。

一个保单的生成,经历了很多子域、业务状态变更和跨微服务业务数据的传递。这个过程会产生很多的领域事件,这些领域事件促成了保险业务数据、对象在不同的微服务和子域之间的流转和角色转换。在下面这张图中,我列出了几个关键流程,用来说明如何用领域事件驱动设计来驱动承保业务流程。

6825e8a12d42470c88165dc10fe1bf76.png

事件起点:客户购买保险-业务人员完成保单录入-生成投保单-启动缴费动作

  1. 投保微服务生成缴费通知单,发布第一个事件:缴费通知单已完成,将缴费通知单数据发布到消息中间件。收款微服务订阅缴费通知单事件,完成缴费动作,缴费通知单已生成,领域事件结束
  2. 收款微服务缴费完成后,发布第二个领域事件:缴费已完成,将缴费数据发布奥消息中间件。原来的订阅方收看微服务这是则变成了发布方。原来的时间

img

总之,通过领域事件驱动的异步化机制,可以推动业务流程和数据在各个不同微服务之间的流转,实现微服务的解耦,减轻微服务之间服务调用的压力,提升用户体验。

一个完整的领域事件 = 事件发布 + 事件存储 + 事件分发 + 事件处理。

事件发布:构建一个事件,需要唯一标识,然后发布;

事件存储:发布事件前需要存储,因为接收后的事件也会存储,可用于重试或对账等;就是每次执行一次具体的操作时,把行为记录下来,执行持久化。

事件分发:服务内的应用服务或者领域服务直接发布给订阅者,服务外需要借助消息中间件,比如Kafka,RabbitMQ等,支持同步或者异步。

事件处理:先将事件存储,然后再处理。

当然了,实际开发中事件存储和事件处理不是必须的。

因此实现方案:发布订阅模式,分为跨上下文(kafka,RocketMq)和上下文内(spring事件,Guava Event Bus)的领域事件。

用户注册后,发送短信和邮件,使用spring事件实现领域事件代码如下:

/*** 用户注册事件* @Author WDYin**/
public class UserRegisterEvent extends ApplicationEvent {public UserRegisterEvent(Object source) {super(source);}
}/*** 用户监听事件* @Author WDYin**/
@Component
public class UserListener {@EventListener(UserRegisterEvent.class)public void userRegister(UserRegisterEvent event) {User user = (User) event.getSource();System.out.println("用户注册。。。发送短信。。。" + user);System.out.println("用户注册。。。发送邮件。。。" + user);}@EventListener(UserCancelEvent.class)public void userCancelEvent(UserCancelEvent event) {User user = (User) event.getSource();System.out.println("用户注销。。。" + user);}}/*** 发布用户注册事件* @Author : WDYin*/
@RunWith(value = SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = DemoApplication.class)
public class MyClient {@Autowiredprivate ApplicationContext applicationContext;@Testpublic void test() {User user = new User();//发布事件applicationContext.publishEvent(new UserRegisterEvent(user));}
}

事件风暴

img

DDD分层架构

DDD 的分层架构在不断发展。最早是传统的四层架构;再后来领域层和应用层之间增加了上下文环境(Context)层,五层架构(DCI)就此形成了。

img

用户接口层

用户接口层是前端应用和微服务之间服务访问和数据交换的桥梁。它处理前端发送的Restful 请求和解析用户输入的配置文件等,将数据传递给应用层。或获取应用服务的数据后,进行数据组装,向前端提供数据服务。主要服务形态是 Facade 服务。Facade 服务分为接口和实现两个部分。完成服务定向,DO 与 DTO 数据的转换和组装,实现前端与应用层数据的转换和交换。

①一般包括用户接口、Web 服务、rpc请求,mq消息等外部输入均被视为外部输入的请求。对外暴露API,具体形式不限于RPC、Rest API、消息等。

②一般都很薄,提供必要的参数校验和异常捕获流程。

③一般会提供VO或者DTO到Entity或者ValueObject的转换,用于前后端调用的适配,当然dto可以直接使用command和query,视情况而定。

④用户接口层很重要,在于前后端调用的适配。若你的微服务要面向很多应用或渠道提供服务,而每个渠道的入参出参都不一样,你不太可能开发出太多应用服务,这样Facade接口就起很好的作用了,包括DO和DTO对象的组装和转换等。

应用层

应用层是很薄的一层,理论上不应该有业务规则或逻辑,主要面向用例和流程相关的操作。但应用层又位于领域层之上,因为领域层包含多个聚合,所以它可以协调多个聚合的服务和领域对象完成服务编排和组合,协作完成业务操作。除了同步方法调用外,还可以发布或者订阅领域事件,权限校验、事务控制,一个事务对应一个聚合根。

应用层负责不同聚合之间的服务和数据协调,负责微服务之间的事件发布和订阅。通过应用服务对外暴露微服务的内部功能,这样就可以隐藏领域层核心业务逻辑的复杂性以及内部实现机制。应用层的主要服务形态有:应用服务、事件发布和订阅服务。应用服务内用于组合和编排的服务,主要来源于领域服务,也可以是外部微服务的应用服务。

领域层

领域层包含聚合根、实体、值对象、领域服务等领域模型中的领域对象。

这里我要特别解释一下其中几个领域对象的关系,以便你在设计领域层的时候能更加清楚。首先,领域模型的业务逻辑主要是由实体和领域服务来实现的,其中实体会采用充血模型来实现所有与之相关的业务功能。其次,你要知道,实体和领域对象在实现业务逻辑上不是同级的,当领域中的某些功能,单一实体(或者值对象)不能实现时,领域服务就会出马,它可以组合聚合内的多个实体(或者值对象),实现复杂的业务逻辑。

领域层主要的服务形态有实体方法和领域服务。实体采用充血模型,在实体类内部实现实体相关的所有业务逻辑,实现的形式是实体类中的方法。实体是微服务的原子业务逻辑单元。在设计时我们主要考虑实体自身的属性和业务行为,实现领域模型的核心基础能力。不必过多考虑外部操作和业务流程,这样才能保证领域模型的稳定性。

①包含了业务核心的领域模型:实体(聚合根+值对象),使用充血模型实现所有与之相关的业务功能,主要表达业务概念,业务状态信息以及业务规则。

②真正的业务逻辑都在领域层编写,聚合根负责封装实现业务逻辑,对应用层暴露领域级别的服务接口。

③聚合根不能直接操作其它聚合根,聚合根与聚合根之间只能通过聚合根ID引用;同限界上下文内的聚合之间的领域服务可直接调用;两个限界上下文的交互必须通过应用服务层抽离接口->适配层适配。

④跨实体的状态变化,使用领域服务,领域服务不能直接修改实体的状态,只能调用实体的业务方法

DDD 提倡富领域模型,尽量将业务逻辑归属到实体对象上,实在无法归属的部分则设计成领域服务。领域服务会对多个实体或实体方法进行组装和编排,实现跨多个实体的复杂核心业务逻辑。对于严格分层架构,如果单个实体的方法需要对应用层暴露,则需要通过领域服务封装后才能暴露给应用服务

基础层

也叫基础设施层,基础层是贯穿所有层的,它的作用就是为其它各层提供通用的技术和基础服务,包括第三方工具、驱动、消息中间件、网关、文件、缓存以及数据库等。比较常见的功能还是提供数据库持久化。

基础层的服务形态主要是仓储服务。仓储服务包括接口和实现两部分。仓储接口服务供应用层或者领域层服务调用,仓储实现服务,完成领域对象的持久化或数据初始化。

比如说,在传统架构设计中,由于上层应用对数据库的强耦合,很多公司在架构演进中最担忧的可能就是换数据库了,因为一旦更换数据库,就可能需要重写大部分的代码,这对应用来说是致命的。那采用依赖倒置的设计以后(说白了就是多套一层接口),应用层就可以通过解耦来保持独立的核心业务

①为业务逻辑提供支撑能力,提供通用的技术能力,仓库写增删改查类似DAO。

② 防腐层实现(封装变化)用于业务检查和隔离第三方服务,内部try catch

架构原则

架构原则

在《实现领域驱动设计》一书中,DDD 分层架构有一个重要的原则:每层只能与位于其下方的层发生耦合。

而架构根据耦合的紧密程度又可以分为两种:严格分层架构和松散分层架构。优化后的DDD 分层架构模型就属于严格分层架构,任何层只能对位于其直接下方的层产生依赖。而传统的 DDD 分层架构则属于松散分层架构,它允许某层与其任意下方的层发生依赖。那我们怎么选呢?综合我的经验,为了服务的可管理,我建议你采用严格分层架构。

在严格分层架构中,领域服务只能被应用服务调用,而应用服务只能被用户接口层调用,服务是逐层对外封装或组合的,依赖关系清晰。而在松散分层架构中,领域服务可以同时被应用层或用户接口层调用,服务的依赖关系比较复杂且难管理,甚至容易使核心业务逻辑外泄。试想下,如果领域层中的某个服务发生了重大变更,那该如何通知所有调用方同步调整和升级呢?但在严格分层架构中,你只需要逐层通知上层服务就可以了。

3ec889eddf5d4ab181d12a8df132c784.png

a3a7216f695d41b2927c5b4a21736d3a.png

防腐层(ACL)

当某个功能模块需要依赖第三方系统提供的数据或者功能时,我们常用的策略就是直接使用外部系统的API、数据结构。这样存在的问题就是,因使用外部系统,而被外部系统的质量问题影响,从而“腐化”本身设计的问题。

img

因此我们的解决方案就是在两个系统之间加入一个中间层,隔离第三方系统的依赖,对第三方系统进行通讯转换和语义隔离,这个中间层,我们叫它防腐层

img

说白了就是,两个系统之间加了中间层,中间层类似适配器模式,解决接口差异的对接,接口转换是单向的(即从调用方向被调用方进行接口转换);防腐层强调两个子系统语义解耦,接口转换是双向的。

服务的调用

img

微服务内跨层服务调用

微服务架构下往往采用前后端分离的设计模式,前端应用独立部署。前端应用调用发布在API 网关上的 Facade 服务,Facade 定向到应用服务。应用服务作为服务组织和编排者,它的服务调用有这样两种路径:

第一种是应用服务调用并组装领域服务。此时领域服务会组装实体和实体方法,实现核心领域逻辑。领域服务通过仓储服务获取持久化数据对象,完成实体数据初始化。

第二种是应用服务直接调用仓储服务。这种方式主要针对像缓存、文件等类型的基础层数据访问。这类数据主要是查询操作,没有太多的领域逻辑,不经过领域层,不涉及数据库持久化对象。

微服务之间的服务调用

微服务之间的应用服务可以直接访问,也可以通过 API 网关访问。由于跨微服务操作,在进行数据新增和修改操作时,你需关注分布式事务,保证数据的一致性。

领域事件驱动

领域事件驱动包括微服务内和微服务之间的事件。微服务内通过事件总线(EventBus)完成聚合之间的异步处理。微服务之间通过消息中间件完成。异步化的领域事件驱动机制是一种间接的服务访问方式。当应用服务业务逻辑处理完成后,如果发生领域事件,可调用事件发布服务,完成事件发布。当接收到订阅的主题数据时,事件订阅服务会调用事件处理领域服务,完成进一步的业务操作。

服务依赖

服务依赖

DDD 分层架构有一个重要的原则就是:每层只能与位于其下方的层发生耦合。

那根据耦合的紧密程度,分层架构可以分为两种:严格分层架构和松散分层架构。在严格分层架构中,任何层只能与位于其直接下方的层发生依赖。在松散分层架构中,任何层可以与其任意下方的层发生依赖。

在松散分层架构中,领域层的实体方法和领域服务可以直接暴露给应用层和用户接口层。松散分层架构的服务依赖关系,无需逐级封装,可以快速暴露给上层。

但它存在一些问题,第一个是容易暴露领域层核心业务的实现逻辑;第二个是当实体方法或领域服务发生服务变更时,由于服务同时被多层服务调用和组合,不容易找出哪些上层服务调用和组合了它,不方便通知到所有的服务调用方。

严格分层架构可以避免将核心业务逻辑的实现暴露给外部,将实体和方法封装成领域服务,也可以避免在应用层沉淀过多的本该属于领域层的核心业务逻辑,避免应用层变得臃肿。还有就是当服务发生变更时,由于服务只被紧邻上层的服务调用和组合,你只需要逐级告知紧邻上层就可以了,服务可管理性比松散分层架构要好是一定的。

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

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

相关文章

ViLT 论文精读【论文精读】

ViLT 论文精读【论文精读】_哔哩哔哩_bilibili 目录 ViLT 论文精读【论文精读】_哔哩哔哩_bilibili 1 地位 2 ViLT做了什么能让它成为这种里程碑式的工作? 3 ViLT到底把模型简化到了什么程度?到底能加速到什么程度? 2.1 过去的方法是怎…

MATLAB实战 | 不同形式的三维曲面图

通常,MATLAB中绘制三维曲面图,先要生成网格数据,再调用mesh函数和surf函数绘制三维曲面。若曲面用含两个自变量的参数方程定义,则还可以调用fmesh函数和fsurf函数绘图。若曲面用隐函数定义,则可以调用fimplicit3函数绘…

使用com组件编辑word

一个普通的窗体应用,6个button using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; u…

JavaScript 的初步学习上篇

JavaScript 的介绍 JavaScript 之父 布兰登 . 艾奇 (Brendan Eich) ,1995 年, 用 10 天时间完成 JavaScript 的设计. JavaScript 和 Java 的关系 两者之间就像老婆和老婆饼的关系,即毫无关系, JavaScript 最初的名字叫LiveScript,为了蹭 Java 热度,才改名为 JavaScript.JavaScr…

JVM——垃圾回收器(Serial,SerialOld,ParNew,CMS,Parallel Scavenge,Parallel Old)

目录 1.垃圾回收器的组合关系1.年轻代-Serial垃圾回收器2.老年代-SerialOld垃圾回收器3.年轻代-ParNew垃圾回收器4.老年代- CMS(Concurrent Mark Sweep)垃圾回收器CMS执行步骤:CMS垃圾回收器存在的问题缺点:CMS垃圾回收器存在的问题 – 线程资源争抢问题…

【Redis缓存】RedisTemplate如何获取符合要求的key,批量获取key

RedisTemplate如何获取符合要求的key,批量获取key 一、方法/命令二、数据使用 一、方法/命令 如果使用命令的形式,输入以下命令即可 keys *如果使用RedisTemplate,则方法为 redisTemplate.keys()获取所有符合条件的key。 二、数据使用 redis中缓存了…

动静分离+多实例实验(nginx+tomcat)

Nginx服务器:192.168.188.14:80 Tomcat服务器1:192.168.188.11:80 Tomcat服务器2:192.168.188.12:8080 192.168.188.12:8081 部署Nginx负载均衡器 关闭防火墙 systemctl stop firewalld setenforce 0 安装依赖 yum -y install pcre-dev…

自研分布式IM-HubuIM RFC草案

HubuIM RFC草案 消息协议设计 基本协议 评估标准 【性能】协议传输效率,尽可能降低端到端的延迟,延迟高于200ms用户侧就会有所感知 【兼容】既要向前兼容也要向后兼容 【存储】减少消息包的大小,降低空间占用率,一个字节在亿…

抖音本地生活服务商申请要多久审核通过?

近年来,随着互联网的普及和社交媒体的兴起,本地生活服务行业也迎来了巨大的发展机遇。作为最受欢迎的短视频平台之一,抖音也不例外。抖音本地生活服务商申请要多久审核通过?这是许多想要加入抖音本地服务行业的人们最关心的问题之…

paddleocr笔记

PP-OCRv1 PP-OCR中,对于一张图像,需要完成以下3个步骤提取其中的文字信息: 使用文本检测方法,获取文本区域多边形信息(PP-OCR中文本检测使用的是DBNet,因此获取的是四点信息)。对上述文本多边形…

Echarts-使用渐变色填充

垂直方向的渐变 color: {type: linear,// x0,y1,柱子的颜色在垂直方向渐变x: 0,y: 1,colorStops: [// 0%处的颜色{offset: 0,color: #12c2e9,},// 50%处的颜色{offset: 0.5,color: #c471ed,},// 100%处的颜色{offset: 1,color: #f64f59,},],global: false // 缺省为 false} 水…

11.25快速排序细节,sort函数,二分使面积差最小

快速排序 i从左边开始&#xff0c;遇到大于等于x的停下来&#xff0c;j遇到小于等于x的停下来&#xff0c;然后如果i<j&#xff0c;就交换。 递归可能有两种写法&#xff0c;即开始&#xff0c;左-1&#xff0c;左&#xff0c;结束 与开始&#xff0c;右&#xff0c;右1&a…