Coding

领域驱动设计

DDD 是什么 传统的 MVC MVC 是模型(Model)、视图(View)、控制器(Controller)的简写,其核心思想是通过将业务逻辑、数据、显示分离来组织代码,其中 Model 是数据库模型,View 层负责视图展示,而业务

16 min read

DDD 是什么

传统的 MVC

MVC 是模型(Model)、视图(View)、控制器(Controller)的简写,其核心思想是通过将业务逻辑、数据、显示分离来组织代码,其中 Model 是数据库模型,View 层负责视图展示,而业务逻辑在 Controller 中实现(一般由众多 Service 来辅助实现)。

mvc 架构

对于小型,且业务逻辑不复杂的系统,MVC 简单高效,不失为一个合适的设计架构。但是随着业务迭代,逻辑膨胀,MVC会开始力不从心,主要有几个原因:

  1. MVC 是完全面向技术的架构设计,分层的系统设计方便了开发者,但是完全没有考虑业务,一股脑把所有业务逻辑放到了 Service 层
  2. MVC模式天然切割了数据和行为,然后用数据库实现数据,用服务实现行为,容易造成需求的首尾分离
  3. 缺乏明确的边界划分,至少在顶层设计层面没有边界划分的规范要求,更多地是靠技术负责人根据经验进行划分,大规模团队协作容易出现职责不清晰、分工不明确的问题

DDD 定义

DDD 全称 Domain-Driven-Design,领域驱动设计,是一种模型驱动设计的方法,通过领域模型捕捉领域知识,使用领域模型构造更易维护的软件。模型在领域驱动设计中,有三个重要用途:

  1. DDD 通过对业务需求的分析和领域模型设计,划分出领域、子领域、限界上下文,指导了系统的架构设计,直接反映软件实现的结构
  2. DDD 聚焦于领域模型,直接反映业务,和任何技术实现无关,降低了业务和技术的耦合度,以模型为基础形成团队的统一语言,将复杂的问题分而治之,指导团队成员分工协作
  3. 领域模型设计使得模型与业务的真实世界保持一致,促使业务知识通过模型得以传递和沉淀

DDD在构建复杂业务的软件模型上,有天然的优势,但对于逻辑简单的业务和产品,或者非业务形态的应用,如内部 OA、BigData 等场景,DDD 并非最佳选择。

ddd-全览

整体流程

首先第一步,根据业务诉求,提炼出整体的业务流程,同时拆解出里面的关键事件,角色,参与者等核心实例。整个拆解和梳理的方法论,目前业界有一些比较成熟的,比如事件风暴,四色建模法等。

提炼完整个业务流程后,进入战略设计阶段,这个阶段主要是从全局和顶层的视角,把整个业务语义转换为结构化分层。通过领域和子域的划分,同时结合通用域、支撑域、限界上下文等设计,分解问题复杂度,也就是前面说到的“分而治之”的思想。

接下来就会到具体的战术设计阶段,通过前面的战略设计阶段,已经把整个领域、边界、上下文等关键模块都梳理完成,现在就是从各个域中再次拆解更细粒度的模块,去指导最终的编码实现,这些更细粒度的模块包括实体、聚合、聚合根等。

最后就到了编码实现阶段,DDD有一个关键价值,叫做“设计即实现”,所以在战术阶段的设计,理论上是可以直接作用于代码的分层结构,如果架构和战术阶段有出入,说明之前的设计有问题,可以复盘重新推演。

ddd 建模

基础概念

在DDD中分为战略设计和战术设计:

  • 战略设计主要面向业务,进行分析拆解,为业务系统的设计打下基础
  • 战术设计更多的是面向技术,在战略设计的基础上,具体设计系统实现
graph LR
    %% 定义节点
    A[DDD]
    B(战略设计
    微服务边界划分)
    C(战术设计
    微服务内部设计)

    %% DDD 分支到战略设计和战术设计
    A --- B
    A --- C

    %% 战略设计
    B --> 领域,子域,限界上下文

    %% 战术设计
    C --> 实体,值对象,聚合,工厂,资源库,领域服务,领域事件

战略设计

战略设计指的是在高层次、宏观上,对整个领域进行分析和规划,确定领域中的概念、业务规则和领域边界等基础性问题。在战略设计中,需要对领域进行全面的了解、分析、拆解,探究业务的规则和本质,并且需要考虑到领域的未来发展趋势和可能的变化。

领域

领域,Domain,是一个团队所要做的业务全集,这是一个面向业务的概念,处在某个业务团队里,你们要做的事就是这个团队的领域。

子域

子域,Sub Domain,是在领域这个整体下,划分出来的业务子领域,每个子领域有各自的业务概念、规则、流程,这些子域互相独立,但又相互关联。

根据在领域中的重要程度,分为:

  • 核心域:决定业务核心竞争力的子域,如业务交易、订单
  • 通用域:具有通用功能,被多个子域使用,如登录、权限
  • 支撑域:支撑其它领域,具有业务特性,但又不通用,如某某业务的基础数据

限界上下文

Bounded Context,是业务边界的划分,可以是一个子域或多个子域的集合,通常划分的依据是:一个限界上下文必须支持一个完整的业务流程,保证这个业务流程所涉及的领域都在一个限界上下文中。例如业务的售前流程,对应的限界上下文要能支持用户完整地浏览、下单。

限界上下文封装了通用语言和领域对象,限界就是领域的边界,而上下文则是语义环境,保证在领域之内的一些术语、业务相关对象等有一个确切的含义,没有二义性。限界上下文一般是微服务拆分的依据,即每个限界上下文对应一个微服务。

一个很形象的隐喻:细胞质所以能够存在,是因为细胞膜限定了什么在细胞内,什么在细胞外,并且确定了什么物质可以通过细胞膜。

领域划分

战术设计

战术设计则是在战略设计的基础上,对领域中的具体问题进行具体的解决方案设计。关注的是领域中的具体情境和场景,需要针对具体的问题进行具体的分析和设计,以满足业务需求。

实体

实体是拥有唯一标识和状态,且具有生命周期的业务对象,通常代表现实世界中的某个概念,可以是名词,也可以是动作。

根据数据和业务逻辑在对象中封装程度的不同,实体有四种类型:

  1. 失血模型:仅包含属性,连基本的 getter/setter 都没有,或者完全由框架动态生成,所有操作都通过外部工具类、DAO 或反射完成,如某些 ORM 的代理对象
  2. 贫血模型:数据和行为分离,模型仅包含属性和简单的 getter/setter,几乎没有业务逻辑,业务逻辑都通过领域服务(Service)来完成,如 Java 中的 POJO
  3. 充血模型:领域对象不仅包含数据,还封装了与自身相关的业务逻辑和行为,能主动响应业务操作
  4. 胀血模型:领域对象承担了过多职责,不仅包含自身业务逻辑,还混入不属于它自身的业务逻辑、跨领域逻辑,甚至基础设施逻辑。

看了几篇文章,各自对失血、贫血、充血的定义都不太一样。失血模型一般只有和底层 DB 进行映射才会使用,而涨血模型是一种 anti-pattern,会导致对象臃肿、难以测试、违反单一职责原则 (SRP)。DDD 推荐使用充血模型,个人觉得实际开发应该用贫血模型+领域服务来协同构建业务逻辑,简单行为放到实体模型中,涉及到持久层以及复杂的行为逻辑都抽到领域服务中。

值对象

通过对象属性值来识别的对象,它将一个或多个相关属性组合为一个概念整体,对实体的状态和特征进行描述,实体可以聚合多个单一属性的值对象,也可以引用一个多属性聚合的值对象。

值对象没有唯一标识(和实体的核心区别),没有生命周期,不可修改,当值对象发生改变时只能替换,如果值对象的所有属性都相同,那么就认为是同一个值对象。典型的例如字符串、整型、枚举等。

通常,领域模型的实体,和数据模型的表是一一对应的,实体引用值对象可以减少表的数量,降低数据建模的复杂度,但无法支持基于值对象的检索,而且如果实体引用的值对象过多,会导致实体聚合了一堆缺乏概念完整性的属性,导致值对象失去业务涵义。因此,一堆属性到底聚合成实体,还是值对象,需要考虑实际的业务场景。 如果这个领域对象在其它聚合内维护生命周期,且在它依附的实体对象中只允许整体替换,我们就可以将它设计为值对象。如果这个对象是多条且需要基于它做查询统计,建议将它设计为实体。

聚合

聚合是一组领域对象的集合,作为一个整体,可以看作是一个修改数据的单元,通常包含一个或多个实体和值对象。聚合内的所有对象要么全部成功保存,要么全部不保存,以此来保证数据的一致性。

聚合根,是聚合中的一个特定实体,它是外部对象唯一可以持有的引用。聚合外部的对象只能通过聚合根来访问或修改聚合内部的状态和对象。

聚合根可以理解成聚合根是若干对象的管理器,统筹这些对象所反映的业务实体。举个例子,订单通常可以作为一个聚合根,它聚合了下单的用户、订单价格、订单内容等实体,订单决定了整个聚合的状态和行为。聚合根还是这个聚合对外的接口人,是外部对象与聚合交互的唯一途径,外部只能通过聚合内部唯一的 id 如订单号来操作订单。

聚合

聚合之间的边界是松耦合的。按照这种方式设计出来的微服务很自然就是“高内聚、低耦合”的。识别聚合,可以通过下面两点来判断:

  1. 对象是否有独立存在的意义,不依赖其它对象的存在才有意义;
  2. 可以被独立访问;

聚合的一些通用设计原则:

  1. 在一致性边界内建模真正的不变条件 聚合用来封装真正的不变性,而不是简单地将对象组合在一起。聚合内有一套不变的业务规则,各实体和值对象按照统一的业务规则运行,实现对象数据的一致性,边界之外的任何东西都与该聚合无关,这就是聚合能实现业务高内聚的原因。
  2. 设计小聚合 如果聚合设计得过大,聚合会因为包含过多的实体,导致实体之间的管理过于复杂,高频操作时会出现并发冲突或者数据库锁,最终导致系统可用性变差。而小聚合设计则可以降低由于业务过大导致聚合重构的可能性,让领域模型更能适应业务的变化。
  3. 通过唯一标识引用其它聚合 聚合之间是通过关联外部聚合根ID的方式引用,而不是直接对象引用的方式。外部聚合的对象放在聚合边界内管理,容易导致聚合的边界不清晰,也会增加聚合之间的耦合度。
  4. 在边界之外使用最终一致性 聚合内数据强一致性,而聚合之间数据最终一致性。在一次事务中,最多只能更改一个聚合的状态。如果一次业务操作涉及多个聚合状态的更改,应采用领域事件的方式异步修改相关的聚合,实现聚合之间的解耦(相关内容我会在领域事件部分详解)。
  5. 通过应用层实现跨聚合的服务调用 为实现微服务内聚合之间的解耦,以及未来以聚合为单位的微服务组合和拆分,应避免跨聚合的领域服务调用和跨聚合的数据库表关联。

领域、子域、限界上下文、聚合都是用来表示一个业务范围,领域、子域、限界上下文属于战略设计,而聚合属于战术设计,聚合的范围是小于前三者的。

领域服务

有些领域中的动作看上去并不属于任何对象。它们代表了领域中的一个重要的行为,不能忽略它们或者简单地把它们合并到某个实体或者值对象中。当这样的行为从领域中被识别出来时,推荐的实践方式是将它声明成一个服务,这个服务就是领域服务。通常,跨多个实体的业务逻辑通过领域服务来实现,跨多个聚合的业务逻辑通过应用服务来实现。

领域服务是无状态的,只有行为,它的存在是为了协调领域对象共同完成某个操作,换句话说,领域对象聚焦个体,领域服务聚焦整体。与此同时,领域服务还可以避免领域逻辑泄露到应用层。例如业务系统常用的各种 Service 就是领域服务的实践。

领域事件

领域事件是发生在领域中且值得注意的事件,通常意味着领域对象状态的改变,在系统中起到了传递消息、触发其他动作的作用。

领域事件驱动设计可以切断领域模型之间的强依赖关系,事件发布完成后,发布方不必关心后续订阅方事件处理是否成功,这样可以实现领域模型的解耦,维护领域模型的独立性和数据的一致性。在领域模型映射到微服务系统架构时,领域事件可以解耦微服务,微服务之间的数据不必要求强一致性,而是基于事件的最终一致性。

我们往往利用消息队列来传递领域事件,当然也可以采用应用服务直接调用的方式,实现数据和服务的实时访问,弊端就是跨微服务的数据同时变更需要引入分布式事务,以确保数据的一致性。分布式事务机制会影响系统性能,增加微服务之间的耦合,所以还是要尽量避免使用分布式事务。

领域事件

领域事件驱动机制,主要分为以下几个步骤:

  1. 事件构建和发布:事件应包括事件基本属性以及业务属性,发布可以是领域服务,也可以是基于事件表的日志捕获(binlog)
  2. 事件数据持久化:用于系统之间的数据对账和审计
  3. 事件总线 / 消息中间件:
    • 微服务内聚合之间的领域事件一般用事件总线
    • 跨微服务的领域事件大多会用到消息中间件
    • 可以实现解耦、一个发布者多个订阅者,以及最终一致性
  4. 事件接收和处理

工厂和资源库

DDD 还涉及了两种设计模式:

  1. 工厂模式:将创建复杂对象和聚合的职责分配给一个单独的对象,该对象本身并不承担领域模型中的职责,但是依然是领域设计的一部分。
  2. 资源库模式(仓储 Repository):用于封装数据访问逻辑,将底层数据存储进行抽象,提供对数据的持久化和查询。旨在将数据访问细节与领域模型分离,使领域模型更加独立和可测试,例如 MyBatis 的 Mapper。

领域建模

以前,我们构建需求有一些简单便捷的方式:

  1. 用例图:最简单直观的表达了用户与系统的交互。
  2. 用户故事:敏捷开发模式下用的较多,从Who、What和Why三个维度描述了业务需求。
  3. 交互原型:用户操作的页面及其操作流程,其缺点是过于关注用户体验,而忽略了业务底层逻辑。

但这些建模方式过于简单,面对大型的业务需求有点力不从心。

事件风暴建模

事件建模法是一种元方法,底层逻辑是通过寻找事件,以及事件背后的领域概念,来完成对领域概念的挖掘和建模。

事件风暴(Event Storming)是事件建模的一种实践方式,是一种轻量级、协作式的领域建模方法,最初由意大利软件顾问 Alberto Brandolini 在2013年提出。它主要用于快速探索和理解复杂业务领域,特别适用于领域驱动设计的上下文映射和模型构建。其核心目标是:

  • 快速发现业务流程中的关键元素。
  • 促进开发人员与领域专家之间的高效沟通。
  • 可视化整个业务流程,识别问题和改进点。
  • 为后续的软件系统设计(尤其是微服务架构)提供清晰的输入。

基本元素

事件风暴建模通常用不同颜色的便利贴表示(颜色不重要,只要团队内统一即可):

元素 颜色(常见约定) 含义
领域事件(Domain Event) 橙色 系统中已经发生的重要事实,用过去时描述,如“订单已创建”、“支付已成功”。
命令(Command) 蓝色 触发领域事件的动作,通常由某个角色或系统发出,如“提交订单”、“发起支付”。
热点问题(HotSpot) 紫色 业务痛点,瓶颈,模糊点
角色/参与者(Actor) 黄色 执行命令的人或系统,如“客户”、“支付网关”。
外部系统(External System) 浅粉色 与当前系统交互的第三方服务。
策略/规则(Policy) 粉色 自动响应事件的业务规则,可触发新命令。
读模型(Read Model) 绿色 用以支撑决策的信息。通常与界面布局有关。

事件风暴建模

活动流程(头脑风暴)

  1. 召集跨职能团队:包括开发人员、测试、产品经理、领域专家等。
  2. 从领域事件开始:大家一起在墙上贴出业务过程中所有重要的“已发生事件”(橙色)。
  3. 反向推导命令:问“是什么导致了这个事件?” → 贴出命令(蓝色)。
  4. 标识谁发出命令:添加角色(黄色)或外部系统(浅色)。
  5. 划定聚合边界:确定哪些命令和事件属于同一个聚合
  6. 识别策略和读模型:补充自动行为和查询需求。
  7. 梳理流程、发现瓶颈或不一致:讨论并优化业务逻辑。

特点

优点:

  • 低技术门槛:只需便利贴和一面墙,无需工具。
  • 高度协作:打破技术与业务之间的隔阂。
  • 快速反馈:几小时内就能获得对复杂领域的初步共识模型。
  • 支持DDD落地:自然引出限界上下文(Bounded Context)、聚合等概念。

问题:

  • 模式偏重,需要不同角色的成员集体参与,涉及的人员多、流程长
  • 发散阶段,所有参与者可以天马行空,但在产生有效信息的同时,也会产生大量的噪音,需要主持人收敛逻辑,因此事件风暴法极度依赖主持人的经验与判断,最终结果自然就会存在一定的随意性

应用场景

  • 新项目启动前的业务探索。
  • 现有系统重构或微服务拆分。
  • 梳理混乱的遗留业务流程。
  • 敏捷团队的需求分析与用户故事拆分。

四色建模法

四色建模法(Four-Color Modeling)是一种轻量级、面向对象的业务建模方法,由 Peter Coad、Eric Lefebvre 和 Jeff De Luca 在 1990 年代提出。它通过四种“原型”来描述现实世界的业务系统,帮助开发人员快速识别核心业务概念、建立清晰的对象模型。

用一句话来概括四色原型就是:一个什么什么样的人或组织或物品,以某种角色在某个时刻或某段时间内参与某个活动。 其中“什么什么样的”就是描述 DESC,“人或组织或物品”就是PPT,“角色”就是Role,而”某个时刻或某段时间内的某个活动"就是MI。

四种原型(颜色与含义)

颜色 原型名称 核心作用 典型例子
红色 关键时刻(Moment-Interval) 表示在时间轴上发生或持续的活动、事件或过程,具有业务意义。通常是系统要“记录”或“追踪”的核心。 订单、预约、销售、航班、会议、支付
黄色 角色(Role) 事件参与方在事件中扮演的角色 客户(在订单中)、员工(在排班中)、供应商(在采购中)
绿色 参与方/地点/事物(Party-Place-Thing,PPT) 系统中的基本实体,是角色的“载体”。它们本身不直接参与业务流程,但通过“扮演角色”参与。 张三(人)、北京仓库(地点)、iPhone 手机(物品)
蓝色 描述/规格(Description) 对上述对象的描述性信息 商品品类、产品型号、服务套餐、职位类型

事件风暴建模

四色之间的关系(建模规则)

  1. PPT → 扮演 → Role
    • 例如:“张三”(PPT)在“订单#123”中扮演“客户”(Role)。
  2. Role → 参与 → Moment-Interval
    • 例如:“客户”(Role)参与了“下单”这个 Moment-Interval。
  3. Description → 描述 → PPT 或 Moment-Interval
    • 例如:“iPhone 15 Pro”(Description)描述了多个具体的手机设备(PPT);
      “标准配送服务”(Description)描述了多个配送事件(Moment-Interval)。

四色建模 vs 事件风暴

维度 四色建模 事件风暴
起源 1990s,面向对象分析(OOA) 2013,领域驱动设计(DDD)
核心焦点 静态对象模型(谁、什么、角色、事件) 动态业务流程(事件流、命令、聚合)
输出形式 类图、对象关系 时间线上的事件流 + 聚合边界
适用阶段 需求早期、概念建模 领域探索、微服务划分
是否强调时间 是(Moment-Interval 本质是时间相关) 是(事件是过去发生的)
协作方式 分析师主导,可协作 高度协作式工作坊

💡 两者可以互补:先用事件风暴梳理流程,再用四色建模提炼对象结构。

特点

优点:

  • 简单直观,非技术人员也能理解。
  • 避免过早陷入技术细节,聚焦业务本质。
  • 有效防止“贫血模型”(只有属性没有行为)。
  • 支持高内聚、低耦合的对象设计。

注意点:

  • 不是所有系统都严格符合四色,但大多数业务系统能映射到这四类。
  • 它是一种分析模式,不是数据库设计规范,后续仍需转换为具体实现。
  • 在 DDD 中,Moment-Interval 常对应聚合根,尤其是那些有生命周期的业务过程。

系统设计

系统架构

DDD 推荐的系统架构是优化后的四层架构,从上到下依次是:

  1. 用户接口层:负责项目启动配置和业务入口,包括接口协议适配、基础参数校验、请求转发、响应结果封装、异常处理等工作(RPC、HTTP、定时任务、MQ 消费者等)
  2. 应用层:协调多个聚合的服务和领域对象,实现流程编排、聚合查询等工作流。应用层是很薄的一层,理论上不应该有业务规则或逻辑,主要面向用例和流程相关的操作。
  3. 领域层:实现核心业务逻辑,通常抽象出领域对象,体现领域模型的业务能力,表达业务概念、业务状态和业务规则。
  4. 基础层:贯穿所有层,作用是为其它各层提供通用的技术和基础服务,包括数据库CRUD、第三方工具、消息中间件、外部网关、文件、缓存以及数据库等

分层架构

代码结构

Facade服务:位于用户接口层,包括接口和实现两部分。用于处理用户发送的Restful请求和解析用户输入的配置文件等,并将数据传递给应用层。或者在获取到应用层数据后,将DO组装成DTO,将数据传输到前端应用。

应用服务:位于应用层。用来表述应用和用户行为,负责服务的组合、编排和转发,负责处理业务用例的执行顺序以及结果拼装,对外提供粗粒度的服务。

领域服务:位于领域层。领域服务封装核心的业务逻辑,实现需要多个实体协作的核心领域逻辑。它对多个实体或方法的业务逻辑进行组合或编排,或者在严格分层架构中对实体方法进行封装,以领域服务的方式供应用层调用。

基础服务:位于基础层。提供基础资源服务(比如数据库、缓存等),实现各层的解耦,降低外部资源变化对业务应用逻辑的影响。基础服务主要为仓储服务,通过依赖倒置提供基础资源服务。领域服务和应用服务都可以调用仓储服务接口,通过仓储服务实现数据持久化。

代码结构

微前端

微前端与微服务一样,都是希望将单体应用,按照领域模型和微服务边界,将前端页面进行拆分,并重组为多个可以独立开发、独立测试、独立部署和独立运维,松耦合的微前端或者微服务。以适应业务快速变化及分布式多团队并行开发的要求。

微前端

边界问题

逻辑边界:微服务内聚合之间的边界是逻辑边界。它是一个虚拟的边界,强调业务的内聚,可根据需要变成物理边界,也就是说聚合也可以独立为微服务。

物理边界:微服务之间的边界是物理边界。它强调微服务部署和运行的隔离,关注微服务的服务调用、容错和运行等。

代码边界:不同层或者聚合之间代码目录的边界是代码边界。它强调的是代码之间的隔离,方便架构演进时代码的重组。

流程回顾

首先第一步,根据业务诉求,提炼出整体的业务流程,同时拆解出里面的关键事件,角色,参与者等核心实例。整个拆解和梳理的方法论,目前业界有一些比较成熟的,比如事件风暴,四色建模法等。

提炼完整个业务流程后,进入战略设计阶段,这个阶段主要是从全局和顶层的视角,把整个业务语义转换为结构化分层。通过领域和子域的划分,同时结合通用域、支撑域、限界上下文等设计,分解问题复杂度,也就是前面说到的“分而治之”的思想。

接下来就会到具体的战术设计阶段,通过前面的战略设计阶段,已经把整个领域、边界、上下文等关键模块都梳理完成,现在就是从各个域中再次拆解更细粒度的模块,去指导最终的编码实现,这些更细粒度的模块包括实体、聚合、聚合根等。

最后就到了编码实现阶段,DDD有一个关键价值,叫做“设计即实现”,所以在战术阶段的设计,理论上是可以直接作用于代码的分层结构,如果架构和战术阶段有出入,说明之前的设计有问题,可以复盘重新推演。

ddd 建模

总结

DDD 是一种软件架构设计方法,直接面向业务的软件工程指导思想,究其本质,还是为了应对软件的高复杂度,实现高内聚、低耦合的一个工具,而并非银弹。因此在实战中需要结合实际业务理解学习其思想。

参考文档

  1. https://zhuanlan.zhihu.com/p/641295531
  2. https://www.woshipm.com/share/6058576.html
  3. https://tech.meituan.com/2017/12/22/ddd-in-practice.html
  4. https://time.geekbang.org/column/intro/100037301

评论加载中。如果这里长期空白,请检查 giscus.app / GitHub 是否可访问。