Chapter 01
领域与语言:领域、模型、统一语言三块地基
总览给了"先发现业务、再设计模型"的两段式地图。这一章重置最底层的三块地基词汇——领域、模型、统一语言。后面所有战略与战术构件都建在这三个词上,所以这里每个定义都要落到「云市」的具体生意上,而不是停在抽象。
本章你将建立的 schema
- 领域 Domain 是"问题"(云市这门生意),不是你写的那套软件
- 模型 Model 是对领域的选择性抽象——地图不是疆域,同一个领域可以有多个模型
- 统一语言 Ubiquitous Language 是把专家口语和代码绑死的那根绳子;说话和代码分叉,是模型错了
- 语言的作用域是限界上下文,不是全局——同一个"商品"在不同上下文是不同的东西(埋钩子,03 章兑现)
- 贫血领域模型 Anemic Domain Model 是穿着 OO 外衣的过程式代码——先认得它的样子
全章用同一个贯穿全书的例子:「云市」电商交易平台。云市要做的生意是——买家挑商品、下单、付款、发货、签收。这门生意背后有五个职责分区(目录、销售、库存、配送、支付),但本章先不拆边界,只用它来钉死三个最基础的词:什么是这门生意本身(领域),什么是你为它建的那套抽象(模型),以及怎么让说生意的人和写代码的人用同一套词(统一语言)。
1.1领域 Domain:是"问题",不是"软件"
领域(Domain)是你要解决问题的那块真实业务世界本身——云市这门生意,而不是你为它写的代码。
多数工程师听到"领域"会下意识想到一堆类、表、接口——那是软件。把领域错认成软件,会让人一上来就讨论用什么框架、建几张表,而真正的领域问题(云市的商家为什么要"价格冻结"?"已确认的订单"在生意上意味着什么不能反悔?)从没被认真拆解。先认准"领域=问题空间里的真实业务",才谈得上为它建一个称手的模型。
把这层说穿:领域是问题,软件是你提交的一个解。云市的领域,是"在一个交易平台上,让买卖双方安全地完成一笔买卖"这件事——它包含商家定价规则、库存怎么算可用、订单一旦确认就受什么约束、钱货怎么对得上。这些事实在你写第一行代码之前就存在,也独立于你最终用 Java 还是 Go。你写的 OrderService、那张 t_order 表,是你对这个领域的一次回答,回答得好不好,要拿回领域去验。
这个区分不是咬文嚼字。它决定了你提问的顺序:领域优先的人先问"云市的订单,什么情况下不能再改?"——这是业务事实;软件优先的人先问"订单表要加哪些字段、状态用 int 还是枚举?"——这是实现选择。前者锚定问题,后者锚定一个还没被验证的解。DDD 全书的方法论,就是强制你把提问顺序倒过来。
可以类比成医生看病:领域是病人身上真实发生的病(问题),处方和手术是解(软件)。好医生先把病诊断清楚,再开方;差医生拿着自己最熟的那把手术刀到处找能下刀的地方。类比失效处:病灶是客观的,而软件领域里"什么算一笔有效订单"往往是商家、运营、法务一起约定出来的——所以领域知识要靠和领域专家(domain expert,最懂这门生意的业务方)反复对话才能挖出来,这正是统一语言要解决的事(1.3 节)。
与下一个概念的关系:领域是无穷复杂的真实世界,代码不可能、也不需要复刻它的全部。你只能挑出对解决当前问题有用的那部分,做一份"够用就好"的抽象——这份抽象就是模型。
1.2模型 Model:对领域的选择性抽象
模型(Model)是对领域的一次选择性抽象:刻意只保留对当前问题有用的概念和规则,扔掉其余——地图不是疆域。
真实领域里的"订单"有上百个事实:下单时间、买家心情、平台当天的促销文案、物流公司的车队调度……如果模型试图装下全部,它会大到没人能维护,也回答不了任何具体问题。模型的价值恰恰在于它扔掉了什么——一张只画了地铁线和换乘站的地图,比一张 1:1 复刻每栋楼的航拍图更能帮你坐地铁。
把这层说穿:模型是有目的的简化。"地图不是疆域"——一张地图永远不等于它描绘的那片土地,地图的好坏不看它复刻了多少,而看它为了某个用途保留了对的东西。云市的销售场景关心"这笔订单总额对不对、状态能不能往下走",所以销售的订单模型只保留 OrderLine(订单项)、Money(金额)、状态机;它不保留商品的详情图集和营销文案——那些对"算钱、推进状态"毫无用处。
由此推出一个让很多人意外的结论:同一个领域,可以、而且应该有多个模型。"商品 Product"这个领域概念,在不同用途下需要不同的抽象——
| 场景(用途) | "商品"这个模型保留什么 | 刻意扔掉什么 |
|---|---|---|
| 目录 Catalog(展示) | 标题、详情、图集、类目、参数、营销文案 | 实时库存、运费 |
| 销售 Sales(下单) | productId + 名称快照 + 单价快照(价格被冻结) | 图集、详情、营销文案 |
| 库存 Inventory(履约) | Sku + 可用数量 + 仓位 | 标题、图片、价格 |
| 配送 Shipping(运输) | 重量、体积、件数 | 价格、标题、类目 |
四个场景说的都是"商品",但需要的模型截然不同。试图用一个企业级统一的 Product 类同时服务这四种用途,它会长出几十个互相矛盾、半数永远为空的字段——配送场景拿到这个对象,要在一堆营销文案里翻找重量。这个"一个统一模型服务全场景"的旧直觉,正是本书要重置的第一个执念;它的正式答案叫限界上下文,03 章兑现。
与下一个概念的关系:模型既然是人为选择的抽象,就必须有一套办法保证"业务方脑子里的模型"和"代码里实现的模型"是同一个,否则模型就成了工程师的一厢情愿。绑住这两端的那根绳子,叫统一语言。
1.3统一语言 Ubiquitous Language:绑住口语和代码的绳子
统一语言(Ubiquitous Language)是领域专家和工程师共用、并且直接体现在代码里的同一套术语——白话讲就是"嘴上怎么说、代码就怎么写"。
领域知识在领域专家脑子里,能力在工程师手里。两边各说一套话时,中间一定有一道翻译层:专家说"确认订单",需求文档写成"提交保存订单数据",代码落成 setStatus(2)。每翻译一次,就丢一点意图、混进一点误解。统一语言要做的,就是取消这道翻译层——让代码里的类名、方法名,就是业务方开会时嘴里说的那个词。
把这层说穿:统一语言是一根双向绑死的绳子。它要求模型里的每个概念,在专家口语和代码里用同一个名字;更关键的是它的诊断作用——当说话和代码对不上时,那是一个信号:模型错了,必须改一端去对齐另一端。语言不是事后给代码贴的注释,它是模型的骨架。Evans 的原话是:术语上的卡壳,往往暴露了模型里一个还没想清楚的概念。
场景走查:一句"确认订单"是怎么断掉的
云市的运营专家在评审会上说:"买家点确认后,这笔订单就确认了,确认之后订单项不能再为空,金额也不能再变。"这句话里藏着三个领域事实:有一个叫"确认"的动作;确认是订单状态的一次推进;确认后有些东西被锁死(订单项非空、金额冻结)。现在看两种代码怎么承接这句话:
// 专家说"确认订单",代码说"把状态字段设成 2"——绳子断了
class OrderStatusService {
void setStatus(Order order, int status) {
order.setStatus(status); // status=2 是什么?只有写的人当时知道
}
}
// 调用处:业务意图被翻译成了一个魔法数字
statusService.setStatus(order, 2);
// 专家说"确认订单",代码就叫 confirm()——同一个词
class Order {
void confirm() {
// "确认后订单项不能为空"这条领域规则,就长在这个动作里
...
}
}
// 调用处:读代码就是读那句业务原话
order.confirm();
第一段不是"风格差一点",而是语义丢了。setStatus(2) 这个名字里没有"确认",没有"确认后订单项非空",没有"状态只能往前走不能回退"——它把一句有约束的业务陈述,压成了一个谁都能传任意整数的赋值操作。三个月后没人记得 2 是确认还是取消,于是有人传了 setStatus(2) 去跳过支付,状态机就此被绕开。第二段的 order.confirm() 则把那句业务原话原封不动搬进了代码,专家、文档、代码三处用的是同一个词。Vernon 把这条概括为:统一语言活在代码里,也活在团队的嘴里,两处必须是同一套词。
当你发现"业务方一直说确认,代码里却叫 setStatus(2) / updateOrder() / process()"——不要简单把方法改个名了事。这种偏差通常意味着模型本身没把"确认"当回事:它没有"确认"这个动作概念,只有一个谁都能改的状态字段。正确的修法是回到模型,让"确认"成为一个带规则的真实行为。语言对不上,先怀疑模型,而不是先怀疑命名。
与下一个概念的关系:既然语言要绑死模型,那"这套语言在哪些范围内有效"就成了关键问题。直觉会说"全公司统一一套术语最好"——这恰恰是下一节要拆掉的错觉。
1.4语言的作用域是限界上下文,不是全局
一套统一语言只在一个明确的边界内部保持一致和无歧义;这个边界叫限界上下文(Bounded Context)。追求"全公司一套术语"既不可能也不该追。
"统一语言"这个名字容易让人误以为目标是"全公司只用一套词"。但真实的大企业里,同一个词在不同部门本就是不同的东西。强行让所有部门共用一个 Product / Customer 定义,结果是谁都不满意、字段越长越多、没人敢删。给语言划定一个明确的有效边界,每个边界内部自洽,边界之间显式翻译——这比一个虚假的全局统一健康得多。
把这层说穿:统一语言的"统一",统一的是一个边界之内,不是全公司。回到 1.2 节那张表——"商品"在目录里是胖展示对象,在销售里是价格冻结的下单快照,在库存里是 SKU+数量。这三个"商品"不是同一个东西穿了三件衣服,它们就是三个不同的概念,各自在自己的边界里有清晰且唯一的含义。把它们硬塞进一个全局 Product,等于把三个互相矛盾的定义压进一个名字,歧义不会消失,只会被埋进字段里。
"客户 Customer"同理:在销售里它是下单的买家 Buyer,在支付里它是付款的付款方 Payer,在营销里它是有等级积分的会员 Member。三个上下文说"客户"时,指的是三种不同的责任和数据。
有人提议:"为了避免数据冗余,全公司用一张 Product 表、一个 Product 类,目录、销售、库存、配送都从它取数。"这个统一的 Product 类,三个月后会长成什么样?它违反了本章哪条原则?
先答,再展开
它会长成一个有几十个字段的"上帝类":展示要的图集文案、下单要的价格快照、库存要的仓位、配送要的重量体积全堆在一起,任一场景拿到它都有一大半字段为空或不相关;改一个字段要担心是否影响另外三个场景,于是没人敢动。
它违反了 1.2 节"模型是选择性抽象"——这个类什么都不舍弃,于是什么用途都服务不好;也违反了本节的核心——它假设语言能全局统一,而真实情况是"商品"在四个边界里本就是四个概念。正确做法是承认边界,每个边界一个自洽的模型,边界之间在缝隙处显式翻译。这条边界的正式名字、以及边界之间怎么翻译,03 章限界上下文讲透。
与下一个概念的关系:到这里,三块地基(领域、模型、语言)和它们的有效边界都立住了。还差一件事——认出一种最常见的"伪富模型":它长着面向对象的外壳,骨子里却是过程式代码,让前面所有原则形同虚设。
1.5贫血领域模型 Anemic Domain Model:穿着 OO 外衣的过程式代码
贫血领域模型(Anemic Domain Model):领域对象只剩 getter/setter 的数据,全部业务规则被抽到外部的 Service 里——看着是对象,实质是过程式。
这是最普遍、也最容易被当成"标准分层"的反模式。它有完整的类、有 private 字段、有封装的外形,所以几乎所有用过 Controller/Service/DAO 分层的工程师都在写它而不自知。本节只做一件事:给它点名、描出它的样子,让你以后一眼认出。它为什么是问题、什么时候反而合理,本章按下不表(见下方边界声明)。
它长什么样:Fowler 给的判据很直接——领域对象里几乎没有行为,只有一堆 getXxx() / setXxx();所有真正的业务逻辑(校验、状态推进、金额计算)都住在一个名字像 XxxService 的类里,这个 Service 把领域对象当成纯数据袋,取出字段、算一通、再 set 回去。对象有数据没行为,行为有逻辑没数据——这正是过程式编程的形状,只不过套了一层类的外壳。
// 领域对象:只有 getter/setter,零行为——一个数据袋
class Order {
private OrderStatus status;
private List<OrderLine> lines;
private Money total;
public OrderStatus getStatus() { return status; }
public void setStatus(OrderStatus s) { this.status = s; }
public List<OrderLine> getLines() { return lines; }
public void setLines(List<OrderLine> l){ this.lines = l; }
public Money getTotal() { return total; }
public void setTotal(Money t) { this.total = t; }
}
// 业务规则全被抽到外面:Order 自己管不住自己的不变量
class OrderService {
void confirm(Order order) {
if (order.getLines().isEmpty()) // 规则① 在外面
throw new IllegalStateException("订单项不能为空");
if (order.getStatus() != OrderStatus.CREATED) // 规则② 也在外面
throw new IllegalStateException("状态不允许确认");
order.setStatus(OrderStatus.CONFIRMED); // 谁都能绕过这里直接 setStatus
}
}
// 领域对象:行为和数据在一起,规则被对象自己守住
class Order {
private OrderStatus status;
private final List<OrderLine> lines;
private Money total;
// "确认订单"是 Order 的能力;规则被关在对象内部,外部绕不过去
public void confirm() {
if (lines.isEmpty()) // 不变量①:确认后订单项非空
throw new IllegalStateException("订单项不能为空");
if (status != OrderStatus.CREATED) // 不变量②:状态机单向推进
throw new IllegalStateException("只有 CREATED 才能确认");
this.status = OrderStatus.CONFIRMED;
}
// 注意:没有 setStatus —— 状态只能经由 confirm()/pay()/ship() 这些
// 带规则的动作改变,外部无法直接把它设成任意值
}
两段的差别不在代码量,在谁来保证规则永远成立。贫血版里,Order 暴露了 setStatus(),意味着任何拿到 order 的代码都能 order.setStatus(CONFIRMED) 直接跳过 OrderService.confirm() 里的两条校验——规则只在"大家都走 Service 这条路"的假设下成立,而这个假设迟早被打破。富模型版里,Order 没有 setStatus(),状态只能经由 confirm() 改变,而 confirm() 里焊死了规则,于是"已确认订单不能为空"这条不变量无论谁、从哪里调用,都不可能被违反。
如果 Order 只有 setStatus(status)、没有 confirm(),那么"已确认的订单不能为空"这条规则,由谁来保证?这种保证可靠吗?
停 10 秒,再展开
只能由每一处调用 setStatus(CONFIRMED) 之前的人各自记得先检查订单项非空。也就是说,规则不在对象里,而散落在所有调用方手里——靠"大家都记得、都自觉走校验"来维持。这不可靠:只要有一个调用方(一段补数据脚本、一个新同事写的接口、一次紧急修复)忘了校验直接 set,非法状态就进了数据库,而且 Order 对此毫无防御。
富模型把这条规则关进 confirm(),让对象自己守护自己的不变量,外部根本没有"绕过去"这个选项。这就是 05 章聚合的核心思想的雏形——这里先埋下。
本节故意只描述现象、不下最终判决。贫血模型为什么会发生(业务逻辑为什么会"流"进 Service 层),它真正的代价和成因,留到 06 章战术构件讲领域服务 vs 应用服务时算总账。而"贫血是不是一定错"——并不是:对没有复杂规则的支撑域、通用域,贫血+CRUD 往往是最划算的选择,这个平衡观点留到 08 章。现在你只需要做到一件事:看到"对象全 getter/setter、规则全在 Service",能立刻认出这是贫血模型。
OrderService,Order 暴露 setStatus 让外部能绕过校验;右边 confirm() 长在 Order 内部、没有 setStatus,规则无处可绕。差别不是代码量,是"谁守护不变量"。§本章 self-check
先合上教程,把答案写在纸上或编辑器里。写完再点开对照——直接点开等于把这一节当再读一遍。
- 用一句话各自说清:领域、模型、统一语言三者分别是什么?它们之间什么关系?
- "同一个领域可以有多个模型"——拿云市的"商品"举例说明,为什么不该用一个统一的
Product类服务所有场景? - 为什么统一语言的作用域是限界上下文、而不是全公司?用云市的"客户"说明。
- 给你一段代码:
Order全是 getter/setter,确认、算金额的逻辑都在OrderService里。这叫什么模型?它最直接的代价是什么?
答案(先做完再展开)
- 领域是你要解决问题的那块真实业务(云市这门生意,是"问题");模型是为某个用途对领域做的选择性抽象(地图不是疆域);统一语言是让业务专家口语和代码用同一套词、并以此诊断模型对错的绳子。关系:领域是问题,模型是对它的抽象,语言把模型的口语端和代码端绑死。
- "商品"在目录是胖展示对象、在销售是价格冻结的下单快照、在库存是 SKU+数量、在配送是重量体积——四个用途要保留/舍弃的东西完全不同。一个统一
Product类要同时装下全部,会长出几十个互相矛盾、半数为空的字段,谁都服务不好,也没人敢改。模型必须是选择性的。 - 因为同一个词在不同业务边界本就是不同概念:"客户"在销售是 Buyer(下单方)、在支付是 Payer(付款方)、在营销是 Member(会员等级积分)。强行全局统一只会把三个矛盾定义压进一个名字。语言只在一个边界(限界上下文)内部保持唯一含义,边界之间显式翻译。
- 贫血领域模型。最直接的代价:
Order守不住自己的不变量——它暴露setStatus等写口,任何调用方都能绕过OrderService里的校验直接改状态,于是"已确认订单非空""状态单向推进"这些规则只在"大家都自觉走 Service"的假设下成立,迟早被打破,非法状态进库。
不看 03、05 章,先自己推一下"商品"该被切成几个类
云市目前只有一个 Product 类,目录、销售、库存、配送全用它。请你不查后面的章节,先在纸上把这个 Product 拆成它真正应该变成的几个独立概念,给每个起一个准确的名字、列出它各自保留哪些字段。拆完想一想:这几个概念之间,"是同一个商品"这件事靠什么字段维系?
提示(卡住再展开)
按用途拆,至少四个:目录里的展示商品(标题/图集/文案)、销售里的下单快照(productId + 名称快照 + 单价快照)、库存里的 StockItem(Sku + 可用量 + 仓位)、配送里的货物(重量/体积/件数)。维系"是同一个商品"的,不是对象指针,而是一个共享的标识——productId / skuId。"按标识引用、而不是持有对象"这条,正是 05 章聚合的关键原则;这些边界怎么划、之间怎么翻译,是 03 章限界上下文。你刚刚自己推到了这两章的门口。