# 写 SQL 之前，先说清“已完成订单”指什么

> WHERE 子句会悄悄选定一种含义，选错了，驻场的前线部署工程师（FDE）要用客户信任买单。

原文: https://fdetimes.net/zh/guides/business-glossary-before-writing-code/

设想你在一家零售公司驻场的第二周。你演示一个名为“本月已完成订单”的看板。销售负责人点头，财务主管却皱起眉头，因为这个数字比她账上的高。数据管道没有 bug，两人只是用同一个名字在数不同的东西。

Martin Fowler 认为这种情况在一个又一个项目里反复出现：“Customer”“Product”这类多义词会带来无休止的混乱。对开发内部产品的工程师来说，代价是多开几次会。

对驻场客户现场的前线部署工程师（FDE）来说，代价是信任：人们一旦见过一个错误的数字，之后的每个数字都会带着怀疑去看。

之所以要在写代码之前解决这个问题，原因很简单。Fowler 写道，软件不擅长应对歧义。人可以说“已完成订单”，并在上下文中彼此理解。而 `WHERE status = 'completed'` 子句必须只选一种含义，而且是悄悄选的，不会告诉任何人。

## 为什么一个词会有三种含义？

事件风暴（Event Storming）的创始人 Alberto Brandolini 直言，术语天生就有歧义：每个人对同一个词都有自己的理解，因为每个部门的视角和需求不同。

Codurance 博客的一篇文章举了同一家公司内部的例子：销售专家和财务专家可能给同一个词赋予两个不同的概念。

Fowler 还有一个更值得记住的观点：业务上下文之间的边界通常不是由技术决定的，而主要由人的文化决定，比如哪些团队一起工作、哪个部门负责什么。每个上下文对同一个概念可能有完全不同的模型。

所以你的目标不是在“正确”含义的争论中胜出。Three Dots Labs 说得很清楚：同一个词在不同上下文中含义不同是正常的，只要上下文之间的边界划分清楚。

FDE 的工作是把每种含义摆到台面上，给它起名，并记录它适用的范围。

**要点:** 不要强迫全公司只用一种含义，给每种含义起一个独立的名字。

## 一个“已完成”，三个数字

回到那家假设的零售公司。销售认为客户付款成功，订单就算完成。仓库认为货物扫码出库、交给承运商，订单才算完成。财务则要求客户已收货、退货期已过且没有退款，订单才算完成。

用数字说得更具体些：假设本月有 1,000 笔订单付款成功，920 笔已从仓库发出，850 笔已过退货期且没有退款。你的看板按付款状态列统计，所以显示 1,000。财务记的是 850。这 150 笔订单的差距不是数据错误，而是定义错误。

再加上一个回答“我的订单完成了吗？”的客服 Agent。如果 Agent 按销售的含义理解这个词，那么对一笔还在仓库里的订单，它也会回答“是”。同样的定义错误，这次直接到了终端客户那里。

这个例子确定后的术语表可能是这样的：

| 规范名称 | 负责部门 | 可度量规则 | 数据字段 | 已确定的边界情况 |
|---|---|---|---|---|
| 已付款订单 | 销售 | 支付交易成功 | `payment_status = 'captured'` | 货到付款订单何时算数？ |
| 已发货订单 | 仓库 | 有出库扫码记录 | `shipped_at IS NOT NULL` | 部分发货的订单 |
| 已结算收入订单 | 财务 | 已送达、已过退货期、无退款 | `settled_at IS NOT NULL` | 换成其他商品的订单 |

注意，这张表刻意不再出现“已完成”一词。从现在起，单独的“已完成”不得出现在代码、指标名称或 Agent 的提示词中。

## 五个步骤确定术语表

第一步是收集术语。阅读工单、表名、列名、客户发来的 Excel 文件和会议纪要，标出出现最频繁的名词。正如 Fowler 所警告的，“客户”“订单”“商品”这些最熟悉的词往往最危险。

第二步：按照 Codurance 的建议，分别访谈每位领域专家。不要问“已完成是什么意思？”，因为每个人都会泛泛而谈。要用边界情况来问：“退了一半款的订单算不算？”“客户换了尺码，原订单算什么？”正是这些情况，才能暴露人们实际使用的定义。

第三步：把各部门请到同一场会议里。Three Dots Labs 认为，事件风暴是发现并统一开发者与利益相关方词汇的最佳方法之一。

当“客户付款”“货物出库”“退货期结束”这些事件按时间顺序贴出来，每个人都能看到自己的“已完成”落在这条线的哪个位置。

第四步：为每种含义命名并指定负责人。每种含义有自己的名字，有一个在定义需要变更时负责的部门，还有一条机器可以执行的规则。“订单没问题”不是定义，“有 `settled_at` 且没有退款记录”才是。

第五步：把术语表写进系统。Eric Evans 把这个终点称为通用语言（ubiquitous language）：开发者和业务人员之间共享的严谨语言，在会议里和代码里用法完全一致。变量名、列名、指标名和 Agent 的工具描述都必须使用规范名称。

这一步可以借助工具。dbt 的语义层让你在建模层只定义一次指标，无论各业务部门使用什么工具，都共享同一个定义。

定义变更时，改动会同步到所有调用它的地方。在 Palantir，Ontology 用对象、属性和关联来描述客户组织。但工具只负责存储定义，选择哪个定义，仍然是人与人之间的事。

## 让术语表早早夭折的错误

最常见的错误，是采用嗓门最大的人或者为项目付钱的部门的含义。另一个部门不会在会上争论，而是默默继续用自己的 Excel 文件，你的系统就成了第二个没人信任的系统。

第二个错误，是术语表只放在 wiki 上，从未进入代码。如果列名仍叫 `is_completed`，半年后新来的工程师会按自己猜测的含义去用它。

第三个错误，是把术语表冻结。Fowler 强调，语言必须随团队对领域的理解一起演进，所以要记录每个术语确定的日期和批准人，并在客户流程变化时重新讨论。

## 把这项技能变成职业优势

阅读 FDE 岗位描述时，留意“domain modeling”“ontology”“semantic layer”或“work with business stakeholders”这类表述。它们说明公司需要的正是这项技能。

简历上不要写“善于与客户沟通”。写一行有具体结果的经历，比如与三个部门确定了一套指标定义，消除了看板与财务账目之间的差距。

面试时，准备一个关于某个术语让两个部门互相误解的故事：你是怎么发现的，问了哪些边界情况，最终的定义落在系统的哪个位置。这样的故事能说明你既能处理数据，也能与人打交道，而这正是这个岗位的要求。

## 练习：一个状态列，一个下午

打开你正在处理的订单表或客户表，对状态列执行 `SELECT DISTINCT`，列出其中的所有取值。为每个取值写一个边界情况问题，比如“部分退款的订单取哪个值？”。

拿着这份清单去问两个不同部门的人，然后把结果填进像上面示例那样的五列表格。两方回答不一致的地方，就是你术语表的第一行。

写错的代码一个下午就能改好。一个被误解的词一旦扩散到每张表、每份报表和每个 Agent 回答里，就还得在客户的脑子里纠正过来。所以，在敲下第一个 `WHERE` 之前，先把它的含义确定下来。

**本周动手试试:**

- 挑出当前项目工单或表名中出现最频繁的三个词，分别问两个不同部门的两个人每个词是什么意思，记下他们回答不一致的地方。
- 为每个词写两个边界情况问题（部分退款的订单、客户换货），再回去问一遍。
- 在代码或看板中重命名一个有歧义的指标，让名称直接说明它衡量的是哪种含义。

## 信源

- [Bounded Context (Martin Fowler)](https://martinfowler.com/bliki/BoundedContext.html)

- [Ubiquitous Language (Martin Fowler)](https://martinfowler.com/bliki/UbiquitousLanguage.html)

- [Alberto Brandolini on Domain-Driven Design and EventStorming (codecentric)](https://www.codecentric.de/en/knowledge-hub/blog/alberto-brandolini-domain-driven-design-eventstorming)

- [Talk to the business (Codurance)](https://codurance.com/2015/10/26/Talk-to-the-business/)

- [Ubiquitous Language (Three Dots Labs Academy)](https://academy.threedots.tech/knowledge/ubiquitous-language/)

- [dbt Semantic Layer (dbt Docs)](https://docs.getdbt.com/docs/use-dbt-semantic-layer/dbt-semantic-layer)

- [Ontology overview (Palantir Foundry docs)](https://www.palantir.com/docs/foundry/ontology/overview/)
