读书笔记丨《悟道领域驱动设计》 永久链接

读书笔记 · 第 7

读书笔记丨《悟道领域驱动设计》

阅读《悟道领域驱动设计》后的一些笔记和思考。

思维转变

领域驱动设计(Domain-Driven Design,以下简称 DDD)的核心价值在于其对「业务领域」的深度聚焦。这里的「领域」并非单纯的技术范畴,而是指代软件系统所要映射的现实业务场景及其核心价值主张。DDD 通过建立与业务高度契合的领域模型,使得技术实现与业务本质形成同频共振,从而有效解决复杂业务场景下的认知鸿沟问题。

在 VUCA(Volatile 易变性、Uncertain 不确定性、Complex 复杂性、Ambiguous 模糊性)特征愈发显著的现代商业环境中,任何架构设计都面临固有局限。这种局限性既源于业务需求本身的动态演进,也受制于人类认知的有限性——正如 Eric Evans 在开山之作中强调的“模型永远是对现实的近似抽象”。但正是这种局限性,凸显了 DDD 方法论的战略意义:

它通过"战略设计"构建业务全景图,运用限界上下文划定领域边界,通过"战术设计"落地聚合根、实体/值对象等模式,形成应对复杂性的结构化解决方案。

需要特别指出的是,DDD 的复杂性并非方法论本身的缺陷,而是其应对现实业务复杂度的必要代价。这种复杂性体现在三个维度:

  1. 认知复杂性:要求开发团队与领域专家共建"通用语言",实现业务概念与代码模型的精准映射。
  2. 架构复杂性:通过分层架构实现业务逻辑与技术实现的解耦,采用防腐层处理系统集成问题。
  3. 演进复杂性:借助子域划分和上下文映射,为持续演进的业务提供可扩展的架构基础。

对于实践者而言,DDD 的价值不在于提供完美无缺的终极方案,而是为 VUCA 环境下的系统建设提供基础性指引。其核心思想——无论是通过限界上下文实现的领域自治,还是通过聚合根维护的业务一致性——都为控制软件熵增提供了可落地的模式库。即便不完全采用 DDD 完整体系,其领域建模思想、分层架构理念等核心要素,仍能显著提升复杂系统的可维护性和演进能力。这种开放包容的哲学,恰是 DDD 历经二十年仍保持生命力的关键所在。

贫血模型 vs. 充血模型

  • 贫血模型:指的是只有属性而没有行为的模型。
  • 充血模型:指的是既有属性又有行为的模型。

笔者过往的实践中,基本上都使用类似于 controller→service→repository[model] 的三层架构:

  • conrtoller 负责暴露对外接口。
  • service 负责执行所有的业务逻辑。
  • repository 复杂数据的存储和缓存,包含数据对象 model 的定义。

在这个模式下,基本上所有的核心逻辑都充斥在 service 层中,所以 service 层一般都会非常大,它要扮演多面手,即要负责跟各个模块协作,还要负责处理具体的业务规则,最终完成一个业务行为。这个过程中,model 即为贫血模型,因为逻辑都给 service 处理了,这种架构也称为贫血三层架构

在 DDD 的理念下,很多的核心业务概念都会被建模为「领域对象」,这些「领域对象」本身就是一种业务规则的体现,所以把业务的处理逻辑,都归属到这些「领域对象」的行为当中了,即所谓的充血模型。

在这个理念下,一个优化后的充血四层架构如下图所示:

贫血模型推荐场景:业务简单、迭代快速、团队技术栈偏传统(如 Spring Boot+MyBatis)时,避免过度设计。

充血模型推荐场景:业务复杂、需长期演进(如核心交易系统)、团队具备 DDD 经验时,通过实体、值对象、领域服务等战术设计理念降低系统熵增。

混合使用的场景:部分核心领域用充血模型(如订单、支付),非核心模块用贫血模型(如日志、配置),平衡效率与质量。

实际上,充血模型因其状态完整,适合进行状态变更类的操作,以确保业务操作符合领域规则;贫血模型由于其轻量级,更适合作为不会涉及状态变更的操作的数据容器。这其实就是 CQRS 的理念。

概念清单

战术设计

实体

定义:会随着业务变化发生变化的业务概念叫作实体对象。

关键点:实体需要唯一表示

值对象

定义:一些对象在表达业务概念时是必须的,可业务并不围绕着它们进行,它们仅是对这些重要业务概念的描述,这一类对象叫作值对象。

关键点

  1. 值对象的意义取决于属性,只要对象的属性一模一样,那么对象就是相同的。
  2. 尽量把值对象实现为不可变对象。
领域服务

定义:领域服务自身是没有数据的,只是表达了某种业务计算逻辑,或者业务的某种策略。

关键点

  1. 领域服务是无状态的。
  2. 只有在确实表达了一个相对独立的业务概念或者业务策略,并且不能简单地把它归结到某个既有的业务对象上时,才是一个真正的领域服务。
领域事件

定义:领域事件代表从业务专家视角看到的某种重要的事情发生了。

关键点

  1. 领域事件是一种特殊的值对象。
  2. 应该根据限界上下文中的通用语言来命名事件:AccountActivited。
  3. 应该将事件建模成值对象或贫血对象。
聚合

定义:聚合从本质上讲是在基础的构造块上增加了一层边界,用边界把那些紧密相关的对象放到了一起。

关键点

  1. 紧密相关的对象存在数据一致性问题;
  2. 缺乏边界时,维护数据一致性是困难的;
  3. 划分边界的关键在于既不要让整个系统成为一个整体,又让每个单独划分出的聚合具有明确的业务意义;
  4. 聚合需要关注三条法则:
    1. 生命周期一致性:如果一个对象在聚合根消失之后仍然有意义,那么说明此时在系统中必然存在能够访问该对象的方法。这和聚合的定义矛盾,所以聚合内的其他元素必然在聚合根消失后失效。
    2. 问题域一致性:不属于同一个问题域的对象,不应该出现在同一个聚合中。
    3. 尽量小的聚合:聚合的本质作用是提升对象系统的粒度,确保一致性、降低复杂度。不过,粒度绝不是越大越好。如果聚合的粒度太大,那内部的逻辑复杂度也会大大增加还会影响到复用度。因此,要能够比较容易地断开聚合。
资源库

定义:对于查询、创建、修改、删除数据的操作,领域模型使用“资源库(Repository)”这个概念来承载它们。

关键点:一个聚合对应一个资源库,应以聚合根命名资源库,除了聚合根之外的其他对象,都不应该提供资源库对象。

工厂

定义:工厂用于构建聚合。

关键点:一个聚合往往包含多个对象,这些对象的数据之间又可能存在联系,如果允许分别创建这些对象,就会让聚合是业务完整性的单元这个定义面临失败。

战略设计

统一语言

定义:与业务专家协作定义全团队通用的术语表,消除沟通歧义。

关键点

  1. 同一个概念在不同的上下文中可能存在不同的含义;
  2. 同一个概念在同一上下文中的不同环节,也可能存在不同的含义,需要非常明确清晰的界定,降低沟通成本。
子域

定义:子域是对业务领域的逻辑划分,用于分解复杂问题。通常分为核心子域(业务核心竞争力)、支撑子域(辅助核心业务)和通用子域(可复用的标准化能力)。

关键点:因业务目标、团队定位和组织发展阶段等方面的不同,这三个子域的划分并非一成不变,而是会互相转换。

限界上下文

定义:限界上下文本质上是一个自治的小世界,它有完备的职责,还有清晰的边界。

关键点

  1. 一个子域的一切资产,包括领域模型、数据库、包、可执行程序、接口声明等,都应该封装在限界上下文中,避免跨越边界。
  2. 如何平衡边界的价值和不利影响,是划分边界时要做的一种重要取舍。一个较为稳妥的策略是考虑认知的渐进特征,不要过早隔离。在已经确定的边界上进行划分,延缓划分那些尚具模糊性的边界,在这些边界逐渐变得清晰时再分离它们。
上下文映射

定义:限界上下文约定了基于领域模型的架构层次的设计分解,而分解必然意味着集成和协作。上下文映射就是对限界上下文之间的协作关系的模式总结。

关键点

  1. 在边界上完成概念映射是一种基本模式。通过在应用层组装或者使用适配器完成概念映射,可以保持领域概念的清晰,避免领域模型遭到不必要的污染。
  2. 防腐层模式、标准开放服务模式、客户-供应商模式、追随者模式。

串讲

在应对复杂业务系统时,DDD 通过分治策略将业务领域拆分为多个子域(如电商系统的订单、支付子域),每个子域对应一个限界上下文——这是技术与业务对齐的关键边界,既承载领域模型的实现,也通过上下文映射(如防腐层、共享内核等模式)实现跨子域协作,避免模型污染。

限界上下文内的领域对象是业务逻辑的载体:具备唯一标识和生命周期的实体(如订单实体通过 ID 跟踪状态变化)、描述特征且不可变的值对象(如地址由省市构成,修改需整体替换),以及通过聚合根统一操作保证一致性的聚合(如订单聚合根管理订单项和配送信息)。当业务逻辑跨越多个聚合时,由无状态的领域服务协调(如支付计算需整合订单、账户聚合)。

对象的创建与持久化分别由工厂(封装复杂初始化逻辑)和资源库(隔离存储细节)负责,而领域事件(如订单支付成功事件)则驱动跨上下文的异步协作。

战术设计

factory

  • factory 用于构建复杂的领域对象。

repository

  • 只有聚合根有 repository。
  • repository 就只提供 loadsave 功能,且要保证事务一致性。
  • 尽可能提供行级的 repository,而不是表级的 repository,对于表级的 repository,可以抽成一个领域服务。

设计模式

责任链模式

将请求的发送者和接受者解耦,使多个对象都有机会处理请求。

  • 责任链模式的使用要点在于要将维护责任链的代码和业务代码分开。
  • 在 DDD 中使用责任链模式时,应创建一个领域服务,在领域服务中完成责任链的创建和执行。
  • 尽量不要在责任链的处理器中通过 set 修改领域对象(聚合根)的状态,责任链应仅用于某些值的计算,最终将计算结果交给聚合根完成业务操作。

笔者实现了一个快速构建责任链的工具:

https://github.com/hedon954/devkit-go/blob/main/designmode/responsibility/builder.go

策略模式

允许在运行时根据需要选择不同的实现。

  • 在 DDD 中使用策略模式时,通常先定义一个领域服务接口,再在其实现类中完成策略的加载、选择和执行。
  • 注意屏蔽策略模式的实现细节,避免上层关注领域服务内的设计模式细节。

桥接模式

旨在通过解耦抽象和实现,使两者能够独立扩展和变化。

  • 多维解耦机制:桥接模式通过组合/聚合关系替代继承关系,将原本紧密耦合的抽象层(功能定义)与实现层(具体操作)分离例如遥控器(抽象)与电视(实现)的协作,遥控器通过接口控制电视,无需关注具体品牌。
  • 正交扩展能力:支持两个独立变化维度(如消息类型与通知渠道、图形与渲染方式),避免类数量呈指数级增长(M×N 组合问题)。电商物流系统中,新增微信通知渠道时,无需修改所有消息类即可实现扩展。

规约模式

规约模式是一种用于定义业务领域中规则和约束的模式,通常由规约接口(Specification)和验证器(Validator)两个部分组成。

  • 在 DDD 中,规约模式并不是在聚合根进行业务操作之前做前置校验,而是在聚合根完成业务操作之后做后置校验,确保 Repository 保存的聚合根符合业务规则。

适配器模式

被适配者(Adaptee)的接口转换为目标接口(Target),使原本因接口不兼容而无法协同工作的类能够协同。

  • 在 DDD 中,可以使用适配器模式来实现防腐层,以将外部上下文接口(如开放主机服务)返回的模型转换为本地上下文定义的领域模型,并将本地上下文的操作转换为对外部上下文的操作。可以有效隔离外部上下文的领域模型,避免互相污染。

领域事件

幂等性

领域事件的定义

领域事件是领域模型的组成部分,它通常由聚合根产生,并被其他聚合或者限界上下文订阅和处理,触发相应的业务逻辑。

注意点:

  • 应该根据限界上下文中的通用语言来命名事件:AccountActivited。
  • 应该将事件建模成值对象或贫血对象。

应用:

  1. 解耦领域对象之间的关系;
  2. 触发其他领域对象的行为;
  3. 记录领域内已发生的状态变化;
  4. 实现跨聚合的最终一致性;
  5. 进行限界上下文集成。

消息体:

{
  "event_id": "",
  "event_type": "",
  "entity_id": "",
  "event_time": 0,
  "extra_data": "{}"
}

领域事件的生成

  1. 应用层创建领域事件。
  2. 聚合根创建领域事件。

要避免在聚合根内部调用基础实施发布领域事件,而是生成后返回给应用层,由应用层去发布。

type Entity struct {
  Events []Event
}

func(e *Entity)ResgisterEvent(event Event) {
  e.Events = append(e.Events. event)
}

func(e *Entity) GetEvents() []Event {
  res := e.Events()
  e.Events = []Event{}
  return res
}

领域事件的发布

  1. 直接发布并轮询补偿:为事件存储一个发布状态标识,用于记录是否补发成功。并提供定时任务检索超时未发布成功的事件进行重新发布。
  2. 采用事务日志拖尾:引入变更数据捕获组件(Change Data Capture,简称 CDC),捕获数据的变更日志,解析后获得领域事件并发布。

领域事件的订阅

将领域事件订阅者放置在用户接口层 user-interface-subscriber,收到事件后调用应用服务执行业务逻辑。

事件溯源

事件溯源(Event Sourcing)是一种将所有的领域事件(Domain Event)存储到事件存储(Event Store)中,并通过重放历史事件来还原领域对象状态的模式。

核心思想是将系统中所有的状态变更都视为事件,将这些事件以事件顺序记录下来,并存储到事件存储中。这样,可以通过重放这些事件,来还原任意时刻的系统状态。

三种方案:

  1. 通过回放所有的历史事件重建聚合根。
  2. 通过快照提高重建聚合根的效率。
  3. 通过拉链表生成所有事件对应的快照。

拉链表是一种用于处理缓慢变化维度问题的数据结构,它可以有效地处理维度数据的历史变化。在拉链表中,每个记录都有一个开始时间和结束时间,用于描述该记录的存活时间,即该记录的有效期。

CQRS

CQRS 将系统的操作分为两类:

  • 命令(Command):负责数据的写操作(增、删、改),不返回数据。
  • 查询(Query):负责数据的读操作,仅返回结果且不修改数据。

两者的数据模型可独立设计,甚至使用不同的数据库或存储技术。

适用场景

应对高并发读写场景

  • 案例 1:B 站点赞系统

    在日均活跃用户近亿的 B 站,点赞功能通过 CQRS 分离读写操作。写入端通过消息队列(如 Kafka)异步处理请求,避免数据库锁竞争;查询端通过缓存优化读取性能,显著提升系统吞吐量和稳定性。

  • 案例 2:实时答题 PK 游戏

    高并发的答题得分计算场景中,CQRS 结合事件溯源(Event Sourcing)记录每个操作事件,确保读写模型的最终一致性,同时支持复杂战况数据的实时展示。

解决复杂查询需求

  • 案例 3:电商订单查询

    随着订单查询需求多样化(如按时间筛选、跨实体聚合数据),CQRS 通过独立读模型简化查询逻辑,避免领域模型被复杂查询逻辑污染。

  • 案例 4:微服务数据聚合

    在微服务架构中,CQRS 允许通过事件同步跨服务数据到专用读库,避免跨服务联表查询的性能瓶颈(如行程管理服务与用户信息服务的聚合查询)。

提升数据模型灵活性

  • 案例 5:文本增量更新

    针对大型文本编辑场景,CQRS 拆分读写模型,增量保存修改记录并通过事件合并,减少网络传输数据量,同时支持任意版本的历史数据恢复。

不适用场景
  • 简单 CRUD 系统(如小型管理后台)
  • 强一致性要求的金融交易场景(如实时扣款)
  • 团队缺乏事件驱动架构经验时

一致性

聚合内事务实现

  • 聚合内事务控制不要放在应用层,会使应用层承担过多的责任。应用层应专注于协调领域对象和基础设施以完成业务操作,不应过多涉及数据访问和事务控制的细节。
  • 聚合内事务控制可以交给 Repository 来实现,采用乐观锁解决并发问题,可以基于版本号和时间戳,一般重试 1-3 次即可。

聚合间事务实现

  • 聚合间控制可以单独建立一个领域服务 Domain Service 来完成。

  • 对于实时性要求不高,仅需最终一致性,可以使用本地消息表或者最大努力通知的方案。

  • 对于实时性一致性要求比较高,可以采用 TCC(Try-Confirm-Cancel) 事务方案。

  • 对于长事务场景,或者涉及外部系统、遗留系统,可以考虑 Saga 事务方案。

    Saga 将事务分为多个事务,这些分支事务按照一定的顺序执行。当某个分支事务执行成功后,会通过消息通知下一个分支执行;当某个分支事务执行失败时,会按照正常事务执行顺序的相反方向进行一系列的补偿操作,以确保全局事务的一致性。

战略设计

事件风暴

核心概念与元素

元素名称颜色标识说明
领域事件(Domain Event)橙色表示已发生的业务事实,以“动词过去式”命名(如“订单已提交”),是事件风暴的核心起点。
命令(Command)深蓝色触发领域事件的操作或意图(如“提交订单”),通常由用户或系统触发。
参与者(Actor)黄色执行命令的角色,包括用户、部门或外部系统(如“客户”触发支付命令)。
外部系统(External System)粉色与当前系统交互的第三方服务(如支付网关回调生成事件)。
策略(Policy)紫色业务规则或约束条件(如“库存不足时取消订单”),决定事件触发的逻辑。
读模型(Read Model)绿色为查询优化的数据视图(如“用户订单列表”),支持决策展示。
聚合(Aggregate)大黄色业务对象集合(如“订单聚合”包含订单项和状态),维护一致性和完整性。
问题(Question)红色未达成共识的争议点(如事件定义分歧),需后续专项讨论。

实施流程与步骤

  1. 准备工作

    • 参与人员:业务专家、开发、产品、测试等跨职能角色,需领域专家主导。
    • 物料:多色便签、白板、马克笔,线上工具辅助远程协作。
  2. 识别领域事件
    团队通过头脑风暴罗列所有可能事件(如电商场景的“订单已创建”“库存已扣减”),按时间轴排列,争议事件用红色便签标记并暂存。

  3. 补充命令与角色
    为每个事件关联触发命令及执行者(如“客户”执行“支付订单”命令生成“支付完成”事件),区分内部操作与外部系统调用。

  4. 定义策略与读模型
    添加业务规则(如“订单金额 ≥1000 元需审核”)和数据展示需求(如“实时库存看板”)。

  5. 构建聚合与划分子域
    将相关事件、命令归类为聚合(如“支付聚合”),划分限界上下文(如“订单服务”“库存服务”),明确微服务边界。

注意事项

  1. 事件粒度的把控:避免过度细化(如“用户已睁眼")或过于宽泛(如“订单已修改”),需聚焦业务关键节点。

  2. 争议处理与迭代:对未达成共识的事件标记为“问题”(红色便签),后续专题讨论;定期回顾模型,修正错误或补充遗漏。

  3. 技术实现衔接 :事件风暴的输出需转化为代码模型,例如通过事件溯源(Event Sourcing)持久化事件流,或结合 CQRS 分离读写逻辑。

C4 架构模型

https://c4model.com/

层级核心目标受众关键元素
Context(上下文)描述系统与外部实体(用户、第三方系统)的交互关系非技术人员(如业务方、客户)系统边界、用户角色、外部依赖(如支付网关)
Container(容器)展示系统内部的高阶技术组件(进程级单元)技术管理者、架构师Web 应用、数据库、消息队列等独立进程单元,关注技术选型与通信协议(如 REST API、gRPC)
Component(组件)细化容器内部的业务模块与交互逻辑开发团队服务、模块、接口(如订单服务、库存服务),强调职责划分与依赖关系
Code(代码)展示组件实现的代码结构开发者类、方法、数据库表(如 UML 类图、ER 图),通常由 IDE 工具自动生成

除了四层核心视图,C4 模型还提供:

  • 部署图:展示容器在物理环境中的分布(如 Kubernetes 集群部署)。
  • 动态图:描述业务流程(如用户下单到支付完成的时序交互)。
  • 系统景观图:多系统协同的全局视图(如企业级中台架构)。

实践案例

参考作者的 ddd-archetype ,笔者实现了一个 Go 版本的 ddd-archetype-go

https://github.com/hedon-go-road/ddd-archetype-go

整体架构如下:

来自开放网络的回应

引用、喜欢、转发和站外回复会通过 Webmention 回到这里。

接收与展示代码已经就绪,注册正式域名后即可开始收集回应。

评论

评论由 GitHub Discussions 保存和管理。