重读领域驱动设计——如何说好一门通用语言

395 阅读7分钟

结论先行

在 DDD 中,通用语言是以限界上下文为边界的。如果一个产品或者项目有多个限界上下文,我们就需要为每个限界上下文定义通用语言。限界上下文提供了一个语义边界,来保持通用语言和领域概念的一一对应关系。这个约束解决了现实世界中同样的名词在不同场景、时机下对应不同的业务概念所带来的歧义问题,帮助团队在使用通用语言交流的时候可以无歧义沟通。

初尝“通用语言”

最初我对于如何构建通用语言的认识,来自于《领域驱动设计》第一章中的案例。这个案例生动的展示了开发人员如何在和领域专家的沟通过程中,建立了双方理解一致的通用语言,并且使用这个语言来进行双方的沟通。基于那个案例,我当时对构建通用语言的理解就是要:

  • 技术人员使用业务人员的用语作为开发词汇;
  • 划分好聚合,将这些词汇关联到聚合上;
  • 技术人员要将这些词汇映射到代码实现中;
  • 这些词汇会随着项目的发展一点点扩展;

带着这份理解,我在曾经负责过的小型项目上做了一些实践,效果都很不错。在很长一段时间,团队的开发人员体会到了在和业务人员交流时候心有灵犀、会心一笑的快感;也很少听到“这个东西不是我要的”这类批评了。

“通用语言”遇到同名词汇时就变得不清不楚了

然而,当我来到ThoughtWorks参与到一些几十号人的项目时,我发现根据这个原则构建起来的通用语言,在遇到同名多义的词汇时,就无法保证团队内部的沟通是无歧义的。而这种歧义又会导致团队成员说着同样的话想着不同的事情的情况出现,例如:

  • 同名的业务词汇与实际业务关系不清:“为什么不能给销售订单增加一个是否投诉的字段,界面上都是显示在销售订单上的”——销售订单到底是个什么东西,能干什么不能干什么是怎么确定的?
  • 同名的业务词汇与不同的业务词汇关联:“我在销售订单付款后改变了买家信息,为什么我看销售订单的预定里的买家也发生了改变”——这里说的买家信息有几个?
  • 同名的业务词汇之间的关系不清楚:“为什么我变更了profile 上的买家地址,销售订单上的买家地址就跟着改变了” ——这里说订单上的买家地址和profile 上的买家地址是一个什么关系?

通过添加约束消除歧义

下图是 DDD 概念的一个元模型图。从图的左下角,我们可以看到在构建通用语言时,还有两个额外的约束条件:子域和限界上下文。

在 DDD 中,软件的核心是其为客户解决领域相关的问题的能力

这里的领域,就是指软件系统要解决的实际问题相关的东西的集合。

例如:为一个电子商务公司开发一个电商系统,我们就需要围绕这个盈利模式的运营方式、业务规则,比如如何进货,如何促销,如何物流等等了解这个电子商务公司的盈利模式,所有和业务相关的东西都属于领域。

领域分为问题域和解决方案域两部分。

为了分解问题域的复杂度,问题域又会被拆解为多个子域,每个子域都要明确待解决的业务问题和业务流程,以及通过解决业务问题为企业带来了什么样的业务价值(这个是因,业务流程和要解决的业务问题是果)。

在清晰的定义子域后,我们就可以建立通用语言来提取该子域的领域知识,并基于通用语言为解决问题建立领域模型。

一个领域模型会存在于一个限界上下文中。限界上下文在 DDD 中用来定义模型的适用范围、模型的用途、以及在何处保持一致,限界上下文会让团队明确模型的职责边界是什么。同时,通用语言被限定在限界上下文中;限界上下文提供了一个语义边界,在每个限界上下文内通用语言的每个词汇必须和领域概念一一对应。

理想条件下,子域和限界上下文是一一对应。但是子域划分的粒度,遗留系统的现状,语言的歧义,团队结构等子域和限界上下文对应可能是1:N 或者 N:N 的。

通过限界上下文间的映射,上下文中的多个模型会协作以满足系统需求。我们也可以了解在不同上下文中的同名词汇是否存在关系,存在什么样的关系。

对通用语言而言,子域解释了通用语言和现实世界业务活动的关系;限界上下文提供了一个语义边界,来保持通用语言和领域概念的一一对应关系;上下文映射则提供了不同限界上下中的通用语言的转换关系。

来解决下前文的问题

前文所述的订单及订单的相关概念存在着歧义,我们来看下通过子域、限界上下文和上下文映射是怎么消除这些歧义的:

因为同名的业务词汇与实际业务关系不清导致的疑惑

“为什么不能在销售订单中增加一个是否投诉的字段,界面上都是显示在销售订单上的”

假设,这里所说的销售订单存在于销售子域下,那么这个订单应该解决的是销售过程中的问题。订单的生命周期以销售开始到销售终止。一般而言投诉属于售后环节,在销售订单上声明是否投诉字段,意味着销售订单的职能突破了销售子域。UI 上的销售订单展示了聚合的信息,和同名的领域模型不一定保持一致。

因为同名的业务词汇与不同的业务词汇关联导致的疑惑

“我在订单付款后改变了买家信息,为什么我看订单的预定里的买家也发生了改变”

在订单上有两种买家信息,可以通过在不同的上下文中隔离来区别这两个拥有相同含义但却是不同词汇的词汇。在销售子域中建立两个上下文,分别为预定有界上下文和购买上下文,把订单领域模型拆分到这两个上下文中。在不同的上下文中,订单都有自己的买家信息,就解决了“在订单付款后改变了买家信息,为什么我看订单的预定里的买家也发生了改变”这个问题。

因为同名的业务词汇之间的关系不清楚导致的疑惑

“为什么我变更了profile 上的买家地址,订单上的买家地址就跟着改变了”

订单存在于购买上下文,profile 存在于身份信息上下文中,购买上下文和身份信息上下文存在映射关系,在订单创建时候从身份信息上下文复制买家地址,在订单中单独保存。这样就解决了“为什么我变更了profile 上的买家地址,订单上的买家地址就跟着改变了” 的问题。

引用:

  1. 《领域驱动设计》
  2. 《实现领域驱动设计》
  3. 当Subdomain遇见Bounded Context
  4. DDD的终极大招——By Experience
  5. 《领域驱动设计学习:领域、子域、限界上下文》

文/ThoughtWorks王岩

更多精彩洞见,请关注微信公众号:ThoughtWorks洞见