14 领域语言
用贴近问题域的语言表达规则,让术语、语法、代码和错误信息共同承担领域约束。
学习目标
- 能把“14 领域语言”解释为领域词汇、语义边界、表达、执行和反馈共同组成的规则接口
- 能用“提示22:靠近问题域编程”把业务规则写成可读、可验证且能拒绝非法状态的内部或外部 DSL
- 能在真实代码中改变一条领域规则,定位错误语义和首个边界偏离,并让领域人员与独立复核者重放验证
为什么 14 领域语言不是给代码换名字
本页依据 David Thomas、Andrew Hunt《程序员修炼之道:通向务实的最高境界(第2版)》,云风译,电子工业出版社,2020年4月,ISBN 9787121384356 的公开完整中文目录,独立重构 14 领域语言。本页不复制原书正文、插图、练习答案或代码,而把目录命题转写成领域语言管线、错误定位、代码样例和复核证据。
领域语言的价值在于让规则靠近提出问题的人:领域专家能读出语义并指出不成立的表达,程序员能把规则编译或解释成明确行为,错误信息能告诉操作者如何修复。单纯把 process() 改名为 settle() 没有改变语义;如果输入、状态、边界和错误仍然隐藏,命名只是装饰。
本单元把问题拆成 领域词汇 → 语义边界 → 表达 → 执行 → 反馈。每一步都写出谁负责、什么输入合法、何时拒绝、错误如何回到领域概念,以及代码和领域表述是否仍然一致。
14 领域语言:规则接口
| 节点 | 要问的问题 | 可观察产物 | 失败动作 |
|---|---|---|---|
| 领域词汇 | 领域人如何称呼对象、动作和状态 | 词汇表、例子和禁用词 | 与领域专家核对同义词和歧义 |
| 语义边界 | 哪些状态、转换和约束合法 | 不变量、边界样例和拒绝条件 | 明确非法状态,不让调用者猜 |
| 表达 | 规则以什么 DSL 或 API 被写出 | 可读的命令、配置或代码 | 缩小语法并保留领域词汇 |
| 执行 | 表达如何产生动作、数据和错误 | 解释器、编译器、规则引擎或函数 | 保留原始领域位置和输入 |
| 反馈 | 使用者怎样知道哪里不符合规则 | 错误定位、修复建议和回归 | 在最早语义边界拒绝 |
Domain Vocabulary 不只是类名;它还包括用户界面、错误信息、配置和测试中的同一语义。
↡定义领域对象能处于哪些状态、允许哪些转换,以及非法输入应在哪里被拒绝的边界。Semantic Boundary 让非法状态在最接近来源处失败,而不是等到数据库或用户结果才暴露。
↡把领域规则组织成可读、可组合、可验证的命令、配置、表达式或函数的内部语言。Internal DSL 可以利用宿主语言的类型和测试,但不能让宿主语言细节掩盖领域约束。
↡由业务人员、配置作者或外部系统直接书写,并由程序解析、校验和执行的领域表达。External DSL 要为语法错误、语义错误、版本和迁移提供明确反馈,不是把复杂文本交给一个通用解析器就结束。
↡把非法输入或状态映射回领域词汇、位置、规则和修复动作的错误反馈方式。Error Localization 让人知道是哪个领域规则不满足,以及下一步应改值、改状态还是请求领域决策。
提示22:靠近问题域编程
提示22:靠近问题域编程。先从 Domain Vocabulary 和 Semantic Boundary 开始,再选择 Internal DSL、External DSL 或普通函数表达规则。目标不是让业务人员直接维护所有代码,而是减少从业务问题到实现行为之间的翻译损耗,让领域反馈能在代码和测试中留下位置。
一个好的表达让“冻结账户”“释放预授权”“部分退款”“超过结算窗口”等动作各自有边界和错误,而不是统一落到一个含糊的 updateStatus。领域语言也必须接受反例:一个词在不同团队有不同含义时,先命名冲突和适用范围,再决定是否合并。
从提示到可失败的因果链
本页主链是 Domain Vocabulary → Semantic Boundary → DSL 表达 → 执行 → Error Localization。每条边要写明传递的是规则、状态、数据、命令还是反馈;每个节点都要有输入、输出、owner、版本和拒绝动作。语法能解析但语义错误,仍然是失败。
先写预测,再执行正常样本、边界样本和一次故障样本。三类样本共享规则版本和输入,只改变一个词汇、约束或表达条件;结果出现差异时记录首个语义偏离,不能靠通用异常消息掩盖领域位置。
用代码表达领域边界
下面的 Internal DSL 让领域词汇出现在代码中,同时用类型和函数结果表达非法状态。它不是完整支付系统,而是把“部分退款必须小于已捕获金额”这条规则放在靠近领域的位置:
type Money = { cents: number; currency: "CNY" | "USD" };
type CapturedPayment = { captured: Money; refundedCents: number };
type RefundDecision =
| { kind: "approved"; remainingCents: number }
| {
kind: "rejected";
reason: "amount-exceeds-captured" | "currency-mismatch";
};
function requestPartialRefund(
payment: CapturedPayment,
amount: Money,
): RefundDecision {
if (amount.currency !== payment.captured.currency)
return { kind: "rejected", reason: "currency-mismatch" };
const available = payment.captured.cents - payment.refundedCents;
if (amount.cents <= 0 || amount.cents > available)
return { kind: "rejected", reason: "amount-exceeds-captured" };
return { kind: "approved", remainingCents: available - amount.cents };
}这里仍需领域人员确认“可退款金额”“已捕获”“部分退款”的语义和错误措辞。类型和测试可以阻止一部分非法输入,但不能独自决定退款窗口、权限、审计或用户通知;这些约束要进入 Semantic Boundary 的合同。
外部 DSL 与反馈
当规则需要被运营、分析或领域专家维护时,External DSL 可以让表达脱离编程语言细节。例如:
refund_policy:
name: partial_refund
when: captured_amount >= requested_amount
deny:
- currency_mismatch
- outside_refund_window
message: requested amount exceeds captured amount解析器应分别报告词法、语法和语义问题,并指出字段、规则和修复建议。requested amount exceeds captured amount 比“invalid policy”更接近 Domain Vocabulary,但仍要由项目 owner 核对金额单位、舍入、权限和窗口时区。
三步完成一次领域语言重构
先对齐词汇与语义边界
与领域人员选一条规则,写对象、动作、状态、合法转换、非法样例和同义词。把 Domain Vocabulary 与 Semantic Boundary 放在同一张规则卡片上,标记仍需决策的术语。
可重放的领域语言记录
domain_language_record:
unit: tpp20-topic-14-domain-languages
rule: 部分退款不得超过已捕获且仍可退的金额
domain_vocabulary: 捕获、可退款、部分退款、退款窗口
semantic_boundary: 币种一致、金额大于零、金额不超过余额、窗口有效
internal_dsl: requestPartialRefund(payment, amount)
external_dsl: refund_policy YAML
error_localization: 字段 -> 规则 -> 修复建议
injected_change: 只把可退款余额边界改为等于上限
first_mismatch: 解析通过但时区窗口错误
recovery: 补时区语义测试并让领域 owner 复核错误信息这份记录把“靠近问题域”变成代码、配置、测试和反馈的共同合同。若领域人员只能确认词汇却无法确认状态转换,或程序员只能确认语法却无法定位业务错误,语言仍然没有靠近问题域。
选择、拒绝与迁移矩阵
| 判断 | 接受证据 | 应拒绝的信号 |
|---|---|---|
| 词汇 | 领域人员能定义、举例和指出禁用同义词 | 同一个词在不同流程含义漂移 |
| 语义 | 合法状态、边界和非法样例清楚 | 只写“有效/无效”没有规则 |
| 内部 DSL | 类型、函数和测试保留领域词汇 | 宿主语言细节掩盖领域动作 |
| 外部 DSL | 解析、版本、迁移和错误定位可验证 | 语法能读但错误只说 invalid |
| 执行 | 领域表达与运行行为一致 | 代码命名正确、行为仍绕过规则 |
| 反馈 | 错误指向规则、位置和修复动作 | 领域专家只能看最终堆栈 |
迁移到云服务、数据系统或 AI 辅助开发时,分别记录领域 owner、规则版本、数据敏感性、解析器版本、自动化反馈和审计要求。工具可以生成类型、解析器和测试,却不能替领域人员决定语义边界。
常见误区
本章回顾
掌握 14 领域语言,不是增加一层术语,而是按 提示22:靠近问题域编程 让 Domain Vocabulary、Semantic Boundary、Internal DSL 或 External DSL、执行行为和 Error Localization 共同表达规则。领域人员能核对语义,代码能拒绝非法状态,错误能回到规则位置,独立复核者才能重放结论。
可验证练习
练习
本组练习覆盖 14 领域语言 和 提示22:靠近问题域编程,要求提交 Domain Vocabulary、Semantic Boundary、Internal DSL、External DSL 和 Error Localization 的代码与复核证据。
问题 1: 如何把“部分退款不得超过可退款余额”从散落条件提升为领域表达?
问题 2: 如何实践“提示22:靠近问题域编程”,而不是只把函数重命名?
问题 3: 外部规则文件语法正确,但生产行为接受了错误时区。怎样定位和恢复?
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- Domain Vocabulary
问题域中稳定、可被领域专家确认的对象、动作、状态和约束词汇。
- Semantic Boundary
定义合法状态、允许转换和非法输入拒绝位置的领域边界。
- Internal DSL
通过宿主语言组织成可读、可组合和可验证领域规则的内部语言。
- External DSL
由业务或外部系统书写并由程序解析、校验和执行的领域表达。
- Error Localization
把错误映射回领域词汇、位置、规则和修复动作的反馈方式。
前后导航
来源与改写范围
- Pragmatic Programmer 作者页面:核对 Topic 14 与提示22的版本位置和主题范围。
- 中文目录页面:核对 14 领域语言、提示22:靠近问题域编程的公开目录范围。
- 出版社书目信息:交叉核对中文译本的出版信息与版次边界。