type
status
date
slug
summary
tags
category
password

1、领域驱动(DDD)是什么

领域驱动设计(Domain-Driven Design,简称DDD)是一种软件设计方法,把业务逻辑抽象为领域模型,领域模型作为代码实现的依据,通过这种方式把业务逻辑清晰映射到代码实现中,避免技术实现与业务需求脱节。
以 DDD(四层模型)和传统MVC(三层模型)进行对比。

1.1 传统MVC应用架构

传统 MVC 应用架构:通常是围绕数据库表为起点进行整体系统设计,分为三层:
  • 模型(Model)
  • 视图(View)
  • 控制器(Controller)
以电商系统为例,MVC 模型的实现如下:
这种设计结构的缺点:
  • MVC模式仅仅反应了软件层面的架构,它不包含业务语言,无法使用该设计直接和业务对话。
  • 面向数据库编程,数据库模型只是数据库映射,服务层围绕数据库完成业务逻辑,造成数据和行为天然割裂。
  • 缺乏明确的边界划分,至少在顶层设计层面没有边界划分的规范要求,更多地是靠技术负责人根据经验进行划分,大规模团队协作容易出现职责不清晰、分工不明确。

1.2 DDD应用架构

DDD分层结构中将三层架构中的业务逻辑拆解为应用层、领域层和基础设施层,核心业务逻辑表现下沉到领域层去实现,以业务领域模型为核心建模(面向对象建模)。这种设计更能体现对现实世界的抽象,通常分为四层:
  • 接口层:主要职责是通过用户界面向用户显示数据信息,同时解释用户的命令,并把用户的请求发送到应用层。
  • 应用层:通过调用基础设置和领域层完成数据资源操作及业务流程编排,相当于BS层。
  • 领域层:将业务逻辑高度内聚到领域层,所以领域层是整个系统的核心,它只与实际业务相关,不关心任何技术细节,尽可能做到与持久化无关。
  • 基础设施层:包含了任何类型的框架、数据库访问代码或者公共的方法等,纯技术的一层。
在 DDD 结构里面,MVC 结构的 Controller + View 的功能由接口层来承担 ,Service 层的业务逻辑则分别拆分到应用层、领域层和基础设施层。
传统Service中的逻辑
DDD中应归属的层
说明
业务逻辑(核心规则)
领域层
转移到聚合根、领域服务或领域对象的方法中
事务管理
应用层
协调领域对象和基础设施的操作
跨聚合协调
领域服务(Domain Service)
保持领域纯度,无状态
技术实现(如邮件发送)
基础设施层
通过接口由领域层调用
DTO转换
应用层
通常放在应用服务中
转变后的代码结构如下所示:
这种设计结构的特点:
  • 统一语言:团队(业务方、产品、设计、技术等)在一个限定的业务上下文形成统一的业务概念。统一语言用于需求文档、PRD文档、系分文档、代码以及日常沟通中,统一的概念和术语可以极大地提升沟通效率和工作效率。
  • 面向业务建模:领域模型和数据模型分离,业务复杂度和技术复杂度分离。DDD聚焦于领域模型,将技术实现细节从模型中剥离出来,能够更好地降低业务和技术的耦合度。
  • 边界清晰的设计方法:通过对需求的识别及分类,划分出领域、子域和限界上下文,进而指导团队成员分工协作,从而做到将复杂的问题分而治之地解决。
  • 业务领域的知识沉淀:通过模型与软件实现关联,统一语言与模型关联,反复论证和提炼模型,使得模型与业务的真实世界保持一致,从而促使业务知识通过模型得以传递和沉淀。
DDD的优点:
  • 业务与代码对齐:减少需求误解,提升软件可维护性。
  • 灵活性:通过清晰的边界,更容易应对业务变化。
  • 可测试性:领域模型独立于基础设施,便于单元测试。
DDD的缺点:
  • 学习曲线陡峭,需要团队对业务有深入理解。
  • 过度设计风险,简单场景可能无需DDD。
  • 需要领域专家持续参与。
DDD的适用场景:
  • 业务逻辑复杂,需要长期迭代的系统(如金融、电商、ERP)。
  • 需要跨团队协作,且业务领域存在多个子域(Subdomain)。
  • 传统架构(如MVC)导致业务代码混乱,难以维护时。

2、DDD的核心概念

  1. 限界上下文(Bounded Context):定义模型的边界。例如,电商系统中“订单”在物流和支付上下文中可能具有不同的含义和属性。如果业务拆成多个子域,通常一个子域对应一个限界上下文。
  1. 统一语言(Ubiquitous Language):在相同的限界上下文内,业务人员和开发团队使用一致的术语,避免沟通歧义。例如,“订单”在电商系统中需明确定义其属性和行为。
  1. 领域模型(Domain Model):包含领域对象、属性、关系、行为、边界范围等各个方面,用于描述业务的本质,是领域驱动模型的核心产出。
      • 实体(Entity):具有唯一标识的对象(如用户、订单)。
      • 值对象(Value Object):不可变对象,通过属性定义的对象(如地址、金额)。
      • 聚合根(Aggregate Root):一组相关对象的入口,保证业务一致性(如“订单”聚合根包含订单项)。
      • 领域服务(Domain Service):处理无状态业务逻辑(如转账服务)。
      • 领域事件(Domain Event):记录业务状态变化(如“订单已支付”)。
      • 仓储(Repository):封装数据访问逻辑,提供聚合根的持久化。

限界上下文(Bounded Context)

限界上下文是指将一个复杂领域划分为多个明确的边界,每个边界内有一套独立的领域模型(代码和术语)。不同上下文中的同名概念可能含义不同。
限界上下文的特点:
  • 语义边界:同一术语在不同上下文中可能有不同含义(例如电商中的“订单”在物流上下文中可能仅关注配送,而在支付上下文中关注金额)。
  • 自治性:每个上下文内的模型、代码和数据库可独立设计,无需与其他上下文强一致。
  • 高内聚性:每一个上下文内部紧密组织,职责明确,具有较高的内聚性。
  • 明确关系:通过上下文映射(Context Mapping)定义不同上下文间的交互方式(如合作、共享内核等)。
限界上下文示例:在实际工程中,通常用一个模块或微服务来表示一个领域的限界上下文。电商系统可能的上下文:
  • 产品目录上下文:管理商品信息。
  • 库存上下文:跟踪库存数量。
  • 订单上下文:处理订单生命周期。
  • 物流上下文:处理配送逻辑。
虽然都涉及“产品”,但:
  • 产品目录中的“产品”包含描述、图片;
  • 库存中的“产品”仅关注SKU和数量。

子域(Subdomain)

子域代表业务领域中的一个逻辑划分,DDD 中将复杂业务领域划分为多个子域,每个子域代表业务的一个特定方面或能力,可分为核心子域、支撑子域和通用子域。
限界上下文和子域的区别和联系
  • 子域:代表业务领域中的一个逻辑划分,DDD 中将复杂业务领域划分为多个子域,每个子域代表业务的一个特定方面或能力,可分为核心子域、支撑子域和通用子域。
  • 界定上下文:是解决方案空间的实现边界,属于技术上的划分。
两者的关系:
  • 一般情况下,一个核心子域会对应一个限界上下文
  • 一个限界上下文可能服务于多个子域(特别是支撑子域或通用子域)。
  • 复杂子域可能会对应多个限界上下文,例如电商系统中,“订单”子域可能拆分为:
    • 订单处理上下文(处理创建、状态流转)
    • 订单支付上下文(处理支付逻辑)
  • 一个子域对应一个微服务,例如在电子商务系统中:
    • 订单子域 → 订单服务
    • 支付子域 → 支付服务
    • 库存子域 → 库存服务
    • 用户子域 → 用户服务
在 DDD 实践中,通常的步骤是:
  1. 首先识别业务中的子域
  1. 然后为每个重要的子域定义限界上下文
  1. 最后明确限界上下文之间的集成方式

统一语言(Ubiquitous Language)

统一语言是指团队(业务方、产品、设计、技术等)在一个限定的上下文中有意识地形成对事物统一的描述和统一的概念。它的目的是消除沟通障碍,确保业务概念准确无误地转化为软件实现。
以“RabbitAdvisors”商业模式中的这句话“用户可以选择自己感兴趣的专栏进行付费订阅”,进行简单的建模。
notion image
根据这个模型,我们可以形成统一语言:
  • 用户(User)是指所有在“RabbitAdvisors”注册过的人。(来自领域模型概念)
  • 订阅的专栏(Subscription)是指用户付费过的专栏。(来自领域模型概念)
  • 用户可以订阅多个专栏。(来自领域模型逻辑)
  • 订阅。(来自限界上下文)
通过定义与解释,我们使这些词语在其所使用的上下文中没有歧义。再通过这些基础词汇,去描述业务的行为或者规则,慢慢就可以将其确立为跨业务与技术的统一语言了。统一语言是在使用中被确立的。
有了统一语言后,我们就可以很方便的描述需求:
用户(User)可以查阅自己订阅过的专栏(Subscription),也可以查看其中的教学内容。
也可以用来描述测试用例:
当用户(User)已购买过某个专栏(Subscription),那么当他访问这个专栏时,就不需要再为内容付费。
如果构建统一语言呢?事件风暴(Event Storming) 是常见的方法,所有参与者通过贴纸等方式梳理业务流程、领域事件、命令和聚合物,在此过程中自然而然地形成统一的术语和规则。
形成的通用语言需要被显式地记录下来,可以用文档、Wiki或简单的表格。例如下表:
通用语言术语
类型
业务含义与规则
对应代码对象(示例)
所属限界上下文
订单
实体
用户提交的购买请求,包含商品列表、总价、状态等。状态流转:已创建->已付款->已发货
Order
销售上下文
商品已下单
领域事件
当用户成功提交订单时触发,包含订单ID和商品信息
OrderCreatedEvent
销售上下文
订单已付款
领域事件
当支付系统确认款项到位时触发
OrderPaidEvent
销售上下文
下单
命令/动作
用户请求创建一个新订单的操作
PlaceOrderCommand
销售上下文
库存不足
业务规则/异常
当订单中商品数量超过当前库存时,禁止下单
InsufficientStockException
库存上下文
在后续的讨论、设计、编码、测试中,团队必须坚持使用通用语言。遇到歧义或发现新概念时,应及时讨论并修正和丰富通用语言。例如开发一个“在线书店”:
  • 在没有通用语言时,业务人员可能说:“用户把书放进购物车,然后点结算按钮,你就生成一个单子,收钱,然后减少库存。”
  • 构建通用语言后,团队共识的表述是:“用户执行添加商品到购物车命令。当用户提交订单时,系统创建订单(状态为待支付)并发出订单已创建事件。支付网关确认后,系统支付订单,订单状态变为已支付,并发出订单已支付事件。库存上下文监听此事件,减少相应商品的库存。”
注意:
  1. 通用语言存在于限界上下文中:这是理解通用语言和DDD的关键。一个术语(如"产品"或"客户")在不同的限界上下文中可能有不同的含义和属性。限界上下文为通用语言划定了语义边界,确保其内部概念的一致性。
  1. 通用语言是演进的:随着对业务理解的深入和业务本身的发展,通用语言也需要不断地迭代和更新å。
  1. 全员承诺:构建和使用通用语言需要团队所有角色(领域专家、产品、开发、测试等)的共同承诺和努力,否则难以发挥其价值。

实体(Entity)

实体具有唯一身份标识且可变的领域对象,是通过身份标识而非属性来定义的对象。
实体的特点:
  • 唯一标识:每个实体都有一个唯一标识,即使其属性发生变化,这个标识保持不变,那就还是同一个实体
  • 可变性:实体的属性可以随时间改变
  • 生命周期连续性:实体在其整个生命周期中保持身份连续性
  • 业务行为:实体包含业务逻辑和行为,而不仅仅是数据容器
实体实例:
  • Customer(客户):有客户ID作为唯一标识
  • Order(订单):有订单号作为唯一标识
  • Product(产品):有产品SKU作为唯一标识

值对象(Value Object)

值对象没有唯一身份标识且不可变的领域对象(一旦创建就不能改变),是通过其属性(值)而非身份标识来定义的对象,可以与其他值对象进行相等性比较。
值对象的特点:
  • 无标识性:没有唯一标识符,通过属性值来区分
  • 不可变性:创建后状态不能被修改
  • 概念整体性:代表一个完整的概念,即使包含多个属性
  • 可替换性:可以整个替换而不只是修改部分属性
  • 值相等性:当所有属性值相等时,两个值对象被视为相等
值对象示例:
  • 货币:由金额和币种组成,$100和¥100是不同的值对象
  • 地址:包含国家、城市、街道等属性
  • 日期范围:开始日期和结束日期组合
  • 颜色:由RGB值或十六进制代码表示
  • 坐标:经度和纬度组合
值对象与实体(Entity)的区别
  • 实体:通过标识区分,即使属性相同但标识不同就是不同对象。
  • 值对象:通过属性值区分,没有唯一标识,属性相同就是相同对象。
特性
值对象(Value Object)
实体(Entity)
标识
由属性值决定
有唯一标识(ID)
可变性
不可变
可变
生命周期
可随意创建/丢弃
有明确的生命周期管理
相等比较
基于所有属性值
基于标识符
例子
货币、日期、地址
用户、订单、产品

聚合根(Aggregate Root)

聚合根是一个特定聚合的根实体(Root Entity),外部访问聚合内所有对象的唯一入口,负责维护聚合内部的一致性和完整性。
聚合根的特点:
  • 全局标识:聚合根具有全局唯一的标识,聚合内的其他实体可以只有本地标识
  • 访问控制:外部对象只能通过聚合根来访问聚合内的其他对象
  • 事务边界:一个聚合通常被视为一个事务边界,修改一个聚合内的多个对象被视为一个原子操作
  • 一致性维护:聚合根负责确保聚合内的所有对象保持一致状态
聚合根示例:
  • Order(订单)可能是一个聚合根
  • 它包含 OrderItem(订单项)实体和 Address(地址)值对象
  • 外部代码只能通过 Order 来修改其中的 OrderItem 或 Address。

领域服务(Domain Service)

领域服务用于封装领域逻辑(业务规则),用来串联领域模型(例如聚合根)、资源库(持久化操作)和防腐层(调用外部接口)等一系列领域内的对象的行为,为其他上下文提供交互的接口。
领域服务的特点:
  • 无状态性:领域服务本身不持有业务状态(数据),仅提供操作逻辑。状态由传入的领域对象(实体或值对象)承载。
  • 跨对象协作:当一个业务逻辑涉及多个实体/值对象的交互时,领域服务可以协调这些对象完成任务,避免将逻辑分散到某个实体中。
  • 依赖领域术语:领域服务的命名和操作直接反映业务语言(Ubiquitous Language),例如 TransferService(转账服务)、OrderApprovalService(订单审核服务)。
  • 与基础设施服务区分:领域服务专注于业务逻辑,不处理技术细节(如数据库访问、网络调用)。技术实现通常由应用服务基础设施层完成。
领域服务的适用场景:
  • 当业务逻辑不属于单个实体的职责时(例如:跨实体的转账、复杂的校验规则)。
  • 当操作需要访问外部系统(如调用风控服务、生成唯一ID)。
  • 当逻辑无法合理放入实体中(避免污染实体的单一职责)。
领域服务和应用服务的区别
  • 领域服务:封装核心业务逻辑,属于领域层,使用领域对象(实体/值对象)进行操作,例如计算运费、校验订单合规性。
  • 应用服务:协调领域层和基础设施层,不包含包含核心业务逻辑,用于处理非业务逻辑,例如事务管理、权限校验。
业务逻辑是放在实体、聚合根还是领域服务?
  • 当业务逻辑只涉及单个实体/聚合根时,优先放在实体/聚合根。
  • 当业务逻辑涉及多个实体/聚合根或需要外部资源时,放在领域服务。

领域事件(Domain Event)

领域事件用于捕获业务领域中发生的重要事实或状态变化。它表示在业务上下文中已经发生的、对业务有实际意义的事情,通常由领域内的某个动作触发,并可能引发后续反应。
领域事件的特点:
  1. 业务意义:领域事件反映的是业务人员关心的具体事件(如“订单已支付”“库存不足”“用户注册成功”),而非技术细节(如“数据库记录更新”)。
  1. 过去时态:事件名称通常使用过去时(如 OrderCancelledPaymentReceived),强调它已经发生,不可更改。
  1. 轻量级通知:事件本身仅包含必要的数据(如事件ID、发生时间、相关聚合根的ID等),不包含复杂逻辑。
  1. 解耦作用:事件允许不同组件(甚至不同限界上下文)以松耦合的方式对事件作出反应,避免直接依赖。
领域事件的作用:
  • 业务一致性:当核心状态变更时,可能需要触发其他业务逻辑(例如,订单创建后发送通知)。
  • 系统集成:跨微服务或模块间通过事件通信,避免强依赖。
  • 审计与溯源:记录事件可用于重建历史状态(如结合事件溯源模式)。

仓储(Repository)

仓储充当领域模型和存储层之间的抽象层。仓储只提供统一接口,具体实现可以由多种存储技术实现,例如数据库、分布式缓存、本地缓存、文件系统等。
仓储的特点:
  • 只处理聚合根(Aggregate Root),不直接处理聚合内的子对象
  • 仓储可以基于ORM(如Entity Framework)实现,但不暴露ORM特性
  • 可以在仓储中实现缓存逻辑以提高性能
仓储的作用:
  • 保持领域模型的纯净性,不掺杂持久化细节
  • 提供灵活的数据访问策略切换能力
  • 使领域模型更容易测试(可通过Mock仓储)

防腐层(Anti-Corruption Layer)

防腐层是一个隔离层,作为核心领域模型与外部系统之间的转换层用于保护核心领域模型免受外部系统或遗留系统的污染
防腐层的作用:
  1. 将外部系统的模型和概念转换为你的领域模型
  1. 隔离外部系统的变化对核心领域的影响
  1. 防止外部系统的概念和模式渗透到核心领域
典型应用场景:
  • 集成遗留系统
  • 使用第三方服务或API
  • 微服务间的通信
  • 不同限界上下文(Bounded Context)间的交互
防腐层实现方式:
  • 适配器模式(Adapter):将外部系统的接口转换为领域模型理解的接口,处理协议、数据格式的差异
  • 门面模式(Facade):为复杂的外部系统提供简化的接口,隐藏外部系统的复杂性
  • 转换器(Translator):在两种不同模型间进行双向转换,通常包含映射逻辑
  • 防腐服务(Anti-Corruption Service):封装所有与外部系统的交互,实现领域语言与外部语言的转换

3、DDD的分层架构

3.1 经典四层架构

notion image
  1. 用户接口层(Interface Layer):处理用户交互和请求,负责数据的输入/输出展示,包含Web界面、API接口、CLI等,不应包含业务逻辑。
  1. 应用层(Application Layer):协调领域层对象完成业务操作,负责事务管理、安全验证等,本身只是业务逻辑的协调者,不包含业务逻辑的具体实现。
  1. 领域层(Domain Layer):核心业务逻辑的实现,包含领域模型和规则。领域层通常包含以下内容:
      • 实体(Entity)
      • 值对象(Value Object)
      • 聚合根(Aggregate Root)
      • 领域服务(Domain Service)
      • 领域事件(Domain Event)
      • 仓储接口定义(Repository Interface)
      • 依赖的外部服务接口定义(ThirdParty Interface)
  1. 基础设施层(Infrastructure Layer):提供底层技术支持,包含仓储实现、消息传递、外部服务调用等。
经典项目结构如下:
各层通信规则:
  • 上层可以调用下层,但下层不应调用上层
  • 领域层应保持纯净,不依赖其他层
  • 基础设施层实现领域层定义的接口
  • 跨层调用通常通过 DTO 或领域对象
酒店交易系统为例:
  • 用户接口层:处理订单提交的 HTTP 请求,并调用应用层服务。包含以下内容:
    • Controller:接收HTTP请求(如Spring MVC的@RestController)。
    • DTO(Data Transfer Object):定义请求/响应数据结构,例如订单创建的OrderRequestDTO
    • 协议适配:支持RESTful API、WebSocket、MQ消息等不同协议。
  • 应用层:接收应用层的请求,调用领域层的领域对象完成业务逻辑。包含以下内容:
    • 应用服务:如OrderAppService,调用领域层的聚合根和仓储,协调转账、订单创建等流程。
    • 事务管理:控制跨聚合的最终一致性事务(如通过MQ事件)。
    • 权限控制:方法级别的权限校验(如@PreAuthorize)。
  • 领域层:业务逻辑的具体实现,包含各种领域模型。包含以下内容:
    • 聚合根(Aggregate Root):如OrderAggregate,维护订单内部一致性。
    • 领域服务:如TransferService,处理跨实体的业务逻辑(如银行转账)。
    • 值对象(Value Object):如Address,无唯一标识且不可变。
    • 仓储接口:如IOrderRepository,定义持久化操作,实现在基础设施层。
  • 基础设施层:提供底层技术支持,包含以下内容
    • 仓储实现:如OrderRepositoryImpl,使用MyBatis或JPA操作数据库。
    • 防腐层(ACL):适配第三方服务(如支付网关),隔离外部模型。
    • 工具类:日志、缓存、消息队列(如RabbitMQ)的客户端封装。
分层交互示例(以订单创建为例):
  1. 用户接口层:接收CreateOrderCommand,校验格式。
  1. 应用层:调用OrderService,开启事务并协调OrderAggregatePaymentService
  1. 领域层:OrderAggregate验证业务规则(如库存检查),生成OrderCreatedEvent
  1. 基础设施层:持久化订单到数据库,并通过MQ发送事件。

3.2 贫血模型和充血模型

领域层的实现通常分为充血模型贫血模型两种主要模式,它们代表了完全不同的设计哲学。

3.2.1 贫血模型(Anemic Domain Model)

特点
  • 领域对象只包含数据(属性),不包含业务逻辑
  • 业务逻辑放在服务层(Service Layer)中
  • 领域对象主要是简单的getter/setter集合
优点
  • 简单直观,容易理解
  • 与许多ORM框架配合良好
  • 适合简单的CRUD应用
缺点
  • 违反了面向对象的基本封装原则
  • 业务逻辑分散在各处,难以维护
  • 领域对象缺乏行为,只是数据结构
示例代码:

3.2.2 充血模型(Rich Domain Model)

特点
  • 领域对象既包含数据也包含行为
  • 业务逻辑封装在领域对象内部
  • 服务层很薄,主要负责协调领域对象
优点
  • 符合面向对象设计原则
  • 业务逻辑集中,内聚性高
  • 领域模型更能反映真实业务
缺点
  • 设计复杂度较高
  • 需要更深入的领域知识
  • 与某些ORM框架配合可能有挑战
示例代码

3.2.3 如何选择模型

两种领域模型类型没有绝对的优劣之分,可以根据具体应用场景选择合适的模型。
充血模型适用场景
  • 业务逻辑复杂且可能变化
  • 需要长期维护的项目
  • 团队具备良好的OO设计能力
贫血模型适用场景:
  • 业务逻辑简单
  • 主要是CRUD操作
  • 开发周期紧张
  • 团队对OO理解不深
现代 DDD 实践通常推荐尽可能使用充血模型,因为它能更好地应对复杂业务逻辑的变化,但也要根据实际项目情况做出权衡。

4、DDD实战(电商系统

以电商系统中订单模块为例:

4.1 领域层

实体:订单项
值对象:价格
聚合根:订单聚合订单项(实体),商品聚合价格(值对象)
仓储:订单和商品的仓储接口
领域服务:在领域服务中聚合订单和商品两个聚合根的业务逻辑
领域事件:订单发布领域事件
防腐层:调用外部系统

4.2 应用服务层

领域驱动设计在互联网业务开发中的实践ShardingSphere使用总结
Loading...