《领域驱动设计·精简版》详解:零基础读懂 DDD 的核心概念
- 约 18 分钟读完 - 全文 6850 字《领域驱动设计·精简版》详解:零基础读懂 DDD 的核心概念
DDD 要回答的问题是:
软件是为了解决现实业务问题而存在的,可为什么代码写着写着,就和业务彻底脱节了?
Eric Evans 的答案是:问题出在"模型"上。业务在领域专家脑子里,代码在程序员手里,中间隔着层层"翻译",每翻译一次就失真一次。DDD 的全部内容,就是围绕"如何建立一个好模型、并让模型直接活在代码里"展开的。
本文面向从未接触过 DDD 的读者,按全书结构逐章展开,配合图解和实例,帮你建立完整的知识地图。
第 1 章 简介:软件为什么会偏离领域
领域与领域模型
**领域(Domain)**就是软件要解决的那个现实问题。做银行系统,金融业务就是领域;做空中交通监控,航线和飞行就是领域。
书里用了一个类比:造汽车。工人们各自负责某个零件,但如果没人理解整车,造出来的只是一堆零件的集合体,不是一辆好车。软件也一样——你不能在对金融业务一无所知的情况下写出好的银行软件。谁懂业务?不是架构师,不是分析师,是领域专家(银行的业务人员、空管的调度员)。
但领域知识是海量而杂乱的,不可能全部塞进软件。我们需要从中建立抽象——即领域模型(Domain Model):
模型不是一张图,也不是专家脑子里的全部知识,而是"经过严格组织并进行选择性抽象的知识"。
建模意味着取舍。银行软件会记录客户的住址,但绝不会记录客户眼睛的颜色——前者是业务需要的,后者不是。选择保留什么、放弃什么,正是建模的核心工作。
书里的案例:空中交通监控系统
全书反复使用同一个例子:空中交通监控系统。飞机(Aircraft)按计划航线(Route)飞行,航线由一系列定位点(Fix)组成,每个定位点是一个三维坐标(3DPoint:经度、纬度、海拔)。系统接收雷达数据,计算飞机的真实飞行轨道,发现两架飞机航线可能交叉时向管制员报警。
最初的设计里,Route 直接关联 3DPoint,后来团队发现:地面屏幕上显示的航线其实只需要二维投影,于是模型被重构为 Route 关联 2DPoint——模型随理解的加深而演进,这正是 DDD 强调的动态过程。
第 2 章 通用语言(Ubiquitous Language)
翻译是万恶之源
开发人员和领域专家说着两种语言:
- 开发人员满脑子是类、方法、继承、多态、框架;
- 领域专家满口是飞机、航线、海拔、偏航告警。
于是每次沟通都在"翻译":开发人员把业务术语翻译成技术概念去写代码,再把技术实现翻译回业务语言去汇报。每一次翻译都丢失信息,错误理解就这样悄悄积累。更糟的是,很多时候团队连"翻译错了"都发现不了,直到软件上线才爆发。
用模型当语言的骨架
DDD 的核心原则之一:使用一种基于模型的语言进行所有交流——讨论用它,文档用它,代码里的类名、方法名也用它。因为语言贯穿所有交流形式,所以叫通用语言(Ubiquitous Language)。
flowchart LR
subgraph 传统方式["传统方式:靠翻译,层层失真"]
E1["领域专家<br/>(业务行话)"] -->|需求翻译| A1["分析师文档"]
A1 -->|设计翻译| D1["开发人员"]
D1 -->|代码实现| C1[("代码<br/>(第三套术语)")]
end
subgraph DDD方式["DDD 方式:一种语言贯通"]
E2["领域专家"] <==>|"通用语言<br/>(基于领域模型)"| D2["开发人员"]
D2 --- C2[("代码 = 模型<br/>术语完全一致")]
E2 --- C2
end
实例:空管系统里有个概念叫"偏航告警"。如果代码里叫 YawDeviationWarningProcessor,而管制员嘴里叫"偏航告警",需求评审时大家讨论的是 A,代码里实现的是 B,三个月后没人能对上号。按 DDD 的做法,团队约定统一叫"偏航告警(RouteDeviationAlert)",类名、数据库表、会议讨论全部用同一个词——模型和代码之间不再存在翻译层。
通用语言不是一次定稿的,它随模型的演进而演进:当你发现某个词说不清一件事,往往不是表达问题,而是模型有问题——这就是重构的信号。
第 3 章 模型驱动设计(Model-Driven Design)
这是全书篇幅最大的一章,讲模型怎么落地成代码。Eric Evans 提供了一套标准化的"构造块"(Building Blocks)。
分层架构
先把领域层隔离出来,别让业务逻辑散落在界面和数据库代码里:
flowchart TD
UI["用户界面层<br/>展示信息、接收输入"]
APP["应用层<br/>协调任务、编排流程<br/>(很薄,不含业务规则)"]
DOM["领域层<br/>★ 业务概念、业务规则、业务状态<br/>模型的家"]
INFRA["基础设施层<br/>持久化、消息、框架支撑"]
UI --> APP --> DOM
INFRA -.支撑.- DOM
关键原则:领域层不依赖界面,不被基础设施污染。判断标准很简单——把界面整个换掉(比如从网页换成 App),领域层应该一行都不用改。
七个构造块
flowchart LR
subgraph 领域层["领域层的构造块"]
direction TB
E["实体 Entity<br/>有身份,有生命周期"]
VO["值对象 Value Object<br/>无身份,不可变"]
S["服务 Service<br/>不属于任何对象的操作"]
M["模块 Module<br/>按概念分组"]
AGG["聚合 Aggregate<br/>一致性边界 + 聚合根"]
F["工厂 Factory<br/>封装复杂创建"]
R["资源库 Repository<br/>封装查询与重建"]
AGG --> E
AGG --> VO
F --> AGG
R --> AGG
end
① 实体(Entity):靠"身份"区分
有些对象,即使属性全变了,它还是"它"。一架飞机换了涂装、换了座椅布局,航班号 CA1234 还是那架飞机。实体由身份标识(ID)定义,而不是属性。人也是典型的实体——同名同姓的两个人依然是两个人。
② 值对象(Value Object):靠"属性"区分
有些对象没有身份,只描述"是什么"。地址"北京市海淀区某路 1 号"就是值对象:两个地址属性完全相同,就是同一个地址,可以互换、可以共享。值对象应该是不可变的——要改就新建一个。
新手最常见的错误是把一切都建成实体。书里建议:优先考虑值对象,只有当"身份"确实重要时才用实体——值对象更简单、更安全、性能更好。
③ 服务(Service):无家可归的操作
有些操作天然不属于任何实体或值对象。比如"转账":钱从 A 账户到 B 账户,这个操作该挂在 A 身上还是 B 身上?都不合适。把它做成一个领域服务 TransferService。判断标准:操作的概念属于领域,但它本身是无状态的。
④ 模块(Module):高内聚、低耦合的分组
把概念上紧密相关的类放进同一个模块。模块名也应该进入通用语言——当团队说"这是计费模块的事",所有人都知道边界在哪。
⑤ 聚合(Aggregate):一致性的边界
对象之间关系复杂时,改一个对象可能牵连一片。聚合的解法是:把关联紧密的对象圈进一个边界,选一个实体做"聚合根"(Aggregate Root),外部只能通过根访问内部对象。
书里的例子:客户(Customer)是聚合根,地址(Address)、联系人是内部对象。外部想要地址?找客户要,拿到的是一个拷贝,不能直接改——所有对聚合内部的修改都经过根把关,不变量(invariant)就有了守护者。
flowchart TD
subgraph 聚合边界["聚合边界"]
ROOT["客户 Customer<br/>(聚合根)"]
ADDR["地址 Address"]
CONTACT["联系人 Contact"]
ROOT --> ADDR
ROOT --> CONTACT
end
OUT["外部对象"] -->|"只能持有根的引用"| ROOT
OUT -.->|"禁止直接访问"| ADDR
⑥ 工厂(Factory):封装"怎么造"
创建一个复杂聚合涉及大量知识:内部结构、对象间关系、初始规则。如果让调用方自己 new 再拼装,等于"给你塑胶、橡胶、金属、硅,自己组装一台打印机"——组装知识泄漏到了所有调用点。工厂把创建逻辑收进一处,客户只要说"我要一个完整的客户聚合"。
⑦ 资源库(Repository):封装"怎么找"
对象创建后存进数据库,之后要用时再取出来。资源库给领域对象提供一个"像集合一样"的查询接口(findById、findByName),把 SQL 和 ORM 细节全部藏在背后。领域代码面对的好像是一个内存中的对象集合,至于底下是 MySQL 还是 MongoDB,领域层不关心。
三者分工一句话:工厂管出生,资源库管找回,聚合管活着时的一致性。
第 4 章 面向深层理解的重构
模型不是一次画对的。随着对业务理解的加深,团队会不断发现"原来的模型表达不了新需求"。DDD 的态度是:持续重构,并且专门朝"深层理解"的方向重构——不是消除代码重复那种技术重构,而是当领域专家随口说出一个新概念时,敏锐地意识到"这个词值得成为模型里的一等公民"。
书里称之为凸现关键概念:把隐晦的、散落在各处的业务规则,提炼成显式的类和方法。
实例:空管系统最初把"两机航线交叉判断"写在一段几百行的过程代码里。重构后,团队把它提炼成 FlightPathConflict 对象和 conflictsWith() 方法——业务规则从"代码的副作用"变成了"模型中可以直接讨论的概念"。测试可以单独写,管制员和开发可以用同一个词讨论它。
第 5 章 保持模型一致性:大型项目的生存法则
前面讲的都是"一个团队、一个模型"的情况。企业级项目有多个团队并行开发,理想中"全公司统一的大模型"实际上根本维持不住——每个团队都知道自己后院里有什么,但对其他地方一无所知,改一处就可能破坏别人的功能。
Eric Evans 的结论很反直觉:不要试图维持一个注定四分五裂的大模型,而是有意识地把大模型拆成几个小模型,每个模型有清晰的边界。
界定的上下文(Bounded Context)
每个模型只在一个明确的边界内有效,这个边界叫界定的上下文。同一个词在不同上下文里可以有完全不同的含义:
实例:电商系统里"商品"这个词——
- 在销售上下文里,它有价格、促销、库存;
- 在物流上下文里,它只有重量、体积、包装方式;
- 在售后上下文里,它只有保修期和维修记录。
试图用一个统一的 Product 类满足三个团队,结果一定是个臃肿的"上帝类"。承认三个上下文各有各的"商品",反而各自清晰。
flowchart LR
subgraph 销售上下文["销售上下文"]
P1["商品:价格/促销/库存"]
end
subgraph 物流上下文["物流上下文"]
P2["商品:重量/体积/包装"]
end
subgraph 售后上下文["售后上下文"]
P3["商品:保修期/维修记录"]
end
P1 <-->|"上下文映射<br/>(翻译)"| P2
P2 <-->|"上下文映射"| P3
上下文之间的六种关系模式
上下文不能各自为战,它们之间需要集成。书里给出了一组模式,按"两个团队的合作意愿和权力关系"来选择:
flowchart TD
Q{"两个上下文如何共处?"}
Q -->|"愿意深度共享代码"| SK["共享内核 Shared Kernel<br/>划出一块双方都依赖的小模型<br/>共同维护,改动需协商"]
Q -->|"上下游关系明确"| CS["客户-供应商 Customer-Supplier<br/>上游按下游需求规划开发"]
Q -->|"下游无力影响上游"| CF["顺从者 Conformist<br/>直接采用上游模型,放弃翻译"]
Q -->|"要对接脏乱差的遗留系统"| ACL["防崩溃层 Anticorruption Layer<br/>建一层翻译,把污染挡在门外"]
Q -->|"集成成本 > 收益"| SW["独立方法 Separate Ways<br/>干脆不集成,各做各的"]
Q -->|"一个系统被多方集成"| OHS["开放主机服务 Open Host Service<br/>定义标准协议,免去一对一翻译"]
几个重点:
- 上下文映射(Context Map):把所有上下文和它们之间的关系画在一张图上。这张图是大型项目里最重要的架构文档——它让每个团队都看清"我的边界在哪、我和谁有契约"。
- 共享内核:两个团队共用一小部分模型(比如通用的客户标识)。共享意味着牵一发而动全身,所以内核要尽量小。
- 防崩溃层(ACL):对接遗留系统时,遗留模型往往概念混乱。如果直接在自己的代码里用它的概念,好模型会被污染。ACL 是一层专门的翻译器:外面进来的是遗留概念,翻成本地模型再入内。遗留系统的"腐烂"被挡在层外。
- 顺从者 vs 防崩溃层:下游对上游没有话语权时,要么顺从(直接用上游模型),要么自建翻译层。前者省成本,后者保纯洁——取决于上游模型的质量。
- 开放主机服务:当你的系统要被十个团队集成时,给每个团队定制翻译层会把你拖垮。定义一套标准协议(比如一套公共 API),所有集成方遵循它。
精炼(Distillation):找到核心域
大模型拆小之后还有一个问题:哪里最值得投入最优秀的人力?答案是核心域(Core Domain)——代表领域本质、构成业务竞争力的那一小块。
书里的例子非常精彩:空中交通监控系统里,“航线"看起来无处不在,但它其实是个普通概念,很多领域都有航线。系统真正的"心脏"是基于雷达数据实时计算飞行轨道、发现航线交叉并告警的那个模块——这才是这套系统区别于任何普通航线系统的本质,也是最该投入最强团队、精雕细琢的地方。其余部分(普通子域)可以外包、可以买现成组件、可以交给初级团队。
战略层面的启示:不是所有代码都值得写好。把最好的资源压在核心域上,其他地方"够用就行”。
第 6 章 专访 Eric Evans
书末是 InfoQ 对 Eric Evans 的访谈,几个观点值得记住:
- DDD 不是新方法,而是对二十年来优秀团队实践的系统化总结;
- DDD 与敏捷并不冲突,恰恰相反——没有好模型的敏捷团队,重构会越来越吃力;
- DDD 的难点不在技术,在于团队是否愿意持续地和领域专家坐在一起打磨模型和语言。
全书总结:一张 DDD 知识地图
mindmap
root((领域驱动设计<br/>DDD))
核心思想
软件是领域的映射
模型是选择性抽象
模型直接活在代码里
通用语言
一种语言贯穿讨论与代码
说不清 = 模型有问题
模型驱动设计
分层架构
界面层
应用层
领域层
基础设施层
构造块
实体 = 身份
值对象 = 属性 + 不可变
服务 = 无状态操作
聚合 = 一致性边界
工厂 = 管创建
资源库 = 管存取
深层理解
持续重构
凸现关键概念
保持模型一致性
界定的上下文
上下文映射
共享内核
客户-供应商
顺从者
防崩溃层
独立方法
开放主机服务
精炼出核心域
DDD 最打动人的地方在于,它把软件开发从"写代码"重新定义为"持续理解业务、并把理解精确表达出来“的过程。代码只是表达模型的介质,模型才是资产。
如果你是零基础读者,建议按这个顺序消化:先吃透"通用语言”(它不需要任何技术背景,却能立刻改善团队协作),再掌握七个构造块(写代码时随时对照),最后在有大型项目或微服务需求时再深入"界定上下文"那一套——你会发现近年火热的微服务拆分方法论,思想源头正是这本书的 Bounded Context。