术语字典
想学习一种思想或一门技术,就需要理解这个思想或技术所赖以存在的环境,也称语境
,也就是俗称的行话
,或者专业术语
。
本想将这些概念融入到具体案例分析之中,但结合自己的学习经历和痛点,觉得有一本小册子一样的东西可能更好,尤其是对于DDD这种复杂度稍高的设计思想来说尤其如此。一来方便集中查阅,二来也可以集中做出解释,避免出现前后不一致的混乱。
通常情况下,现实世界与软件世界的映射关系是这样的。
现实世界有什么
事物
,软件世界就有什么对象
。现实世界有什么
行为
,软件世界就有什么方法
。现实世界有什么
关系
,软件世界就有什么关联
。
统一语言
有的地方中也称为通用语言
,但个人认为统一语言
更合适一些,因为DDD是为了厘清业务边界和业务内容的,所有的参与者都在一个统一的语境下完成工作,而且也可以和UML(统一建模语言)关联起来。
所谓统一语言
,其实就是一个术语转换表。
例如,跨境电商的业务专家可能会说虚拟物流
、无形损耗
、堆码
、共同配送
、拣选报关行
、JIT
、战争附加费
等,这些对工程师来说完全是一头雾水。反过来,技术专家可能会说幂等
、熔断限流
、令牌桶
、异步
、回调
、适配器模式
、SnowFlake
等,这些对业务专家来说也同样是云里雾里。
所以就要专家们都坐下来,花时间把这些不同专业领域的术语做一个翻译,形成术语表
(类似数据字典,也可以看成是业务界的i18n或l10n)。
这其中也包括英文和拼音的命名规范。例如,同样是用来表示性别
的字段,如果后端是gender
,而前端是sex
(或者相反),那么就会造成信息混乱。这同时也为编码规范提供依据,消除代码中的魔法数字。
领域建模
这DDD中进行业务抽象的一个过程。
如前所述,DDD中包括业务分析
、战略设计
和战术设计
这三大过程,也就是把现实世界中的问题搬
到软件世界中,用对应的技术手段去解决掉。
拿买东西来说,它在现实世界中是一手交钱一手交货
,而到了软件世界中就是这样的过程。
从我的账户转账到商家账户(这里的往来账户可以是任意组合,如银行卡、钱包余额、第三方支付、红包、卡券、积分等)。
商家收到支付成功通知。
商家收到支付成功的通知后就开启发货进程,但需要用户确认收货后商家才能真正收到钱。
领域建模(或领域模型)就是实现这种异构世界之间的转换
的一种方法或行为。
业务分析
和通常意义上的业务分析不同,DDD中的业务分析
主要是通过事件风暴的方式快速实现对复杂的业务问题的拆解,从而提取开发需要的(领域)设计模型。
事件风暴类似于头脑风暴,但和头脑风暴不同的是,它还需要明确各类由风暴
产生的实际活动之间的联系,也需要明确所有参与者的输入和输出。
参与者主要包括业务专家、DDD专家、架构师、产品经理、项目经理、开发人员、设计人员、测试人员、运维人员等角色。
拿电商中的WMS子系统来说,其实际活动可能包括:用户下单
、生成拣货单
、生成拣货任务
、拣取货品
、订单合并
、订单校验
、封装包裹
这一系列步骤。

战略设计
从宏观层面来说,战略设计
就是公司的发展目标和实现这些目标的路径。
从DDD层面来说,就是根据业务分析
得到统一语言
、领域分析
、领域模型
,形成清晰的限界上下文
,并进行领域建模。
领域建模可以说是DDD中极为核心和关键的步骤。DDD设计的好不好,直接取决于这一步,它的大致步骤是如下。
找出
实体
和值对象
等领域对象。构建
聚合
和聚合根
。划分聚合到相应的
限界上下文
。
战术设计
有了限界上下文
,也就有了(微)服务规划和拆分的依据。
所以,在战术设计
环节中,就是从实际技术角度出发,用代码来实现战略设计
中的那些领域模型,它包括但不限于识别下面这些东西。
聚合
聚合根
实体
值对象
领域服务
领域事件
领域事件
所谓领域事件
是指在执行业务或业务流程的过程中产生的动作。
对核心业务有实质性影响的事件才叫领域事件
。
例如,在购物车中结算商品时,如果用户支付成功,那么就需要创建对应的订单、同时要扣除库存,订单创建和扣减库存就是领域事件。
而用户登录失败后给出提示,就不是领域事件(但登录失败后锁定账户,也有可能是或不是领域事件)。
领域事件可以是业务流程的一个步骤。例如,登录后增加积分。
也可以是执行定时任务过程中发生的事件。例如,提现额度的定期调整。
或者一个事件发生后触发的后续动作。例如,转账失败后钱款原路退回。
领域事件通常是通过解耦的方式实现业务间的异步化执行。
因为领域事件的本质在于事件的解耦
和异步
。
领域服务
用于执行领域操作和业务规则,就是将一些似是而非的非核心功能单独剥离出来。
例如,对于创建订单而言,一些权益和账户的校验并不属于核心业务,但会给核心业务造成影响。
或者当用户下单却未付款时,超时后需要自动删除该订单。
这种操作既不由用户来完成,也不由商家来完成,所以将它作为领域服务合情合理。
DDD的提出者Eric Evans认为好的领域服务应该具备如下特征。
相关操作不是实体或者值对象的本质部分(可有可无)。
接口定义在领域模型的其他元素中。
操作是无状态的。
事件风暴
事件即事实,即已经发生的并且能对业务产生实质性影响的事实,并且它可能需要保存下来或者得到响应。
官方的定义是一种以协作探索复杂业务领域为目标的,灵活的作坊(workshop)形式的活动
。
用大白话说就是拉上业务方(甲方)、产品经理、技术经理、研发工程师、测试工程师、运维工程师、UI设计师等若干角色,以头脑风暴的形式探讨业务规则、流程及约束,使业务得到真正的统一的认识,避免各方的理解误差。

限界上下文
限界
就是领域的边界,上下文
则是语义环境。通过限定领域的语义环境,就可以在统一的领域边界内用统一的语言交流,而不致于出现理解偏差。
拿商品举例,虽然商品有自身的较为固定的属性,但从DDD的角度来看,在不同的子域应该是有所侧重。
在商品子域,就是商品本身所属的领域,自然是需要具备完整的业务属性。
在店铺子域,由于店铺本身就是商品售卖的聚集地,因此和商品子域的重合度会比较高。而且不同的业务领域,其对应的业务属性也会有略微差异。例如对于某宝来说,店铺子域应该是没有
成色
这个属性的,但如果是二手商品的电商平台就必定会有。包括批发价
,某宝是没有的,但这里仍然显示出来,只为说明业务领域的不同会造成业务属性的不同这个问题。在订单子域,由于订单只关心商品名称、规格、数量和单价,因此大大减少了业务属性的数量,但有一些衍生出来的业务属性。
在物流子域,它只关心商品的仓储、运输、配送状态,最多还需要确认商品是否在有效期内,也有一些因此衍生出来的业务属性。
除了业务属性之外,还需要对应的业务操作。例如,店铺子域可以有商品的上下架,其他子域是没有的,但这里暂时不考虑操作,仅划分属性。

其他划分限界上下文
的方式有这么两种。
具有逻辑一致性的规则和操作一般都会划在
相同的限界上下文
里面。例如,商品的编辑、存草稿、上下架、预览,一般都会在同一个限界上下文里。具有不同时间线的业务需求一般都划分为
不同的限界上下文
。例如,浏览商品、加入购物车、下单/支付、发货、确认收货、评价一般都会在不同限界上下文里。
上下文映射
既然有了限界上下文
,那这些限界上下文
之间就一定会有某种逻辑顺序或调用关系,也就是业务之间的调用关系和流程。
理论上存在着若干种上下文之间的映射关系,但实际上每家公司的业务都是不同的,而且公司的开发文化、技术决策、组织结构都不相同,所以上下文映射也会有些差别。
DDD根据团队协作的方式与紧密程度,定义了若干种团队协作模式。
分离(Separate Way)上下文
:就是两个限界上下文之间没有半毛钱的关系,这也是最好的关系。客户-供应(Customer-Supplier)上下文
:这体现的是一种体现上下游的合作关系,上游标记为U(Upstream)
,下游标记为D(Downstream)
。遵奉者(Conformist)上下文
,是客户-供应
关系的变种,它指的是下游团队完全依赖于上游团队的服务,而且还无法改变。这个在跨团队,甚至跨组织的应用中很可能会出现。之所以有这种情况发生,就是为了抵消模型变换的成本。OHS(Open Host Service) + PL(Published Language)上下文
:开放主机服务
+发布语言
,也算是客户-供应
关系的变种,其实就是上游承诺提供一组不会改变的接口服务给下游。例如,云服务商提供的应用API接口就属于这种模式。发布-订阅(Publish-Subscribe)上下文
:一般是通过消息中间件来解耦,消息生产者标记为P
,消费者标记为S
。共享内核(Shared Kernel)上下文
:通常是为其他业务服务代码或微服务提供代码库,例如jar
包,这比较容易造成耦合,而且改动起来也相当麻烦,牵一发动全身,一般老系统中比较多见。合作关系(Partnership)上下文
:少了其中的一个,另一个也难以继续存在。一种强耦合关系,甚至循环依赖,变动时容易引发问题。

U-D
其实就是客户端-服务器
的另一种表述方式,也可以用自己的理解来代替U-D
。
防腐层
防腐层
又名ACL(Anti Corruption Layer)
,其实就是迪米特法则的应用,它主要用于解决如下问题。
实现新老系统之间的隔离。
对上游系统进行改造,不影响下游系统。
对接口进行封装,对调用者屏蔽内部信息。
使用适配器模式(Adapter)将老接口转换为适合调用者的接口。
防腐层
其实并不是什么新概念,大多数人其实一直都在使用它。
例如,在MVC中Controller
负责调用Service
层提供的服务。如果现在Service
接口的参数放生变化(参数名改变、参数类型改变,或者参数的个数改变),那么调用的时候就会抛异常。为了解决这个问题,就需要一个专门的机制来处理并且对Controller
屏蔽这种变化。
这种屏蔽不同系统之间的变化,或者封装访问方法的机制就称之为防腐层
。

领域与子域
任何业务系统都会有自己需要招揽的顾客、需要服务的用户和需要解决的问题,不同的业务系统,这些内容都是不一样的。
例如,电商系统和社交系统,虽然都可以卖东西,也都可以互发消息,也都可以建群组,但他们的核心关注点,也就是需要解决的核心问题肯定是不一样的。
领域
,顾名思义,就是范围与边界。好像动物世界中狮子的领地一样。DDD也会不断对业务进行抽象、归类、细分,当这项任务完成后,就会形成初步的业务边界,即哪些问题是需要解决的,哪些是暂时不用关注的,而哪些又是毫无关系的,在边界之内就是可以解决业务问题的领域模型。
将领域进一步划分,按照不同的业务单元,就像每家每户都有客厅、主卧、次卧、厨房、卫生间一样,这些不同的业务单元虽然都从属于同一个大领域,但把它们再划分成更小的子领域会有利于问题的解决,正如主卧、厨房和卫生间的风格、功能和装修花费肯定是不同的一样。
一个领域一般会有多个子域,这些子域如果过大,还可以再拆分。例如,客厅连接的阳台可以封闭成一个小空间,其装修又会和客厅有所不同。

子域和限界上下文
一般有两种不同的关系。
一对多
的关系,即子域:限界上下文 = 1:n
。一对一
的关系,即子域:限界上下文 = 1:1
,这样便于维护,其实大多数情况下都是这种关系。
到底是1:n
还是1:1
,更多取决于公司业务规模。
例如,互联网大厂的物流子域就是一个非常庞大的业务,它里面又可分为仓储、运输、配送等子域。那么此时物流子域和限界上下文就可能是一对多的关系,而且还可能是1 : n : m
的关系。

子域多了以后,就要搞清楚一件事:究竟哪些才是最重要的?哪些是都会做的?为了区分很多不同的子域,就出现了核心域
、通用域
和支撑域
的概念。
核心域
就是业务的核心功能。例如,对于电商系统来说,商品
、店铺
、订单
一定是最核心功能的功能。而对于社交系统,即时通讯、通讯录和群组也是最核心的功能。
即使是对于同是电商企业的某宝和某东来说,由于业务属性的不同,其核心域
也是不同的。
例如,对于某宝来说,租户、佣金一定是核心域。但对于某东来说,因为其自营模式比重较大,那么它的仓储、物流和供应链就会是核心域
。
支撑域
以开发为例,对于电商系统来说,除了需要有核心的业务功能以外,一些非核心的业务功能其实也是不可或缺的,因为没有它们,可能核心功能就会运行不畅,比如统计分析、客服工单。
对于DDD来说,支撑域
具有一定的企业专属特性,但不具有通用性。例如,某宝和某东的商业报表从设计到结果都会不一样。
通用域
还是以开发为例,除了核心功能(核心域)和非核心功能(支撑域)之外,还有一部分内容其实是几乎每个电商系统都会有的。
例如,权限系统、监控系统、日志系统,这类系统没有太多定制化的东西,业务属性较弱,对接任何平台都可以,指标也几乎都相同,也没有针对特定企业的限制,这些可以统统放到通用域
中。

实体和值对象
实体(Entity)
是领域模型中富有业务行为且具备唯一标识
的对象。
唯一标识
:可以是用户指定的,可以是系统生成的,也可以是其他限界上下文中传递过来的。业务形态
:在不同的子域中,同一个实体往往有不同的形态,如商品子域中的商品、订单子域中的商品。运行形态
:在相同的子域中,同一个实体有不同的运行形态,如DO
、VO
、DTO
等。
值对象(Value Object)
则用来描述领域的特定方面的属性或属性集合,并且没有唯一标识
。
例如,省/市/区/县不同层次的地址共同组成了收货地址
这个属性集。
package com.itechthink.ddd.datatype;
/**
* 实体和值对象
*
*/
// 用户实体,有唯一标识guid
public class UserEntity {
// 值对象,唯一标志符
public String guid;
// 单一属性值对象
public String name;
// 单一属性值对象
public int gender;
// 多属性值对象,被实体引用
public Address address;
}
// 值对象,无唯一标识
public class Address {
// 单一属性值对象
public String province;
// 单一属性值对象
public String city;
// 单一属性值对象
public String town;
// 单一属性值对象
public String street;
}
聚合和聚合根
聚合
就是一组业务相关对象的集合,由业务紧密关联的实体和值对象组合而成,且具备业务约束。例如,一个公司中的部门,就是共同从事相同职责的员工的聚合。
一个限界上下文
可能包含多个聚合
,一个聚合
可以包含多个实体
和值对象
。
每个聚合
都有一个聚合根
,它类似于一个组织的负责人,它自身也是实体
,且还是实体的管理者。
对聚合
的访问就是通过聚合根
进行的,而聚合
里面的子实体
对外界是完全封闭的。
聚合
和聚合根
的生命周期必须保持一致,聚合根
及其内部实体
和值对象
的声明和销毁需要同步。也就是说,如果聚合根
不存在了,那么聚合
也不应该继续存在。
例如,订单和订单详情组成的聚合
中,订单就是聚合根
,没有订单这个聚合根
,订单详情不可能单独存在。
工厂和资源库
在针对大型的复杂领域进行建模时,聚合
、实体
和值对象
之间的依赖关系可能会变得十分复杂,非常需要按照单一职责原则将对象的创建、使用和销毁行为相分离。
工厂(Factory)
就是专门来干这个事的,它封装实现,屏蔽细节。例如,仓库的分拣和包装工作,就是典型的工厂应用环节。
最好的例子莫过于Spring中的依赖注入
机制。
而资源库(Repository)
是对资源访问的抽象,这里指的资源不限于数据、网络、文件、多媒体等。
它用来解耦对象的操作和具体实现,常见的资源库包括数据资源、文件资源、物流资源、支付资源、消息资源等。
资源库
和业务逻辑无关,只负责资源的读取和保存,至于资源要怎么应用于业务不是资源库的职责。例如,库管员并不关心货品发到哪里,只负责照单发货即可。
有些地方也把资源库
称为仓储
。
总结
名词术语 | 大白话 | 说明 |
---|---|---|
统一语言 | 术语表,类似数据字典,也可以看成是业务中使用的i18n | 战争附加费(WRS):由于船舶、货物、港口及其它方面的种种原因,使得船方在运输货物时增加费用开支或蒙受经济损失,船方为补偿这些开支或损失向货主收取的费用。一般这个费用主要在地中海航线的LATTAKIA(叙利亚)、HAIFA(以色列)、ASHDOD(以色列)、BEIRUT(黎巴嫩)这几个港口出现 |
领域建模 | 将现实世界转换成软件世界的过程和方法 | 建筑施工图就是一种工程上的建设和建筑模型 |
业务分析 | 用事件风暴方式抽象出所需的业务模式、关系、流程和约束 | 产生施工图纸的过程 |
战略设计 | 得到限界上下文、映射关系、子域及其类型、通用语言 | 技术决策包括开发语言、框架选型、技术架构、层次划分、事务、数据架构等 |
战术设计 | 得到聚合(实体、值对象)、工厂、资源库、领域服务、领域事件、命令、业务组件 | 应用服务其实是类似于框架通用的代码或服务,例如日志、任务调度等。战术技术决策包括是否使用多模块依赖、规划DO、DTO、VO、PO |
领域事件 | 在执行业务或业务流程的过程中产生的动作,对核心业务有实质性影响的事件 | 支付成功后给用户发送通知,或者商家发货后给用户发送通知。领域事件和命令的区别在于:命令是人驱动的,而事件是由系统发起的 |
领域服务 | 有些业务逻辑方法不属于实体或值对象,就需要领域服务来完成 | 当用户下单却未付款时,超时后需要自动删除该订单。这种操作既不由用户来完成,也不由商家来完成,由领域服务完成最合适 |
事件风暴 | 类似于头脑风暴,但它还需要明确各类由风暴 产生的实际活动之间的联系,也需要明确所有参与者的输入、输出 | 例如:浏览商品 -> 下单 -> 权益校验 -> 支付 -> 发货 -> 确认收货 -> 交易完成 |
限界上下文 | 限定领域模型的语义边界,让业务边界和职责更加清晰 | 如果不划分限界上下文,那么在每一个子域,如商品子域、店铺子域、订单子域和物流子域中,商品这个实体都需要展现出全部的业务属性,这样显然是比较浪费时间和空间的 |
上下文映射 | 限界上下文之间的调用关系、顺序和流程 | 促销上下文依赖于商品上下文,而订单上下文又依赖于库存上下文 |
防腐层 | 运用软件设计原则对系统解耦 | 收银台和登录选择器就是防腐层最经典的例子 |
领域 | 不同的业务系统有不同的用户群体和不同的业务问题、流程 | 电商领域和社交领域,虽然都有订单、都有消息,但核心业务是完全不同的 |
子域 | 按照不同的业务单元对领域的进一步划分 | 电商中的用户、店铺、商品、订单、物流、结算等 |
核心域 | 包含企业核心业务和竞争力的子域 | 电商中的佣金结算子域和物流仓储子域,就属于电商核心业务 |
支撑域 | 除核心业务以外的一些非核心业务功能,而且具有企业自身专有属性,也就是不同的企业支撑域也会有不同 | 电商中的CRM、BI就是支撑域 |
通用域 | 一些偏技术层面但业务运行上又需要有的部分 | 认证与权限、平台运行监控子系统等 |
实体 | 领域模型中富有业务行为且具备唯一标识的对象 | 可利理解为数据库表,例如用户实体、订单实体 |
值对象 | 用来描述领域的特定方面的属性或属性集合,且没有唯一标识 | 例如用户详细地址,可以组成一个Address值对象,也可以拆分成省/市/区/县 |
聚合 | 一组业务相关对象的集合,类似于一个公司中的部门 | 用户和用户详情、订单和订单详情就是聚合关系 |
聚合根 | 在聚合关系中负责管理聚合的实体 | 订单和订单详情中的订单,就是聚合根,没有订单这个聚合根,订单详情是无法单独存在的 |
工厂 | 需要按照单一职责原则将对象的创建和使用相分离 | 需要将DO装配为DTO或VO时,就需要填补一些缺失的值对象,这种职责最适合由工厂来完成 |
资源库 | 对资源访问的抽象,这里指的资源不限于数据、网络、文件、多媒体等 | 数据库操作方言、文件操作方言等就是资源库的职责 |
至于诸如贫血模型
、充血模型
、业务组件
、CQRS
等概念,更多属于开发层面的术语,就不再啰嗦了。
感谢支持
更多内容,请移步《超级个体》。