领域驱动设计:领域模型与代码模型的一致性
文章目录
- 领域对象的整理
- 从领域模型到微服务的设计
- 领域层的领域对象
- 应用层的领域对象
- 领域对象与微服务代码对象的映射
- 典型的领域模型
- 非典型领域模型
- DDD 强调先构建领域模型然后设计微服务,以保证领域模型和微服务的一体性,因此我们不能脱离领域模型来谈微服务的设计和落地。但在构建领域模型时,我们往往是站在业务视角的,并且有些领域对象还带着业务语言。我们还需要将领域模型作为微服务设计的输入,对领域对象进行设计和转换,让领域对象与代码对象建立映射关系。
领域对象的整理
- 完成微服务拆分后,领域模型的边界和领域对象就基本确定了。我们第一个重要的工作就是,整理事件风暴过程中产生的各个领域对象,比如:聚合、实体、命令和领域事件等内容,将这些领域对象和业务行为记录到下面的表格中。
- 可以看到,这张表格里包含了:领域模型、聚合、领域对象和领域类型四个维度。一个领域模型会包含多个聚合,一个聚合包含多个领域对象,每个领域对象都有自己的领域类型。领域类型主要标识领域对象的属性,比如:聚合根、实体、命令和领域事件等类型。
从领域模型到微服务的设计
- 从领域模型到微服务落地,我们还需要做进一步的设计和分析。事件风暴中提取的领域对象,还需要经过用户故事或领域故事分析,以及微服务设计,才能用于微服务系统开发。这个过程会比事件风暴来的更深入和细致。主要关注内容如下:
- 分析微服务内有哪些服务?
- 服务所在的分层?
- 应用服务由哪些服务组合和编排完成?
- 领域服务包括哪些实体的业务逻辑?
- 采用充血模型的实体有哪些属性和方法?
- 有哪些值对象?
- 哪个实体是聚合根等?
- 最后梳理出所有的领域对象和它们之间的依赖关系,我们会给每个领域对象设计对应的代码对象,定义它们所在的软件包和代码目录。
- 这个设计过程建议参与的角色有:DDD 专家、架构师、设计人员和开发经理。
领域层的领域对象
- 事件风暴结束时,领域模型聚合内一般会有:聚合、实体、命令和领域事件等领域对象。在完成故事分析和微服务设计后,微服务的聚合内一般会有:聚合、聚合根、实体、值对象、领域事件、领域服务和仓储等领域对象。
1. 设计实体
- 大多数情况下,领域模型的业务实体与微服务的数据库实体是一一对应的。但某些领域模型的实体在微服务设计时,可能会被设计为多个数据实体,或者实体的某些属性被设计为值对象。
- 我们分析个人客户时,还需要有地址、电话和银行账号等实体,它们被聚合根引用,不容易在领域建模时发现,我们需要在微服务设计过程中识别和设计出来。
- 在分层架构里,实体采用充血模型,在实体类内实现实体的全部业务逻辑。这些不同的实体都有自己的方法和业务行为,比如地址实体有新增和修改地址的方法,银行账号实体有新增和修改银行账号的方法。
- 实体类放在领域层的 Entity 目录结构下。
2. 找出聚合根
- 聚合根来源于领域模型,在个人客户聚合里,个人客户这个实体是聚合根,它负责管理地址、电话以及银行账号的生命周期。个人客户聚合根通过工厂和仓储模式,实现聚合内地址、银行账号等实体和值对象数据的初始化和持久化。
- 聚合根是一种特殊的实体,它有自己的属性和方法。聚合根可以实现聚合之间的对象引用,还可以引用聚合内的所有实体。聚合根类放在代码模型的 Entity 目录结构下。聚合根有自己的实现方法,比如生成客户编码,新增和修改客户信息等方法。
3. 设计值对象
- 根据需要将某些实体的某些属性或属性集设计为值对象。值对象类放在代码模型的 Entity目录结构下。在个人客户聚合中,客户拥有客户证件类型,它是以枚举值的形式存在,所以将它设计为值对象。
- 有些领域对象可以设计为值对象,也可以设计为实体,我们需要根据具体情况来分析。如果这个领域对象在其它聚合内维护生命周期,且在它依附的实体对象中只允许整体替换,我们就可以将它设计为值对象。如果这个对象是多条且需要基于它做查询统计,建议将它设计为实体。
4. 设计领域事件
- 如果领域模型中领域事件会触发下一步的业务操作,我们就需要设计领域事件。首先确定领域事件发生在微服务内还是微服务之间。然后设计事件实体对象,事件的发布和订阅机制,以及事件的处理机制。判断是否需要引入事件总线或消息中间件。
- 在个人客户聚合中有客户已创建的领域事件,因此它有客户创建事件这个实体。
- 领域事件实体和处理类放在领域层的 Event 目录结构下。领域事件的发布和订阅类建议放在应用层的 Event 目录结构下。
5. 设计领域服务
- 如果一个业务动作或行为跨多个实体,我们就需要设计领域服务。领域服务通过对多个实体和实体方法进行组合,完成核心业务逻辑。你可以认为领域服务是位于实体方法之上和应用服务之下的一层业务逻辑。
- 按照严格分层架构层的依赖关系,如果实体的方法需要暴露给应用层,它需要封装成领域服务后才可以被应用服务调用。所以如果有的实体方法需要被前端应用调用,我们会将它封装成领域服务,然后再封装为应用服务。
- 个人客户聚合根这个实体创建个人客户信息的方法,被封装为创建个人客户信息领域服务。然后再被封装为创建个人客户信息应用服务,向前端应用暴露。
- 领域服务类放在领域层的 Service 目录结构下。
6. 设计仓储
- 每一个聚合都有一个仓储,仓储主要用来完成数据查询和持久化操作。仓储包括仓储的接口和仓储实现,通过依赖倒置实现应用业务逻辑与数据库资源逻辑的解耦。
- 仓储代码放在领域层的 Repository 目录结构下。
应用层的领域对象
- 应用层的主要领域对象是应用服务和事件的发布以及订阅。
- 在事件风暴或领域故事分析时,往往会根据用户或系统发起的命令,来设计服务或实体方法。为了响应这个命令,需要分析和记录:
- 在应用层和领域层分别会发生哪些业务行为;
- 各层分别需要设计哪些服务或者方法;
- 这些方法和服务的分层以及领域类型(比如实体方法、领域服务和应用服务等),它们之间的调用和组合的依赖关系。
- 在严格分层架构模式下,不允许服务的跨层调用,每个服务只能调用它的下一层服务。服务从下到上依次为:实体方法、领域服务和应用服务。
- 如果需要实现服务的跨层调用,我们应该怎么办?建议采用服务逐层封装的方式。
服务的封装和调用主要有以下几种方式:
- 实体方法的封装:实体方法是最底层的原子业务逻辑。如果单一实体的方法需要被跨层调用,你可以将它封装成领域服务,这样封装的领域服务就可以被应用服务调用和编排了。如果它还需要被用户接口层调用,你还需要将这个领域服务封装成应用服务。经过逐层服务封装,实体方法就可以暴露给上面不同的层,实现跨层调用。封装时服务前面的名字可以保持一致,你可以用 *DomainService 或 *AppService 后缀来区分领域服务或应用服务。
- 领域服务的组合和封装:领域服务会对多个实体和实体方法进行组合和编排,供应用服务调用。如果它需要暴露给用户接口层,领域服务就需要封装成应用服务。
- 应用服务的组合和编排:应用服务会对多个领域服务进行组合和编排,暴露给用户接口层,供前端应用调用。在应用服务组合和编排时,你需要关注一个现象:多个应用服务可能会对多个同样的领域服务重复进行同样业务逻辑的组合和编排。当出现这种情况时,你就需要分析是不是领域服务可以整合了。你可以将这几个不断重复组合的领域服务,合并到一个领域服务中实现。这样既省去了应用服务的反复编排,也实现了服务的演进。这样领域模型将会越来越精炼,更能适应业务的要求。应用服务类放在应用层 Service 目录结构下。领域事件的发布和订阅类放在应用层 Event目录结构下。
领域对象与微服务代码对象的映射
典型的领域模型
- 个人客户领域模型中的个人客户聚合,就是典型的领域模型,从聚合内可以提取出多个实体和值对象以及它的聚合根。
- 我们看一下下面这个图,我们对个人客户聚合做了进一步的分析。提取了个人客户表单这个聚合根,形成了客户类型值对象,以及电话、地址、银行账号等实体,为实体方法和服务做了封装和分层,建立了领域对象的关联和依赖关系,还有仓储等设计。关键是这个过程,我们建立了领域对象与微服务代码对象的映射关系。
- 层:定义领域对象位于分层架构中的哪一层,比如:接口层、应用层、领域层以及基础层等。
- 领域对象:领域模型中领域对象的具体名称。
- 领域类型:根据 DDD 知识体系定义的领域对象的类型,包括:限界上下文、聚合、聚合根、实体、值对象、领域事件、应用服务、领域服务和仓储服务等领域类型。
- 依赖的领域对象:根据业务对象依赖或分层调用的依赖关系,建立的领域对象的依赖关系,比如:服务调用依赖、关联对象聚合等。
- 包名:代码模型中的包名,对应领域对象所在的软件包。
- 类名:代码模型中的类名,对应领域对象的类名。
- 方法名:代码模型中的方法名,对应领域对象实现或操作的方法名。
- 在建立这种映射关系后,就可以得到如下图的微服务代码结构了。
非典型领域模型
- 有些业务场景可能并不能如你所愿,你可能无法设计出典型的领域模型。这类业务中有多个实体,实体之间相互独立,是松耦合的关系,这些实体主要参与分析或者计算,你找不出聚合根,但就业务本身来说它们是高内聚的。而它们所组合的业务与其它聚合是在一个限界上下文内,你也不大可能将它单独设计为一个微服务。
- 这种业务场景其实很常见。比如,在个人客户领域模型内有客户归并的聚合,它扫描所有客户,按照身份证号码、电话号码等是否重复的业务规则,判断是否是重复的客户,然后对重复的客户进行归并。这种业务场景你就找不到聚合根。
- 那对于这类非典型模型,我们还是可以借鉴聚合的思想,仍然用聚合来定义这部分功能,并采用与典型领域模型同样的分析方法,建立实体的属性和方法,对方法和服务进行封装和分层设计,设计仓储,建立领域对象之间的依赖关系。唯一可惜的就是我们依然找不到聚合根,不过也没关系,除了聚合根管理功能外,我们还可以用 DDD 的其它设计方法。
你知道的越多,你不知道的越多。
相关文章:

领域驱动设计:领域模型与代码模型的一致性
文章目录 领域对象的整理从领域模型到微服务的设计领域层的领域对象应用层的领域对象 领域对象与微服务代码对象的映射典型的领域模型非典型领域模型 DDD 强调先构建领域模型然后设计微服务,以保证领域模型和微服务的一体性,因此我们不能脱离领域模型来谈…...

TypeScript命名空间和模块
🎬 岸边的风:个人主页 🔥 个人专栏 :《 VUE 》 《 javaScript 》 ⛺️ 生活的理想,就是为了理想的生活 ! 目录 命名空间(Namespace) 命名空间(Namespace)使用场景 第三方库 兼容…...
C++学习笔记--函数重载(1)
文章目录 序言一、洞悉函数重载决议1.1、重载决议的基本流程1.2、Name Lookup1.2.1、Qualified Name Lookup1.2.1.1、Class Member Lookup1.2.1.2、Namespace Member Lookup 1.2.2、Unqualified Name Lookup1.2.2.1、Usual Unqualified Lookup1.2.2.2、Argument Dependant Look…...

交叉编译poco-1.9.2
目录 一、文件下载二、编译三、可能遇到的问题和解决方法3.1 error "Unknown Hardware Architecture."3.2 error Target architecture was not detected as supported by Double-Conversion一、文件下载 下载地址:poco-1.9.2 二、编译 解压目录后打开build/config/…...
C++中如何处理超长的数字(long long类型的整数都无法存储的)
C中如何处理超长的数字(long long类型的整数都无法存储的) 在 C中,如果数字超出了 long long 类型的范围,可以考虑使用字符串或第三方库(如 Boost.Multiprecision)来表示和处理超长数字。要使用第三方库需…...
RabbitMQ MQTT集群方案官方说明
RabbitMQ MQTT 官方网说明 官方地址: https://www.rabbitmq.com/mqtt.html 从3.8开始,该MQTT插件要求存在一定数量的群集节点。这意味着三分之二,五分之三,依此类推。 该插件也可以在单个节点上使用,但不支持两个节点的集群。 如…...

深圳唯创知音电子将参加IOTE 2023第二十届国际物联网展•深圳站
2023年9月20~22日,深圳唯创知音电子将在 深圳宝安国际会展中心(9号馆9B1)为您全面展示最新的芯片产品及应用方案,助力传感器行业的发展。 作为全球领先的芯片供应商之一,深圳唯创知音电子一直致力于为提供高质量、…...

《TCP/IP网络编程》阅读笔记--I/O复用
目录 1--基于I/O复用的服务器 2--select()函数 3--基于I/O复用的回声服务器端 4--send()和recv()函数的常用可选项 5--readv()和writev()函数 1--基于I/O复用的服务器 多进程服务器端具有以下缺点:当有多个客户端发起连接请求时,就会创建多个进程来…...
[C#] 允许当前应用程序通过防火墙
通常在一台装有防火墙的电脑上运行程序的场合,往往会弹出对话框提示:是否允许执行该应用程序。 我们在开发软件的时候,可以事先在软件里面设置当前软件为防火墙允许通过的软件。这样,用户在使用时就可以避开前面提到的弹框了。 在…...

帆软FineReport决策报表Tab实现方案
最近有个需求是要做首页展示,为了减少前端工作量,利用采购的帆软FineReport来实现,记录过程,方便备查。 需求 做个Tab页,实现多个页切换。 方案一、利用帆软自带切换 帆软自带的有Tab控件,可实现切换&a…...
只打印文名
CMakeLists.txt set(CMAKE_C_FLAGS "-O0 -ggdb -D__NOTDIR_FILE__$(notdir $<)") // set(CMAKE_C_FLAGS "-O0 -ggdb -D__NOTDIR_FILE__$(notdir $<) -D__FILENAME__$(subst $(dir $<),,$<)")C文件 #include <stdio.h>#ifdef __NOTDIR_…...

【经典小练习】JavaSE—拷贝文件夹
🎊专栏【Java小练习】 🍔喜欢的诗句:天行健,君子以自强不息。 🎆音乐分享【如愿】 🎄欢迎并且感谢大家指出小吉的问题🥰 文章目录 🎄效果🌺代码🛸讲解&#x…...

FPGA-结合协议时序实现UART收发器(六):仿真模块SIM_uart_drive_TB
FPGA-结合协议时序实现UART收发器(六):仿真模块SIM_uart_drive_TB 仿真模块SIM_uart_drive_TB,仿真实现。 vivado联合modelsim进行仿真。 文章目录 FPGA-结合协议时序实现UART收发器(六):仿真模…...

Spring Boot集成EasyExcel实现数据导出
在本文中,我们将探讨如何使用Spring Boot集成EasyExcel库来实现数据导出功能。我们将学习如何通过EasyExcel库生成Excel文件,并实现一些高级功能,如支持列下拉和自定义单元格样式,自适应列宽、行高,动态表头 ÿ…...
EasyExcel3.0读(日期、数字或者自定义格式转换)
EasyExcel 3.0读(日期、数字或者自定义格式转换) 依赖 <dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>3.2.1</version> </dependency>对象 package com.xiaobu.entity.vo;import …...

浅谈C++|STL之vector篇
一.vector的基本概念 vector是C标准库中的一种动态数组容器,提供了动态大小的数组功能,能够在运行时根据需要自动扩展和收缩。vector以连续的内存块存储元素,可以快速访问和修改任意位置的元素。 以下是vector的基本概念和特点: 动…...
微信、支付宝修改步数【小米运动】
简介 小米运动是一款流行的健身应用,可以记录用户的步数和运动数据。然而,有些用户希望能够修改步数,以达到一些特定的目的。本文将介绍一个Python脚本,可以帮助用户实现修改小米运动步数的功能。 正文 脚本介绍: 本脚本是一个Python脚本,用于修改小米运动步数。通过模…...
stu02-初识HTML
1.HTML概述 (1)HTML是Hyper Text Mark-up Language的首字母缩写。 (2)HTML是一种超文本标记语言。 (3) 超文本:指除了文字外,页面内还可以包含图片、链接、甚至音乐、视频等非文字元…...

软件测试7大误区
随着软件测试对提高软件质量重要性的不断提高,软件测试也不断受到重视。但是,国内软件测试过程的不规范,重视开发和轻视测试的现象依旧存在。因此,对于软件测试的重要性、测试方法和测试过程等方面都存在很多不恰当的认识…...

【深度学习】 Python 和 NumPy 系列教程(十二):NumPy详解:4、数组广播;5、排序操作
目录 一、前言 二、实验环境 三、NumPy 0、多维数组对象(ndarray) 多维数组的属性 1、创建数组 2、数组操作 3、数组数学 4、数组广播 5、排序操作 1. np.sort() 函数 2. np.argsort() 函数 3. ndarray.sort() 方法 4. 按列或行排序 5. n…...

突破不可导策略的训练难题:零阶优化与强化学习的深度嵌合
强化学习(Reinforcement Learning, RL)是工业领域智能控制的重要方法。它的基本原理是将最优控制问题建模为马尔可夫决策过程,然后使用强化学习的Actor-Critic机制(中文译作“知行互动”机制),逐步迭代求解…...

《从零掌握MIPI CSI-2: 协议精解与FPGA摄像头开发实战》-- CSI-2 协议详细解析 (一)
CSI-2 协议详细解析 (一) 1. CSI-2层定义(CSI-2 Layer Definitions) 分层结构 :CSI-2协议分为6层: 物理层(PHY Layer) : 定义电气特性、时钟机制和传输介质(导线&#…...
测试markdown--肇兴
day1: 1、去程:7:04 --11:32高铁 高铁右转上售票大厅2楼,穿过候车厅下一楼,上大巴车 ¥10/人 **2、到达:**12点多到达寨子,买门票,美团/抖音:¥78人 3、中饭&a…...
Qwen3-Embedding-0.6B深度解析:多语言语义检索的轻量级利器
第一章 引言:语义表示的新时代挑战与Qwen3的破局之路 1.1 文本嵌入的核心价值与技术演进 在人工智能领域,文本嵌入技术如同连接自然语言与机器理解的“神经突触”——它将人类语言转化为计算机可计算的语义向量,支撑着搜索引擎、推荐系统、…...

学习STC51单片机31(芯片为STC89C52RCRC)OLED显示屏1
每日一言 生活的美好,总是藏在那些你咬牙坚持的日子里。 硬件:OLED 以后要用到OLED的时候找到这个文件 OLED的设备地址 SSD1306"SSD" 是品牌缩写,"1306" 是产品编号。 驱动 OLED 屏幕的 IIC 总线数据传输格式 示意图 …...

基于Docker Compose部署Java微服务项目
一. 创建根项目 根项目(父项目)主要用于依赖管理 一些需要注意的点: 打包方式需要为 pom<modules>里需要注册子模块不要引入maven的打包插件,否则打包时会出问题 <?xml version"1.0" encoding"UTF-8…...

12.找到字符串中所有字母异位词
🧠 题目解析 题目描述: 给定两个字符串 s 和 p,找出 s 中所有 p 的字母异位词的起始索引。 返回的答案以数组形式表示。 字母异位词定义: 若两个字符串包含的字符种类和出现次数完全相同,顺序无所谓,则互为…...

前端开发面试题总结-JavaScript篇(一)
文章目录 JavaScript高频问答一、作用域与闭包1.什么是闭包(Closure)?闭包有什么应用场景和潜在问题?2.解释 JavaScript 的作用域链(Scope Chain) 二、原型与继承3.原型链是什么?如何实现继承&a…...
ip子接口配置及删除
配置永久生效的子接口,2个IP 都可以登录你这一台服务器。重启不失效。 永久的 [应用] vi /etc/sysconfig/network-scripts/ifcfg-eth0修改文件内内容 TYPE"Ethernet" BOOTPROTO"none" NAME"eth0" DEVICE"eth0" ONBOOT&q…...

c++第七天 继承与派生2
这一篇文章主要内容是 派生类构造函数与析构函数 在派生类中重写基类成员 以及多继承 第一部分:派生类构造函数与析构函数 当创建一个派生类对象时,基类成员是如何初始化的? 1.当派生类对象创建的时候,基类成员的初始化顺序 …...