跳转到内容

Ubiquitous Language 不是 DDD 的装饰术语

Ubiquitous Language 听起来很像那种 DDD 演讲里必须出现的词,好让幻灯片显得足够“架构”。

但它不是装饰。

Ubiquitous Language 是软件项目里非常重要的保护机制之一。它保护团队免受误解、错误模型、失真的 API,以及那些 UI 文案和代码表达完全不同含义的问题。它甚至能预防一类看起来像技术 Bug、实际上只是语言事故顺便带上了 Deployment Pipeline 的问题。

核心思想其实很简单:

一个团队需要一套共同语言,来描述它正在处理的业务领域。

不是随便一套项目黑话,不是 Wiki 里放几个词条,也不是“业务这么叫,IT 那么叫,不过大家都知道是什么意思”。

而是一套有意识连接业务、Product、Development、Test、UI、API 与 Code 的语言。

这就是 Ubiquitous Language。

Domain-driven Design 首先并不是 Pattern。

不是 Aggregate。 不是 Repository。 不是 Value Object。 也不是把一些 Tactical Building Block 扔进项目里,就能让它显得更成熟。

DDD 从 Domain 开始。

也就是 Problem Space:业务规则、术语、流程、例外与系统需要表达的意义。

语言正是在这里变得关键。

如果连业务概念叫什么都没有共识,你怎么构建一个好的 Domain Model?如果同一个词在三个团队里表示三件事,你怎么划分边界?如果没人能准确说清一个对象在业务上代表什么,你又怎么设计稳定的 API?

Model 并不是从代码开始的。

Model 先存在于语言里。

代码只是后来那个更精确、也更不宽容的版本。

语言模糊,模型就会模糊。模型模糊,代码最终一定会把这种模糊暴露得非常诚实。

Ubiquitous 并不意味着所有地方都必须叫同一个名字

Section titled “Ubiquitous 并不意味着所有地方都必须叫同一个名字”

先澄清一个常见误解:Ubiquitous Language 并不要求系统的每个地方都出现完全相同的词。

UI Label 可以比内部业务术语更符合用户语言。 API Field 可以比 Button 更技术化。 Data Model 有自己的约束。 团队日常交流也可能比一个精确的 Domain Term 更简短。

这些并不自动构成问题。

真正的问题是:差异是否只是偶然产生的。

Customer 用一个词,Product Owner 用第二个,Backend 用第三个,Frontend 用第四个,Database 再用第五个——然后所有人都假装这只是个人偏好。

不是。

到了这一步,我们已经无法确定:不同名称是否指向同一概念,或者同一个名称是否指向不同概念。

两种情况都很危险。

所以 Ubiquitous Language 不是语言上的“一刀切”。

它要求的是有意识的映射。

必须能回答:

  • 业务术语是什么?
  • UI 如何展示它?
  • API Contract 里叫什么?
  • Code 里叫什么?
  • 这个含义在哪个 Context 内成立?
  • 哪里发生翻译?
  • 哪里绝对不能悄悄翻译?

如果这些关系说不清,得到的不是灵活系统,而是一团带 Autocomplete 的语义雾。

并不是所有地方都必须同名,但所有名称都必须有意识地对应。

有一次,我被拉进了一场 Bug 讨论。

Call 里有 Frontend Developer、Backend Developer、外部客户、内部 Stakeholder、Product Owner,最后还有作为 Architect 的我。会议要素非常齐全:一个 Bug、六种观点,以及一个名叫 Statement 的对象。

所有人都在讨论这个 Statement

遗憾的是,没有人真正指的是同一件东西。

客户在业务上谈的是一份服务结算。IT 里某个时候把它叫成了 Accounting Statement。为了省时间,日常沟通里最后只剩下 Statement

与此同时,Frontend 在另一个地方又建模了真正意义上的“发票”——业务上更接近 Invoice。但那个对象也叫 statement

也许因为长得差不多,也许因为代码里已经有一个 statement,也许只是“历史上就是这么长出来的”。每次听到这种话,我都知道某个旧的架构事故大概率被压在地板下面。

最后结果是:

客户说的是服务结算。 Backend 说的是 Accounting Statement。 Frontend 想的是 Invoice。 PO 想的是某个 Process State。 部分 Stakeholder 想的是 Report。

所有人都使用同一个词。

可惜并没有讨论同一个东西。

这就是关键。

问题不在于 Statement 是英文。英文不是坏事,德文也救不了架构。

问题在于这个词已经不再代表一个清晰的业务概念。

它变成了多个含义的容器。

一个词如果什么都可以表示,那么在代码里它就再也无法可靠地表示某一件事。

所有人使用同一个词,并不代表脑中是同一个 Model。

有人可能会说:“那把名字改正确不就行了?”

是,也不完全是。

只改名字太浅了。

错误名称往往只是更深层问题的可见症状:团队实际上没有共同的业务模型。

Name 不只是 Cosmetic。

Name 会携带假设。

一个对象叫 Invoice,我会期待它是一张发票。 叫 Statement,我会期待某种对账、说明或报表。 叫 BillingReport,我会期待一个 Report。 叫 Settlement,我会期待一个结算或清算过程。

这些预期会影响 Developer 如何写代码,Tester 如何写 Test Case,Product Owner 如何写 Acceptance Criteria,Stakeholder 如何理解 Requirement,用户如何理解 UI。

Name 是 Architecture。

不是因为好名字看起来更优雅,而是因为名字会划分边界、表达职责、稳定含义——或者把含义彻底搅混。

懒得把概念说清楚,不是一条架构原则。

语言问题往往最先在 Frontend 变得明显。

Frontend 位于 User、Product、Design、Backend 与 API 的交界处。它必须表达业务,指导输入,解释状态,翻译错误,还经常需要把多个技术模型组合成一个用户能理解的 ViewModel。

如果业务语言模糊,Frontend 就会变成替其他建模失误擦屁股的翻译公司。

API Field 叫 statement,UI 显示“发票”,业务部门说“服务结算”,表单里写“结算证明”,代码里又有一个 StatementViewModel,而这个 ViewModel 根据 Route 不同还表示不同东西。

技术上当然都能做。

你也可以给潮湿的地下室重新贴墙纸。

问题只是:你准备假装问题出在墙纸上多久?

因此,Front-end Architecture 尤其需要清晰语言。不是因为 Frontend 听起来“离业务近”,而是因为 Product Model、User Understanding 与 Technical Contract 不一致时,Frontend 最容易暴露这种错位。

当然,同一个词在不同 Context 中完全可以有不同含义。

Identity Context 里的 Account 不一定等于 Accounting Context 里的 Account。Banking 里的 Statement 可以和服务结算 Context 里的 Statement 完全不同。CRM 里的 Customer 也不一定和 Support 或 Billing 里的 Customer 是同一个 Model。

这些都没问题。

前提是边界可见。

这也是 Bounded Context 的真正价值之一:不是一个时髦架构术语,而是给“意义”搭一道围栏。

在一个 Context 内,术语应该足够精确。

跨越 Context Boundary 时可以翻译,但翻译必须显式发生。

不能通过一个到处传递的 DTO 偷偷发生。 不能通过一个号称“中立”的 Shared Model 把所有模糊语义集中起来。 更不能继续用一个什么都能表示的 Statement,只是因为没人愿意做决定。

不同 Context 里一个词有不同含义,并不是问题。

真正的问题是没人知道当前究竟在哪个 Context 里说话。

项目里出现下面这些句子时,应该立即提高警惕:

  • “我们历史上就是这么叫的。”
  • “客户那边叫法不一样。”
  • “Frontend 和 Backend 里名字不一样。”
  • “其实它是一张 Invoice,不过代码里叫 Statement。”
  • “很难解释,但大家都知道什么意思。”
  • “这是个集合对象。”
  • “这个字段要根据情况解释。”
  • “技术上是一个东西,业务上又不完全是。”
  • “以后再统一命名。”
  • “大家都知道什么意思。”

最后一句尤其危险。

“大家都知道什么意思”经常只表示:大家已经学会忍受同一种模糊。

这不是共同理解。

这只是带着 Sprint Goal 的集体妥协。

Ubiquitous Language 不需要从一个大型 DDD Workshop 开始。

很多时候,只需要在正确的时刻慢一点。

尤其是在激烈的 Bug Discussion 里,不要马上跳进 Log、JSON Payload 或责任归属。

先问:

我们现在说的真的是同一个东西吗?

面对一个叫 Statement 的对象,可以继续问:

Statement of what? 给谁? 发生在哪个 Process? 这个对象的业务职责是什么? 它是 Invoice?Settlement?Report?Status?Document?Booking Information?

如果不同人给出了不同答案,那其实找到了一件很有价值的东西。

不只是当前 Bug,而是一整类 Bug 的潜在根源。

实践中,一些简单规则已经很有帮助:

  • 按 Context 定义术语
  • 不要因为省事而稀释 Domain Term
  • Code Object 按业务职责命名
  • API Contract 避免模糊的集合词
  • 翻译时确认语义是否仍然一致
  • UI Label 与 Domain Term 的映射要有意识
  • Glossary 保持轻量,但要持续维护
  • 一个词有多个含义时,显式建模,不要继续绕过去

目标不是 Language Police。

目标是让 Domain 足够稳定,能够可靠地构建软件。

Ubiquitous Language 不是一个装饰性的 DDD Term。

它连接 Domain 与 Code,连接 Customer 与 Team,连接 Product Decision 与 Implementation,也连接“我们真正想表达什么”和“系统最后构建了什么”。

如果这条连接断掉,产生的并不是小小的 Naming 问题。

会出现错误 Model、错误 API、错误 UI,以及一类所有人都很难立刻理解的 Bug——因为大家用相同的词,脑中却是不同的图景。

所以语言本身就是架构工作。

不是那种有 Diagram 和 Arrow 的漂亮架构工作。

更多时候,是会议中比较尴尬的那一种:

“等一下。我们说 Statement 时,到底具体指什么?”

然后房间里突然安静。

这通常是好事。

所有人都使用同一个词,并不代表他们在讨论同一个 Model。

Ubiquitous Language 也不是从 Glossary 开始的。

它开始于有人问出这句话的那一刻:

我们现在说的,真的是同一个东西吗?