领域驱动设计 DDD(Domain-Driven Design)软件架构学习笔记

0 阅读11分钟

Eric Evans 的《领域驱动设计》,不是一种具体架构模式,而是一套思维方式,告诉你怎么划分边界、怎么建模。它可以和上面任何架构模式结合使用

背景

在最近的项目开发中,我的项目体量来到了一个更大的层面,且业务需求改动频率也变高了。我也首次被自己写的“屎山”代码绊住脚,修改一个需求,新加一个东西,发现只能用全局变量硬塞。

所以借这个机会深度了解了一下软件架构,以此作记录。

介绍

DDD.png

原文

译文

DDD 是一套​软件设计方法论​,是一套思维方式,内心先有思路,再去学习我认为会好很多,所以先学习 DDD。

核心思想

软件的设计应该由业务领域(Domain)来驱动​,用业务概念来组织代码,而不是用技术概念。

方法论

战略设计(Strategic Design)

战略设计解决架构问题,决定系统怎么拆,更重要。

通用语言(Ubiquitous Language)

定义: 追求开发业务用同一套术语,代码里的命名直接反映业务语言

Note

你有没有遇到过这种情况 — 业务说"把这个款下架",开发理解成把数据库记录删了,但业务的意思是把商品状态改成不可见。问题出在"下架"这个词,双方理解不一样。 通用语言要求团队先对齐术语。比如大家约定:

  • "下架" = 商品状态改为 inactive,不删除数据
  • "删除" = 物理删除记录

就比如说 sku,在电商项目里,sku ID 必须约定好,平台生成的 sku 叫平台 SKU,我们设定的 sku 要叫,在代码里的体现就是前者是 platform_sku 后者是 sku

方法名、类名、模块名、甚至团队开会时嘴里说的词,都应该和代码里一致。它是一整套语言体系,不只是命名规范。

限界上下文(Bounded Context)

定义: 在这个边界里,所有术语和模型都有唯一含义,不同上下文里的同一个名字可以是完全不同的模型。

!!!这是 DDD 战略设计里最核心的概念。

Note

继续用电商业务。"商品"这个词在不同业务场景里含义完全不同:

  • 商品管理 — 商品有标题、描述、图片、分类、上下架状态。运营关心的是怎么编辑、怎么展示。
  • 订单履约 — 商品变成了"订单里的一行",关心的是 SKU、数量、单价、是否发货。标题描述无所谓。
  • 库存管理 — 商品变成了"库存单位",关心的是在库数量、仓库位置、补货阈值。价格无所谓。
  • 财务结算 — 商品变成了"收入项",关心的是成本、售价、利润率、税率。图片无所谓。

可能商品还是商品,本来就有很多很多字段, 不同业务场景里的“商品”本质上就是不同的业务概念,只不过碰巧都叫这个名字。

Bounded Context 的做法就是,让每个业务场景有自己的 Product,有多个 Product 类,只包含自己需要的类。

所以在限界上下文思维的指导下,自然整个项目会先按照业务场景分文件夹,每个业务场景有自己所需的数据(哪怕这些数据都可以归为 Product 类),但是每个业务场景会自己定义一个只包含自己所需的类。

在实践过程中,通过 Application 的调用来分配传入每个类自己所需的东西。

上下文映射(Context Map)

Context Map 本身不是代码,它是一张架构图,画在白板或文档里,帮你理清上下文之间的关系。真正落到代码里的是各种集成方式,其中最常用的就是防腐层。理想状态每个项目可以先花心思做设计,设计图就是这玩意。

描述不同限界上下文之间的联系和通讯的。

  1. 上游/下游(Upstream/Downstream)

数据提供方是上游 消费方是下游

例如,Excel 模板分析、Excel 填写是一对上下游

  1. 防腐层(Anti-Corruption Layer, ACL)

对接外部系统的中间层,将外部信息格式对应到内部系统,让信息格式解耦。

可以单独放个 acl 目录,里面都是做防腐的。

ACL 的职责就是把外部数据结构转成自己上下文的领域模型,比如把 Temu API 返回的 JSON 转成自己定义的 Order 对象

  1. 共享内核(Shared Kernel)

两个上下文共用一小部分模型,尽量少用

子域(Subdomain)

把整个业务领域拆成核心域、支撑域、通用域,决定资源投入的优先级

避免过度设计带来的开销,先做优先级,判断资源倾斜。

  1. 核心域(Core Domain)

例如我们的印花生成、Excel 生成,值得精心设计做更好的架构,且需要经常维护更新。

  1. 支撑域(Supporting Subdomain)

能用就行。

  1. 通用域(Generic Subdomain)

用谁的都行,目的是有这个。 第三方,比如说什么支付功能。

战术设计(Tactical Design)

实体(Entity)

有唯一 ID、有生命周期的业务对象(就像一个衣服工厂里的一个订单,从下单开始创建,跟着业务一直到发货,收到款,订单结束)

值对象(Value Object)

没有 ID、只关心属性值的对象(​可能是用来封装一系列数据的对象,例如描述订单里的商品,可能这个商品对象就包含了数量、颜色等等​,商品需要独立追踪,所以应该是 Entity),例如地址(Adress),信息本身比较复杂,可能会需要校验等,适合封装成值对象。

# 这不是 Value Object,这只是一个普通数据容器 
data = {"province": "广东", "city": "深圳", "detail": "南山区xxx"} 

# 这是 Value Object 
class Address: 
 def __init__(self, province: str, city: str, detail: str): 
 
  self.province = province 
  self.city = city 
  self.detail = detail 
 def __eq__(self, other): 
  """相等性通过属性值判断,不是通过 ID""" 
  return (
   self.province == other.province 
   and self.city == other.city 
   and self.detail == other.detail) 
 def full_address(self) -> str: 
  """可以有业务方法""" 
  return f"{self.province}{self.city}{self.detail}"

只要是不只是一个普通变量,有含义或者内容丰富了 不需要通过 ID 追踪身份,且有业务含义值得封装(有校验规则、有组合属性、或有业务方法)时,封装成 Value Object。

聚合(Aggregate)

Aggregate 是一组紧密关联的对象(Entity + Value Object)的集合,作为一个整体来维护业务规则的一致性。外部代码不能直接修改聚合内部的对象,必须通过聚合根来操作。

聚合就是一系列对象的集合,例如订单对象和订单项对象,在业务上二者紧密联系,且有唯一入口 订单。

聚合根(Aggregate Root)

聚合根就是聚合的唯一入口,就比如说订单,所有订单里面的订单项、计算订单信息等,都只能通过订单作为唯一入口。

因为聚合根负责在每次操作时保证内部业务规则不被破坏。

仓储(Repository)

聚合的持久化接口,隔离数据库细节

定义: Repository 是聚合根的持久化接口。它让领域层觉得"我只是在从一个集合里存取对象",完全不需要知道背后是数据库、文件、API 还是 Excel。

Repo 代表一个聚合根 Repo 为聚合根提供存取服务,这个聚合根可能有不同来源,不同来源再用子类来区分,核心是业务需求,子类是不同来源或者其他。

关键约束:

  • 一个聚合根对应一个 Repo。
  • repo 要说业务语言,名字和行为对应的是业务,而不是技术

领域服务(Domain Service)

不属于任何单个 Entity 的业务逻辑

定义: 当一段业务逻辑不自然地属于任何一个 Entity 或 Value Object 时,放到 Domain Service 里。

Service 就是处理逻辑,真正写判断做计算的地方,如果能放在 Entity 里就放 Entity 里,如果放 Entity 需要引入别的 Entity,那就单开一个 Domain Service。

领域事件(Domain Event)

领域事件就是这个业务场景发生后,会有一系列需要做事情,而且这一系列事情是动态的,有可能会变化,如果传统的写成 pipeline,那这个地方的代码就需要根据需求经常改。

领域事件的作用就是让这个地方只是发布个状态,或者修改某个状态,让其他依赖这个状态的业务逻辑自己监听并更新。

如果业务逻辑不会经常修改,就不需要领域事件,写死一样用。重点要看业务逻辑是否有需要需改的需求。

工厂(Factory)

封装复杂对象的创建逻辑(专门用于构建“订单”或者“商品”的对象)

为了做出这个聚合需要很多校验或者其他事,多了就专门搞个 Factory。

判断标准: 构造函数超过 5-6 个参数、创建时有校验逻辑、或者需要根据条件组装不同结构 → 考虑 Factory。否则 → 直接构造。

总结

战略设计是业务文件夹,战术设计是对每个业务文件夹里业务实现的思路。

完整项目的开发思路

步骤做什么目的 / 产出
1. 确定业务场景梳理业务,识别不同的业务场景和职责边界划出 Bounded Context,创建顶层业务文件夹
2. 画 Context Map明确各上下文之间谁是上游谁是下游、怎么通信产出架构图,指导上下文之间的集成方式
3. 划分子域判断每个上下文是核心域、支撑域还是通用域决定每个上下文的设计精力投入:核心域全套 DDD,支撑域从简,通用域用第三方
4. 统一通用语言和业务对齐术语,约定每个上下文内的命名确保代码里的类名、方法名直接反映业务语言
5. 识别 Entity 和 Value Object在每个上下文内分析:哪些对象需要 ID 追踪,哪些只关心值产出 models 目录,明确哪些是 Entity、哪些是 Value Object
6. 划定聚合边界找出哪些对象必须一起保持一致性,确定聚合根明确每个聚合的入口和内部结构
7. 定义 Repository 接口为每个聚合根定义持久化接口(ABC)产出 repositories 目录,一个聚合根一个 Repository
8. 确定数据源和防腐层分析每个 Repository 背后的数据来源,外部系统加 ACL产出 infrastructure 的具体实现 + acl 目录
9. 识别 Domain Service找出不属于任何单个 Entity 的跨聚合业务逻辑产出 services 目录
10. 识别 Domain Event找出"发生后需要触发多个可变后续动作"的业务节点产出 events 目录,解耦后续动作
11. 按需添加 Factory聚合创建逻辑复杂时才抽 Factory产出 factories 目录(不复杂就不建)
12. 编写 Application 层编排各上下文的协作,不含业务逻辑产出 application 目录,每个用例一个文件

根据开发思路的一个理想规范项目的架构

project/
│
├── shared_kernel/                     # 共享内核:跨上下文共用的极少量基础定义
│   ├── sku_id.py                      # Value Object:SKU ID 的格式定义和校验
│   └── money.py                       # Value Object:金额(数值+币种)
│
│
│   ============ 以下每个目录 = 一个 Bounded Context(限界上下文)============
│   ============ 顶层按业务场景分,不按技术层分(战略设计)============
│
│
├── catalog/                           # 限界上下文:商品管理(核心域 → 完整 DDD 战术设计)
│   ├── models/                        # 领域模型
│   │   ├── product.py                 # Entity:有唯一 ID、有生命周期的商品
│   │   ├── product_category.py        # Value Object:分类信息,无 ID,按值比较
│   │   └── price_rule.py              # Value Object:定价规则
│   ├── aggregates/                    # 聚合:product 是聚合根,外部只能通过它操作内部对象
│   │   └── product_aggregate.py       # Product(聚合根)+ 关联的 Value Object 们
│   ├── services/                      # 领域服务:不属于任何单个 Entity 的业务逻辑
│   │   └── pricing_strategy_service.py
│   ├── events/                        # 领域事件:业务上有意义的事发生了,解耦后续动作
│   │   └── product_listed_event.py    # "商品上架了" → 后续可能要同步到其他渠道
│   ├── factories/                     # 工厂:创建聚合的逻辑复杂时才用
│   │   └── product_factory.py
│   └── repositories/                  # 仓储:聚合根的持久化接口,隔离存储细节
│       └── product_repository.py      # 接口(ABC),不是具体实现
│
│
├── fulfillment/                       # 限界上下文:订单履约(核心域 → 完整 DDD 战术设计)
│   ├── models/
│   │   ├── order.py                   # Entity + 聚合根:订单,唯一入口
│   │   └── order_item.py             # Entity:订单项,只能通过 Order 访问和修改
│   ├── services/
│   │   └── shipping_cost_service.py   # 领域服务:计算运费需要多个聚合的数据
│   ├── events/
│   │   └── order_paid_event.py        # 领域事件:"订单付款了" → 通知仓库、发邮件等
│   ├── acl/                           # 防腐层:把外部系统数据转成自己的领域模型
│   │   └── temu_order_translator.py   # Temu API JSON → 自己的 Order 模型
│   └── repositories/
│       └── order_repository.py        # 接口(ABC)
│
│
├── asset/                             # 限界上下文:素材管理(支撑域 → 简单设计够用就行)
│   ├── models/
│   │   └── product_asset.py           # 这里的"商品"只有 sku_id + 图片 URL
│   ├── services/                      #   和 catalog 里的 Product 是完全不同的类
│   │   └── image_download_service.py  #   同名不同义 = Bounded Context 的体现
│   └── repositories/
│       └── asset_repository.py
│
│
├── reporting/                         # 限界上下文:报表(支撑域 → 不需要完整 DDD)
│   ├── excel_generator.py             # 直接写,不需要 Entity/Aggregate
│   └── report_config.py
│
│
├── infrastructure/                    # 基础设施层:所有接口的具体实现
│   ├── persistence/
│   │   ├── sqlite_product_repository.py    # 实现 catalog 的 ProductRepository
│   │   └── sqlite_order_repository.py      # 实现 fulfillment 的 OrderRepository
│   ├── external_api/
│   │   └── temu_api_client.py              # Temu HTTP 调用,纯技术
│   └── storage/
│       └── r2_asset_repository.py          # 实现 asset 的 AssetRepository(Cloudflare R2)
│
│
└── application/                       # 应用层:编排各上下文,不含业务逻辑
    ├── create_order_use_case.py       # 用例:协调 fulfillment + catalog
    ├── export_excel_use_case.py       # 用例:协调 catalog + reporting
    └── sync_images_use_case.py        # 用例:协调 asset + external API