Notion 整理:DDD 实战课

DDD 核心概念

领域、子域

领域:从事一种专门活动或事业的范围、部类或部门。领域主要的关注点 —— 范围,也就是边界。在解决业务问题时,将大的问题拆分成边界清晰的小的问题。例如,外卖功能,包含着商品领域、优惠券领域、客服领域等等。

子域:将领域进一步划分为获得更多小的领域,也就是子领域。子领域和领域一样,都是具有边界清晰的特点,仅仅是解决的问题大小不一样。又可以类比 OKR 时,Leader 的 O 对应下属每个人的 O 的和。

从上可以看出,DDD 的核心思想就是将问题域逐步分解,降低业务理解和系统实现的复杂度。

核心域、通用域、支撑域

子域可以根据特点进行分类。公司核心竞争力 = 核心域,提供通用功能的 = 通用域,提供辅助功能的 = 支撑域。举个例子,外卖领域方面,订单服务属于核心域,计数服务、认证权限服务属于通用域,存储服务属于支撑域。三者在实现上没有明显差别。

商业模式的不同,虽然都是购物平台,淘宝、京东、拼多多的核心域并不完全相同。核心域的作用,就是让架构师等角色重点关注核心竞争力服务。而对于通用域,在人手不够的情况下,由于通用性,甚至可以通过购买方式来实现。

通用语言和限界上下文

通用语言不属于领域对象中,其作用是在于领域专家与业务技术人员在沟通的时候,需要一套通用的语言去交流。毕竟技术人员不理解领域内的专业名词、事件情况,领域专家也不理解技术实现上的专业名词、实现方式。具体通用语言应该是什么样子,因人而异,不需要很规范,只需要双方理解的含义是一致的就可以。例子可以参考《领域驱动设计》中作者举的例子。

限界上下文 = 限界 + 上下文。限界指的就是领域的边界,而上下文则是语义环境。两者结合,限界上下文就是指,限定某个特定的领域/环境/问题,提供上下文环境下,领域内的一些术语、对象等没有二义性。

比如说,孩子问‘今天穿几件衣服’,妈妈回道‘能穿多少穿多少’。没有上下文环境下,无法知道到底是穿多还是穿少。正如电商领域的商品一样,商品在不同的阶段有不同的术语,在销售阶段是商品,而在运输阶段则变成了货物。

实体、值对象

实体,有唯一标识符的对象。对这些对象而言,重要的不是其属性,而是其延续性和标识,对象的延续性和标识会跨越甚至超出软件的生命周期。

  • 业务形态。领域模型中的实体,是多个属性、操作或行为的载体。在头脑风暴(事件风暴)中,可以通过命令、操作或事件,找出产生这些行为的业务实体对象。
  • 代码形态。在代码实现时,需要使用充血模型,将这个实体相关的所有业务逻辑都放在该实体类内部,除非业务逻辑和其他实体相关,可能需要放在聚合(多个实体或值对象的聚合)中。
  • 运行形态。无论怎么变化,实体属性可能变,但是实体的唯一标识不会变。
  • 数据库形态。一个实体可能对应 0 个、1 个甚至多个数据库持久化对象。对应 0 个,实体不需要存储到数据库中。对应 1 个,常规形态。对应多个,为了加快查询,数据库进行了垂直分表。

值对象,通过多个属性来组成一个概念整体。典型的例子就是用户信息中的用户地址。用户地址需要包含用户的省、市、区,以及具体详细地址。这个并不需要唯一标识符,只是一堆属性的一个概念整体。当这个整体内任何一个属性变动,变动前后就不再是同一个对象了。

  • 业务形态。和实体一样,包含若干属性,可以与其他实体或值对象组成聚合。
  • 代码形态。单一属性或者 Class 类,作为实体或者聚合的一部分。具有不可变性,线程安全性(前提是实现的时候是)。
  • 运行形态。除了值数据初始化和整体替换行为外,其它业务行为就很少了。
  • 数据库形态。一般依附于实体表,序列化存储到一列中,或者每个属性对应一列。前者不易查询、插入性能低、业务逻辑变化时全部重序列化,后者表中属性过多、大宽表。也可以选择子表的方式,如果前两种都不好。

聚合、聚合根

聚合:业务或逻辑紧密联系的实体、值对象组成。聚合是数据修改和持久化的基本单元,每一个聚合对应一个仓储,实现数据持久化。举个例子,对于一个公司来说,里面有很多的员工、基础设施等属性,但是对外展现的时候是以一个公司这样的集体进行展现,具体内部员工情况如何并不关注。

聚合根:是一个特殊的实体。首先,聚合中是紧密联系的实体和值对象,聚合根就是协调这些实体和值对象完成所需业务逻辑。其次,聚合根控制了聚合对外暴露的接口,不可将一些不必要的内部属性,如实体,暴露出去。若需要聚合之间的协作,需要调用聚合根提供的接口,在聚合根内部去执行所需逻辑。

整体例子:以订单为例,订单在聚合里是聚合根,与订单关联的有订单明细和收货地址。订单明细包括商品 ID、商品名称、价格以及数量等信息,由于订单明细是多个,它是一个集合,它被设计为实体,被订单引用。而订单只有一个收货地址,这个收货地址的值来源于你个人中心维护的收货地址,收货地址只能被整体替换,所以它被设计为值对象。

在设计聚合时需要注意:

  • 边界内的不变性:聚合内部的业务逻辑不会因为边界外任何事物而变化。
  • 设计小聚合:若聚合内实体过多,管理复杂,可能可用性变差。
  • 通过唯一标识符引用聚合:聚合之间是通过关联外部聚合根 ID 的方式引用,而不是直接对象引用的方式。若直接对象引用,不就相当于外部聚合是该聚合的一部分了。
  • 边界之外使用最终一致性:聚合内强一致性,而聚合之间最终一致性。
  • 在应用层进行跨聚合的服务调用:避免跨聚合的领域服务调用和跨聚合的数据库表关联。

基于上述几个特点,聚合才做到“高内聚、低耦合”的特点。

领域事件

领域事件:业务流程中某个操作完成后,还会有后续一个或多个操作。例如用户下了订单后会有一系列后续逻辑,如验证订单合法性、用户支付订单、订单通知商家、商家快递后记录商品运输进度等等。当然还有一些类型,如用户登陆时,密码错误三次触发验证码校验,密码错误十次后停止登陆。

识别领域事件的方式,是发现类似“如果发生……,则……”、“当做完……的时候,请通知……”、“发生……时,则……”等关键词。

领域事件满足最终一致性。因为领域事件需要跨聚合,甚至需要跨微服务,因此需要满足最终一致性,而非强一致性。

领域事件分为两种,微服务内的领域事件和微服务间的领域事件。

  • 微服务内的领域事件,发生在聚合之间,可以通过创建事件实体 + 发布和订阅事件总线(EventBus)的方式来完成。由于聚合都在同一个进程中,不需要引入消息中间件。但如果要同时更新多个聚合,可能需要引入事件总线,但会增加开发复杂度。
    微服务内应用服务,可以通过跨聚合的服务编排和组合,以服务调用的方式完成跨聚合的访问,这种方式通常应用于实时性和数据一致性要求高的场景。这个过程会用到分布式事务,以保证发布方和订阅方的数据同时更新成功。(没懂 = =)
  • 微服务间的领域事件,要总体考虑事件构建、发布和订阅、事件数据持久化、消息中间件,甚至事件数据持久化时还可能需要考虑引入分布式事务机制等。

DDD 分层架构

DDD 分层架构形式

  1. 用户接口层:用户界面、Web 接口等等,Facade 设计模式,适配前端接口所需变更。
  2. 应用层:主要功能有 (1) 协调自身微服务中多个聚合,完成服务编排和组合 (2) 调用其他微服务的应用服务,完成微服务之间的服务编排和组合 (3) 协调执行顺序、进行结果拼装 (4) 可以进行安全认证、权限校验、事务控制、发送或订阅领域事件等。需要注意的是,复杂业务逻辑不要在应用层实现。
  3. 领域层:实现企业核心业务逻辑。领域层包含聚合根、实体、值对象、领域服务等领域模型中的领域对象。
  4. 基础层:为其它各层提供通用的技术和基础服务,包括第三方工具、驱动、消息中间件、网关、文件、缓存以及数据库等。比较常见的功能还是提供数据库持久化。采用依赖倒置设计,封装基础资源服务,实现应用层、领域层与基础层的解耦,降低外部资源变化对应用的影响。

在实现时,推荐使用严格的 DDD 分层架构。即除了基础层,其他各层只能和相邻层进行交互,不允许跨层交互,否则服务的依赖关系会变得混乱和难以管理。

如何将三层架构转换为 DDD 四层架构

400

微服务与中台

DDD VS 微服务 VS 中台

DDD 是设计思想,中台是业务模型,微服务是实现方式。所以中台和微服务的关注点不一样。

中台的关注重点是领域模型上。领域模型的结果会影响到后续的系统模型、架构模型和代码模型,最终影响到微服务的拆分和项目落地。基于 DDD 子域视角,中台也可以划分为核心中台、通用中台。

微服务的关注重点是分层结构上。此时领域模型已经基本有了,需要的就是将 DDD 四层架构落实。微服务根据大小、所处位置不同,实现有所不同。项目级微服务,一般内含领域模型,只需要按照分层模型实现即可。而对于 BFF(服务于前端的后端,Backend for Frontends) 服务,可能没有领域模型,此时没有领域层,只有用户接口层和应用层。在应用层中完成各个中台微服务的服务组合和编排,可以适配不同前端和渠道的要求。

微服务改造策略

  • 绞杀者策略类似建筑拆迁,完成部分新建筑物后,然后拆除部分旧建筑物。
  • 修缮者策略类似古建筑修复,将存在问题的部分功能重建或者修复后,重新加入到原有的建筑中,保持建筑原貌和功能不变。

中台的意义

09 | 中台:数字转型后到底应该共享什么? 讲述了业务中台、数据中台、前台中台后台、平台等概念,有点高深抽象,暂时先不了解了。课后问题在 Notion 页面。

中台来源于阿里的中台战略(详见《企业 IT 架构转型之道:阿里巴巴中台战略思想与架构实战》钟华编著)。2015 年年底,阿里巴巴集团对外宣布全面启动中台战略,构建符合数字时代的更具创新性、灵活性的“大中台、小前台”组织机制和业务机制。

前台用于对接一些业务,用于更快捷地适应瞬息万变的市场。中台将集合整个集团的运营数据能力、产品技术能力,对各前台业务形成强力支撑。中台的本质其实就是提炼各个业务板块的共同需求,进行业务和系统抽象,形成通用的可复用的业务模型,打造成组件化产品,供前台部门使用。前台要做什么业务,需要什么资源,可以直接找中台,不需要每次都去改动自己的底层。

中台业务建模方式

第一步:按照业务流程(通常适用于核心域)或者功能属性、集合(通常适用于通用域或支撑域),将业务域细分为多个中台。核心中台设计时要考虑核心竞争力,通用中台要站在企业高度考虑共享和复用能力。

第二步:选取中台,根据用例、业务场景或用户旅程完成事件风暴,找出实体、聚合和限界上下文。依次进行领域分解,建立领域模型。

第三步:以主领域模型为基础,扫描其它中台领域模型,检查并确定是否存在重复或者需要重组的领域对象和功能,提炼并重构主领域模型,完成最终的领域模型设计。

第四步:选择其它主领域模型重复第三步,直到所有主领域模型完成比对和重构。

第五步:基于领域模型完成微服务设计,完成系统落地。

如何用 DDD 重构中台业务模型

两种方式,一种自顶向下,一种自底而上。前者先做顶层设计,从最高领域逐级分解为中台,分别建立领域模型,适用于全新的应用系统建设,或旧系统推倒重建的情况。后者则是梳理现状,确定当前领域模型,同时找出类似领域模型,沉淀公共可复用的业务能力,适用于遗留系统业务模型的演进式重构。总结就是,“分域建模型,找准基准域,划定上下文,聚合重归类。”

例如,现在要基于以互联网电商和传统核心应用的几个典型业务域为例来进行自底向上的进行重构中台业务模型。

  • 第一步,先基于现状,确定每个业务领域对应的领域模型
    600|600
  • 第二步,将业务域进行对齐
    600|600
  • 第三步,将业务域进行重构,能合并的合并,需要重组的重组
    600|600
  • 第四步,进行核心中台、通用中台等的分类。

如何进行领域建模

头脑风暴怎么做?

  • 各自罗列领域中所有的领域事件,然后对事件进行整合。
  • 对每一个事件,标注导致该事件的命令,以及命令的发起方角色。该角色可以是用户、第三方服务,甚至可以是定时器触发。
  • 基于事件进行分类,整理出实体、聚合、聚合根以及限界上下文。

头脑风暴需要准备什么?

  • 人员方面。领域侧的领域专家,技术侧的架构师、产品、项目经理、开发测试人员。双方都尽早参与头脑风暴,有利于更早将通用语言确定下来。
  • 材料方面。准备一面白板 + 彩色便签纸。彩色便签纸用于记录命令、实体、领域事件和补充信息,不同类型对应不同的颜色,方便分类。

头脑风暴怎么做(二)?

  • 产品愿景。主要目的是对产品顶层价值的设计,使产品目标用户、核心价值、差异化竞争点等信息达成一致,避免产品偏离方向。

    400|600

  • 业务愿景。从用户视角出发的,根据业务流程或用户旅程,采用用例和场景分析,探索领域中的典型场景,找出领域事件、实体和命令等领域对象,支撑领域建模。事件风暴参与者要尽可能地遍历所有业务细节,充分发表意见,不要遗漏业务要点。

  • 领域建模。根据场景分析过程中产生的领域对象,比如命令、事件等之间关系,找出产生命令的实体,分析实体之间的依赖关系组成聚合,为聚合划定限界上下文,建立领域模型以及模型之间的依赖。

  • 微服务拆分与设计。微服务拆分不仅要关注领域模型,还要考虑服务的粒度、分层、边界划分、依赖关系和集成关系。除了考虑业务职责单一外,我们还需要考虑将敏态与稳态业务的分离、非功能性需求(如弹性伸缩要求、安全性等要求)、团队组织和沟通效率、软件包大小以及技术异构等非业务因素。

DDD 代码模型

代码目录结构

application
│   ├── event			# publish 和 subscribe 子目录
│   └── service			# 封装、编排和组合
domain
│   ├── aggregate00		# 聚合
│   │   ├── entity		# 聚合根、实体、值对象以及工厂模式
│   │   ├── event		# 事件实体以及与事件活动相关的业务逻辑代码
│   │   ├── repository	# 查询或持久化领域对象的代码,通常包括仓储接口和仓储实现方法
│   │   └── service
│   ├── aggregate01
│   │   ├── entity
│   │   ├── event
│   │   ├── repository
│   │   └── service
│   └── aggregate02
infrastructure
│   ├── config		# 主要存放配置相关代码。
│   └── util		# 平台、开发框架、数据库、缓存、文件、总线、网关、第三方类库等
│       ├── api
│       ├── driver
│       ├── eventbus
│       └── mq
interfaces
│   ├── assembler 	# 实现 DTO 与领域对象之间的相互转换和数据交换
│   ├── dto 		# 数据传输的载体,内部不存在任何业务逻辑,与内部对象分隔开
│   └── facade 		# 粗粒度的调用接口

如何将领域对象变为代码实现

  • 分析微服务内有哪些服务?
  • 服务所在的分层?
  • 应用服务由哪些服务组合和编排完成?
  • 领域服务包括哪些实体的业务逻辑?
  • 采用充血模型的实体有哪些属性和方法?
  • 有哪些值对象?
  • 哪个实体是聚合根等?
  • 最后梳理出所有的领域对象和它们之间的依赖关系

400|600

600|600

代码中的数据对象

  • 数据持久化对象 PO(Persistent Object),与数据库结构一一映射,是数据持久化过程中的数据载体。
  • 领域对象 DO(Domain Object),微服务运行时的实体,是核心业务的载体。
  • 数据传输对象 DTO(Data Transfer Object),用于前端与应用层或者微服务之间的数据组装和传输,是应用之间数据传输的载体。
  • 视图对象 VO(View Object),用于封装展示层指定页面或组件的数据。

600|600

DDD 的微服务设计实例

18 | 知识点串讲:基于DDD的微服务设计实例

项目的目标是实现在线请假和考勤管理。功能描述如下:

  • 请假人填写请假单提交审批,根据请假人身份、请假类型和请假天数进行校验,根据审批规则逐级递交上级审批,逐级核批通过则完成审批,否则审批不通过退回申请人。
  • 根据考勤规则,核销请假数据后,对考勤数据进行校验,输出考勤统计。

DDD 总结

DDD 的价值

DDD 包括战略设计和战术设计两部分。

战略设计主要从业务视角出发,建立业务领域模型,划分领域边界,建立通用语言的限界上下文,限界上下文可以作为微服务设计的参考边界。

战术设计则从技术视角出发,侧重于领域模型的技术实现,完成软件开发和落地,包括:聚合根、实体、值对象、领域服务、应用服务和资源库等代码逻辑的设计和实现。

DDD 战略设计会建立领域模型,领域模型可以用于指导微服务的设计和拆分。事件风暴是建立领域模型的主要方法,它是一个从发散到收敛的过程。它通常采用用例分析、场景分析和用户旅程分析,尽可能全面不遗漏地分解业务领域,并梳理领域对象之间的关系,这是一个发散的过程。事件风暴过程会产生很多的实体、命令、事件等领域对象,我们将这些领域对象从不同的维度进行聚类,形成如聚合、限界上下文等边界,建立领域模型,这就是一个收敛的过程。

DDD 的优缺点

  • 通过聚合的方式,若服务发生变更,可以相对容易的将某个聚合拆出来单独作为一个微服务,或者将某个聚合迁移到另一个微服务中。
  • 通过依赖倒置机制,降低了层与层之间的依赖,结构清晰、维护方便。

DDD 使用的误区

并非所有地方都需要 DDD。
首先,DDD 上手成本很高。
其次,在非常简单的系统上,使用贫血模型更容易开发和快速推进。