198-如何落地业务建模

开篇词｜为什么你需要学习业务建模？

你好，我是徐昊，欢迎和我一起学习业务建模。

对于业务建模，我想大多数人有这么一串疑问：

这个东西有什么用？好像面试也不怎么问到，学了能加薪吗？

我听说微服务好像需要用到，那我不用微服务需要学吗？微服务如果不流行了，那我是不是白学了？

建模不建模，代码写出来有什么不一样吗？

这些都是好问题。我完全可以理解这些问题的出发点：希望从解决实际问题的角度出发，看看业务建模能帮我们做什么。

然而我要强调的是：业务建模首先是一个定义问题的方法，其次才是解决问题的方法。

解决问题还是定义问题？

我们很容易理解解决问题带来的价值，但也很容易忽略定义问题的力量。这么说你可能不太能理解，我来给你分享个案例，来讲一讲我们是怎么通过定义问题，把解决方案的复杂度直接降低几个数量级。

我曾在做项目时遇到过这样一个需求：客户要求我们将某种报告转换成 PDF 格式，以方便用户下载。

这些报告中有大量的图片，图片上又有很多文字说明。要让这些报告在页面上显示，那我们可以根据浏览器提供的屏幕尺寸和需要显示的文字字数，动态计算字体样式，保证图片说明的可读性。但是在转换成 PDF 的过程中，由于我们无法得知屏幕的信息，难免会出现一些排版格式的错误。

于是，我们项目组的一位技术骨干向我提出一个“完美解决方案”：

使用当时最先进的后台渲染技术，在服务器端的浏览器进程中渲染页面（通过 PhantomJS）。这样我们就能准确地计算合理的字体样式了。

然后，再将渲染好的页面通过浏览器后台进程转存为 PDF 文档，并通过云端的大规模存储服务进行缓存（AWS S3）。

为了应对突发巨量下载的可能性，我们还可以用当时最先进的云计算（AWS EC2），构造一组后台渲染集群。并根据下载量的大小，利用云计算的弹性动态调整集群的大小。

显然，对于我们要解决的问题，这位技术骨干是这样定义的：PDF 中保留的信息样式，和用户在浏览器中看到的一致。

然而真是这样吗？在解决问题之前，我们要先搞清楚客户要保留图片文字说明的目的。

我们知道，文字说明是版权信息声明，因此保留它的主要意义，就在于避免法务纠纷。那么问题来了，PDF 中的信息样式，必须和用户在浏览器中看到的一致吗？

答案是：不一定！

我们都知道 PDF 是一种矢量格式，可以无限放大。那么无论我们提供的文字有多小，都可以经由放大看到其中的信息。

所以我们可以重新定义一下这个问题：PDF 中需要保留图片的版权信息，以避免法务纠纷。而读者只需要知道版权信息的存在，不一定需要直接阅读它。

从这个问题定义出发，最终的解决方案格外简单：当图片说明文字超过一定字数，直接选择一个很小的字号，以保证信息留存在 PDF 中。

而实现方案，其实就是一条 if 语句的事儿。

我想，你一定可以理解一个后台渲染集群和一条 if 语句在成本上的差异。这就是对问题定义不同，带来的解决方案上的差异。

而这两个方案的差异点，主要在于我们看待问题的角度不同：

从业务的角度，去看待会产生什么实际影响；

还是从技术的角度，去看待如何完美地解决某个被别人定义的问题。

通常来说，问题定义得准确，那么实现起来也不会复杂到哪里去。反之，如果没有搞清楚要解决什么问题，就可能需要各种奇技淫巧去弥补问题定义上的不足。

要知道，多数人为了逃避真正的思考，愿意做任何事。而如果程序员为了逃避理解问题并给出定义，最后通常就会成为只会倒腾各种技术方案的架构师。

至此，定义问题的重要性想必你已了然于胸，接下来一个更为重要的问题是：该怎么有效定义问题呢？

想要有效定义问题，就要从业务出发，首先尝试在业务中寻找简化问题的可能性，然后在技术中寻找对应的解决方案。业务建模就是这样一个过程。明确业务中的关键问题，使用易于实现的模型将业务问题表达出来。

可以看到，在上面的案例中，我们甚至没有动用任何建模手段，而仅仅是在业务上下文中澄清了真正的诉求，就极大地简化了解决方案。

而一旦涉及软件开发的核心难点，也就是处理隐藏在业务知识中的核心复杂度，除了清晰地理解业务诉求之外，还需要通过建模的方式对这种复杂度进行简化与精炼。

学习业务建模的难点在哪里？

业务建模的方法有很多种，我们日常用到的：从着眼数据库设计的实体关系法（E-R Modeling），到面向对象分析与设计法（Object Oriented Analysis and Design），再到围绕知识消化的领域驱动设计（Domain Driven Design），不一而足。

而业务建模方法的吊诡之处就在于，使用它的难度并不在于建模本身。无论是哪种建模方法，你总能按照书里教程中的例子，照猫画虎地做个七七八八。

业务建模真正的难点有两个：

清晰地定义业务问题，并让所有干系人都接受你对业务问题的定义；

在特定架构的约束下，将模型实现出来。

接下来我就着重讲讲这两个难点，看看我们该怎么应对。

难点一：如何定义问题并让所有人接受？

对于“定义问题并让所有人接受”，我相信有些人会双手一摊，然后说：我不是业务专家，没有业务知识，怎么来定义问题？

注意，这里我所说的定义业务问题，是指对业务问题的梳理和总结，明确对业务的影响及产出。

所以不是让你说明自己做的是什么业务，而是要你去提炼总结它，并通过你所选用的业务建模方法中蕴含的逻辑框架去验证它。如果发现漏洞和不足，要及时提出，让人参与讨论。

那么这里对你的挑战就不仅仅是建模本身了，而在于如何获取业务方的信任，并展开有效的讨论。关于这个问题，大部分建模的教程中都不怎么涉及，但这却是能否有效使用业务建模方法的关键。

难点二：如何在特定架构下实现模型？

建模方法有着更长的生命周期，而技术架构却总在不停地演化，所以“在特定架构的约束下实现模型”，就成了学习业务建模的另一个难点。

我们在学习建模方法的时候，往往会不自觉地忽略架构对模型的影响。于是大概率会出现这样一种情况：学会了一种建模方法，却因为不知道怎么处理架构约束，而无法将其应用到实际工作中。

就好像如果今天你去学习面向对象建模方法，那么很可能你为模型所编写的代码，仍然会是在与数据存储无关的单体架构下，练习各种继承与接口的用法。而一旦在工作中真正使用，数据库、网络的访问开销，会把你的模型打散得七零八落。

正因为这两个困难，业务建模方法成了一种“所有人都在谈论它，但是没人知道具体怎么做；所有人都觉得其他人在使用它，于是只能声称自己也在用的东西”。

因而在学习业务建模时，一方面，我们需要转移自己的关注点。

不要太在意获得模型是否完美，是否在概念上足够抽象，是否使用了模式，等等。反而，我们更应该关注该如何围绕模型，建立有效的沟通与反馈机制。也就是说，该怎么将模型中蕴含的逻辑讲给别人听。并且还要让别人能听懂，能给出反馈。

理想的模型，需要是所有人都能懂的模型，而不是诘屈聱牙，满是完美的模式和抽象的概念。

另一方面，我们还需要对架构演化趋势保持足够的关注度。

通常而言，每 3-5 年就会出现新的架构风格。比如过去 15 年，我们就经历了从单体到多层，再到微服务的改变。

在不同的架构风格下，业务建模和模型实现模式（Implementation Pattern）的最佳实践会存在些许差异。而这些差异，很可能会决定建模的成败。在后面的课程里，你就会看到很多这样的例子。

课程是如何设计的？

刚刚说的这些，其实也正是咱们这门课的核心设计理念。接下来，我就说说这门课具体是怎么设计的。

我将课程分成“旧约”和“新约”两部分。“旧约”主要讲解从单体到多层架构风格下，业务建模的最佳实践以及实现模式。“新约”则更偏重在云计算的时代里，新的架构模式下业务建模的最新发展与演化。

旧约：“前云时代”的领域驱动设计

首先，我会为你介绍领域驱动设计方法。领域驱动设计时至今日仍是绝大多数人进行业务建模的首要方法。作为一种建模方法，它并不是那么出色，然而在如何引领需求发掘，如何建立沟通反馈，如何与业务方共建模型等问题上，提供了一套出色的框架。

而后，我会为你介绍在多层架构成为主流架构选择的时代中，领域驱动设计在模型实现上遇到了哪些挑战，我们该如何应对它。这部分是一个热身与预告，可以帮助我们理解架构约束会对模型带来何种影响。

最后我会介绍四种建模方法，分别是：催化剂法、角色 - 目标 - 实体法、事件风暴与四色法，以弥补领域设计在建模能力上的缺陷。

这部分是过去十五年“前云时代”，我们对领域驱动设计应用的总结与提炼，因而称为“旧约”。

新约：“云时代”的业务建模

来到今时今日，云时代彻底改变了我们构造软件的方式，微服务、中台、软件的 SaaS 化都是这一影响的体现。新的架构约束会极大影响我们业务建模的方法，但同时也大大扩展了业务建模的内涵。

首先，我会为你介绍云到底带来了哪些观念上的改变，它具体的颠覆性体现在什么地方，对我们构造业务系统有什么影响。

而后呢，我会介绍一种由我发明的业务建模方法 8X Flow 法，用于解决以微服务、分布式事务为主导的架构风格中的业务建模问题。这个方法同样可以用于构建中台系统，也是我司目前用于中台建模的主要方法。

最后，我会介绍另外一个由我发明的用于 SaaS 化业务建模的方法：魔球服务建模法（Money Ball Offering Modeling）。它是一种从运营角度出发，构造 SaaS 化服务的方法。

开篇寄语

最后的最后，还想说段题外话。正如人生三大恨一样：海棠无香；鲥鱼多刺；随便一个应景的什么。我们这个行业呢，也有三大难：命名；缓存过期；随便一个应景的什么。

作为 ThoughtWorks 全球技术战略委员会成员之一，我有大量的机会和这个行业里最优秀的人才共事，比如 Rachel Laycock、Ian Robinson、Jim Webber、Sam Newman、Neal Ford、Martin Folwer、Rebecca Parsons 等等。

这些人有一个不那么广为人知的称号——字匠（Word Smith），也就是说他们对于某个概念，可以寻找到极端贴切，而又饶有趣味的命名。

我相信，是对定义问题的偏执，让他们获得了长久而成功的职业生涯。因而，哪怕仅仅作为一种思维训练法，业务建模也是值得学习的。

好了，从现在开始，就让我们一起开启学习业务建模的美妙旅程吧！同时，关于业务建模，如果你有什么想法或疑问，也欢迎在评论区留言，我会和你交流讨论。

01｜领域驱动设计到底在讲什么？

你好，我是徐昊。今天我们来聊聊领域驱动设计（Domain Driven Design，即 DDD）。

说起业务建模，领域驱动设计是一个绕不过去的话题。自从 Eric Evans 在千禧年后发布他的名著“Domain Driven Design：Tackling the Complexity in the Heart of Software”，领域驱动设计这一理念迅速被行业采纳，时至今日仍是绝大多数人进行业务建模的首要方法。

有意思的是，可能因为成书年代过于久远，大多数人并没有读过 Eric 的书，而是凭直觉本能地接受了领域驱动这一说法，或是在实践中跟随周围的实践者学习使用它。但是对于 Eric 到底在倡导一种什么样的做法并不了然。

所以今天这节课，我们要回顾一下领域驱动设计的要点和大致做法，从而可以更好地理解 DDD 从何处而来，以及 DDD 在其创始人的构想中是如何操作的。

领域模型对于业务系统是更好的选择

我们都知道，软件开发的核心难度在于处理隐藏在业务知识中的复杂度，那么模型就是对这种复杂度的简化与精炼。所以从某种意义上说，Eric 倡导的领域驱动设计是一种模型驱动的设计方法：通过领域模型（Domain Model）捕捉领域知识，使用领域模型构造更易维护的软件。

模型在领域驱动设计中，其实主要有三个用途：

通过模型反映软件实现（Implementation）的结构；

以模型为基础形成团队的统一语言（Ubiquitous Language）；

把模型作为精粹的知识，以用于传递。

这样做的好处是显而易见的：

理解了模型，你就会大致理解代码的结构；

在讨论需求的时候，研发人员可以很容易明白需要改动的代码，并对风险与进度有更好的评估；

模型比代码更简洁，毕竟模型是抽象出来的，因而有更低的传递成本。

模型驱动本身并不是什么新方法，像被所有人都视为编程基本功的数据结构，其实也是一系列的模型。我们都知道有一个著名的公式“程序 = 算法 + 数据结构”，实际上这也是一种模型驱动的思路，指的是从数据结构出发构造模型以描述问题，再通过算法解决问题。

在软件行业发展的早期，堆、栈、链表、树、图等与领域无关的模型，确实帮我们解决了从编译器、内存管理到数据库索引等大量的基础问题。因此，无数的成功案例让从业人员形成了一种习惯：将问题转化为与具体领域无关的数据结构，即构造与具体领域无关的模型。

而领域驱动则是对这种习惯的挑战，它实际讲的是：对于业务软件而言，从业务出发去构造与业务强相关的模型，是一种更好的选择。那么模型是从业务出发还是与领域无关，关键差异体现在人，而不是机器对模型的使用上。

构造操作系统、数据库等基础软件的团队，通常都有深厚的开发背景，对于他们而言，数据结构是一种常识。更重要的是，这种常识并不仅仅体现在对数据结构本身的理解上（如果仅仅是结构那还不能算难以理解），还体现在与数据结构配合的算法，这些算法产生的行为，以及这些行为能解决什么问题。

比如树（Tree）这种非常有用的数据结构，它可以配合深度优先（Depth-First）、广度优先（Breadth-First）遍历，产生不同的行为模式。那么当开发人员谈论树的时候，它们不仅仅指代这种数据结构，还暗指了背后可能存在的算法与行为模式，以及这种行为与我们当前要解决的业务功能上存在什么样的关联。

但是，如果我们构造的是业务系统，那么团队中就会引入并不具有开发背景的业务方参与。那么这个时候，与领域无关的数据结构及其关联算法，由于业务方并不了解，那么在他们的头脑中也就无法直观地映射为业务的流程和功能。这种认知上的差异，会造成团队沟通的困难，从而破坏统一语言的形成，加剧知识传递的难度。

于是在业务系统中，构造一种专用的模型（领域模型），将相关的业务流程与功能转化成模型的行为，就能避免开发人员与业务方的认知差异。这也是为什么我们讲，领域模型对于业务系统是一种更好的选择。

或许在今天看起来，这种选择是天经地义的。但事实是，这一理念的转变开始于面向对象技术的出现，而最终的完成，则是以行业对 DDD 的采纳作为标志的。

不同于软件行业对数据结构的长时间研究与积累，在不同的领域中该使用什么样的领域模型，其实并没有一个现成的做法。因而在 DDD 中，Eric Evans 提倡了一种叫做知识消化（Knowledge Crunching）的方法帮助我们去提炼领域模型。这么多年过去了，也产生了很多新的提炼领域模型的方法，但它们在宏观上仍然遵从知识消化的步骤。

知识消化的五个步骤

知识消化法具体来说有五个步骤，分别是：

关联模型与软件实现；

基于模型提取统一语言；

开发富含知识的模型；

精炼模型；

头脑风暴与试验。

在知识消化的五步中，关联模型与软件实现，是知识消化可以顺利进行的前提与基础。它将模型与代码统一在一起，使得对模型的修改，就等同于对代码的修改。

而根据模型提取统一语言，则会将业务方变成模型的使用者。那么通过统一语言进行需求讨论，实际就是通过模型对需求进行讨论。

后面三步呢，构成了一个提炼知识的循环：通过统一语言讨论需求；发现模型中的缺失或者不恰当的概念，精炼模型以反映业务的实践情况；对模型的修改引发了统一语言的改变，再以试验和头脑风暴的态度，使用新的语言以验证模型的准确。

如此循环往复，不断完善模型与统一语言。因其整体流程与重构（Refactoring）类似，也有人称之为重构循环。示意图如下：

说句题外话，目前很多人把 Knowledge Crunching 翻译为“知识消化”。不过在我看来，应该直译为“知识吧唧嘴”更好些，Crunching 就是吃薯片时发出的那种难以忽略的咔嚓咔嚓声。

你看，Knowledge Crunching 是一个如此有画面感的词汇，这就意味着当我们获取领域知识的时候，要大声地、引人注意地去获得反馈，哪怕这个反馈是负面的。

而且如果我们把它叫做“知识吧唧嘴”，我们很容易就能从宏观上理解 Knowledge Crunching 的含义了：吸收知识、接听反馈——正如你吃薯片时在吧唧嘴一样。

好了，言归正传，通过以上的分析，我们其实可以把“知识消化”这五步总结为“两关联一循环”：

“两关联”即：模型与软件实现关联；统一语言与模型关联；

“一循环”即：提炼知识的循环。

今天我们先介绍模型与软件实现关联。后面两节课，再关注统一语言与提炼知识的循环。

模型与软件实现关联

我们已经知道，领域驱动设计是一种模型驱动的设计方法。那么很自然地，我们可以得到这样一个结论：

模型的好坏直接影响了软件的实现；

模型的好坏直接影响了统一语言；

模型的好坏直接影响了传递效率与成本。

但 Eric Evans 在知识消化中并没有强调模型的好坏，反而非常强调模型与软件实现间的关联，这是一种极度违反直觉的说法。

这种反直觉的选择，背后的原因有两个：一是知识消化所倡导的方法，它本质上是一种迭代改进的试错法；第二则是一些历史原因。

所谓迭代改进试错法，就是不求一步到位，但求一次比一次好。正如我们刚才总结的，知识消化是“两关联一循环”。通过提炼知识的循环，技术方与业务方在不断地交流与反馈中，逐步完成对模型的淬炼。

无论起点多么低，只要能够持续改进，总有一天会有好结果的。而能够支撑持续改进基础的，则是实现方式与模型方式的一致。所以比起模型的好坏（总是会改好的），关联模型与软件实现就变得更为重要了。

历史原因则有两点：一是因为在当时，领域模型通常被认为是一种分析模型（Analysis Model），用以定义问题的，而无需与实现相关。这样做的坏处呢，我们下面再细讲。

二是因为当时处在面向对象技术大规模普及的前夕，由于行业对面向对象技术的应用不够成熟，将模型与实现关联需要付出额外的巨大成本，因而通常会选择一个相对容易、但与模型无关联的实现方式。这个相对容易的方式，往往是过程式的编程风格。

而与模型关联的实现方法，也就是被称作“富含知识的模型（Knowledge Rich Model）”，是一种面向对象的编程风格。因此，我们强调模型与实现关联，实际上也就在变相强调面向对象技术在表达领域模型上的优势。接下来我们具体分析。

从贫血模型到富含知识的模型

在 DDD 出版的年代，Hibernate（一种 Object Relationship Mapping 框架，可以将对象模型与其存储模型映射，从而以对象的角度去操作存储）还是个新鲜事物。大量的业务逻辑实际存在于数据访问对象中，或者干脆还在存储过程（Store Procedure）里。

如果把时光倒回到 2003 年前后，程序的“常规”写法和 DDD 提倡的关联模型与实现的写法，在逻辑组织上还是有显而易见的差异的。

我们现在考虑一个简单的例子，比如极客时间的用户订阅专栏。我们很容易在头脑中建立起它的模型：

在 ORM 流行起来之前的 2003 年（当然那时候没有 try-close 语法），如下的代码并不是不可接受：

class UserDAO {

  ...
  public User find(long id) {
    try(PreparedStatement query = connection.createStatement(...)) {
      ResultSet result = query.executeQuery(....);
      if (rs.next)
         return new User(rs.getLong(1), rs.getString(2), ....);
      ....
    } catch(SQLException e) {
       ...
    }
  }
}

class SubscriptionDAO {
  ...
  // 根据用户Id寻找其所订阅的专栏
  public List<Subscription> findSubscriptionsByUserId(long userId) {
     ...
  }

  // 根据用户Id，计算其所订阅的专栏的总价
  public double calculateTotalSubscriptionFee(long userId) {
     ...
  }
}

这样的实现方式就是被我司首席科学家 Martin Fowler 称作“贫血对象模型”（Anemic Model）的实现风格，即：对象仅仅对简单的数据进行封装，而关联关系和业务计算都散落在对象的范围之内。这种方式实际上是在沿用过程式的风格组织逻辑，而没有发挥面向对象技术的优势。

与之相对的则是“充血模型”，也就是与某个概念相关的主要行为与逻辑，都被封装到了对应的领域对象中。“充血模型”也就是 DDD 中强调的“富含知识的模型"。不过作为经历那个时代的程序员，以及 Martin Fowler 的同事来说，“充血模型”是我更习惯的一个叫法。

Eric 在 DDD 中总结了构造“富含知识的模型”的一些关键元素：实体（Entity）与值对象（Value Object）对照、通过聚合（Aggregation）关系管理生命周期等等。按照 DDD 的建议，刚才那段代码可以被改写为：

class User {

   // 获取用户订阅的所有专栏
   public List<Subscription> getSubscription() {
     ...
   }

   // 计算所订阅的专栏的总价
   public double getTotalSubscriptionFee() {
     ...
   }
}

class UserRepository {
  ...
  public User findById(long id) {
    ...
  }
}

从这段代码很容易就可以看出：User（用户）是聚合根（Aggregation Root）；Subscription（订阅）是无法独立于用户存在的，而是被聚合到用户对象中。

通过聚合关系表达业务概念

不同于第一段代码中单纯的数据封装，改写后这段代码里的 User，具有更多的逻辑。Subscription 的生命周期被 User 管理，无法脱离 User 的上下文独立存在，我们也无法构造一个没有 User 的 Subscription 对象。

而在之前的代码示例中，我们其实可以很容易地脱离 User，直接从数据库中查询出 Subscription 对象（通过调用 findSubscriptionsByUserId）。所有与 Subscription 相关的计算，其实也被封装在 User 上下文中。

这样做有什么好处呢？首先我们需要明白，在建模中，聚合关系代表了什么含义，然后才能看出“贫血模型”与“富含知识的模型”的差异。我们还是以极客时间的专栏为例。

为了表示用户订阅了某个专栏，我们需要同时使用“用户”与“订阅”两个概念。因为一旦脱离了“订阅”，“用户”只能单纯地表示用户个人的信息；而脱离了“用户”，“订阅”也只能表示专栏信息。那么只有两个放在一起，才能表达我们需要的含义：用户订阅的专栏。

也就是说，在我们的概念里，与业务概念对应的不仅仅是单个对象。通过关联关系连接的一组对象，也可以表示业务概念，而一部分业务逻辑也只对这样的一组对象起效。但是在所有的关联关系中，聚合是最重要的一类。它表明了通过聚合关系连接在一起的对象，从概念上讲是一个整体。

以此来看，当我们在这个例子里，谈到 User 是 Subscription 的聚合根时，实际上我们想说的是，在表达“用户订阅的专栏”时，User 与 Subscription 是一个整体。如果将它们拆分，则无法表示这个概念了。同样，计算订阅专栏的总价，也只是适用于这个整体的逻辑，而不是 Subscription 或 User 独有的逻辑。

总结来说，我们无法构造一个没有 User 的 Subscription 对象，也就是说这种概念在软件实现上的映射，比起“贫血模型”的实现方式，“富含知识的模型”将我们头脑中的模型与软件实现完全对应在一起了——无论是结构还是行为。

这显然简化了理解代码的难度。只要我们在概念上理解了模型，就会大致理解代码的实现方法与结构。同样，也简化了我们实现业务逻辑的难度。通过模型可以解说的业务逻辑，大致也知道如何使用“富含知识的模型”在代码中实现它。

修改模型就是修改代码

关联模型与软件实现，最终的目的是为了达到这样一种状态：修改模型就是修改代码；修改代码就是修改模型。

在知识消化中，提炼知识的重构是围绕模型展开的。如果对于模型的修改，无法直接映射到软件的实现上（比如采用贫血模型），那么凝练知识的重构循环就必须停下来，等待这个同步的过程。

如果不停下来等待，模型与软件间的割裂，就会将模型本身分裂为更接近业务的分析模型，以及更接近实现的设计模型（Design Model）。这个时候，分析模型就会逐渐退化成纯粹的沟通需求的工具，而一旦脱离了实现的约束，分析模型会变得天马行空，不着边际。如下所示，分析模型参与需求，设计模型关联实现：

事实上，这套做法在上世纪 90 年代就被无数案例证明难以成功，于是才在 21 世纪初有了模型驱动架构（Model-Driven Architecture）、领域驱动设计等一系列使用统一模型的方法。那么，在模型割裂的情况下，统一语言与提炼知识循环也就不会发生了，所以我们才必须将模型与软件实现关联在一起，这也是为什么我们称它是知识消化的基础与前提。

你或许会有疑惑，“富含知识的模型”的代码貌似就是我们平常写的代码啊！是的，随着不同模式的 ORM 在 21 世纪初期相继成熟，以及面向对象技术大规模普及，将领域模型与软件实现关联，在技术上已经没有多大难度了。虽然寻找恰当的聚合边界仍然是充满挑战的一件事，但总体而言，我们对这样的实现方式并不陌生了。

由此我们可以更好地理解，DDD 并不是一种编码技术，或是一种特定的编码风格。有很多人曾这样问我：怎么才能写得 DDD 一点？

我一般都会告诉他，只要模型与软件实现关联了，就够了。

毕竟“DDD 的编码”的主要目的是不影响反馈的效率，保证凝练知识的重构循环可以高效地进行。如果不配合统一语言与提炼知识循环，那么它就只是诸多编码风格之一，难言好坏。

而如果想“更加 DDD”的话，则应该更加关注统一语言与提炼知识循环，特别是提炼知识循环。事实上，它才是 DDD 的核心过程，也是 DDD 真正发挥作用的地方。

小结

领域驱动设计是一种领域模型驱动的设计方法，它强调了在业务系统中应该使用与问题领域相关的模型，而不是用通用的数据结构去描述问题。这一点已被行业广泛采纳。

Eric Evans 提倡的知识消化，总结起来是“两关联一循环”：模型与软件实现关联；统一语言与模型关联；提炼知识的循环。

知识消化是一种迭代改进试错法，它并不追求模型的好坏，而是通过迭代反馈的方式逐渐提高模型的有效性。这个过程的前提是将模型与软件实现关联在一起。

这种做法在 21 世纪初颇有难度，不过随着工具与框架的成熟，也成为了行业熟知的一种做法。于是，通过迭代反馈凝练知识就变成了实施 DDD 的重点。

不过，在进入这部分之前，我们还要看看如何将统一语言与模型关联起来，这个我们下节课再深入讨论。

思考题

既然领域驱动设计是一种模型驱动的设计方法，为什么不能让业务方直接去使用模型，而要通过统一语言？这是不是有点多余？

欢迎把你的想法和思考分享在留言区，和我一起交流。相信经过你的深度思考，知识会掌握得更牢固。

02｜统一语言是必要的吗？

你好，我是徐昊。今天我们来聊一聊领域驱动设计中的统一语言与模型的关联。

在上一讲，我们介绍了领域驱动设计的核心理念，即在业务系统中应该使用与问题域相关的模型，而不是通用的数据结构去描述问题。并由此介绍了 Eric Evans 提倡的知识消化，总结起来就是“两关联一循环”。

我们讲了第一个关联，那就是模型与软件实现的关联，并解释了为什么它是实践知识消化的前提。那么今天我们来讲第二个关联：统一语言与模型关联，也就是从模型中提取统一语言。

不过在讲具体做法之前，我们要先思考一下，为什么需要统一语言，以及统一语言会带来哪些好处。

统一语言是基于领域模型的共同语言

统一语言（Ubiquitous Language）是一种业务方与技术方共同使用的共同语言（Common Language），业务方与技术方通过共同语言描述业务规则与需求变动。可以说，共同语言为双方提供了协作与沟通的基础。注意，这里的业务方泛指一切非最终软件实现者，他们可能有很多名字：客户、产品、业务、业务分析师、解决方案架构师、用户体验设计师等等。

共同语言也有很多种形式。比如，用户画像（User Persona）与相关的用户旅程（User Journey） ，就能从流程角度有效地构成共同语言。再比如，数据字典（Data Dictionary）在很长的时间里，也是从软件实现侧形成共同语言的依据。

不过领域驱动设计中的统一语言，特指根据领域模型构造的共同语言。这也是为什么要为它特意构造一个专有且生僻的词汇 Ubiquitous Language，来和我们一般意义上的共同语言作出区分。

我们知道，领域驱动设计是一种模型驱动的设计方法。那么在理论的理想情况下，模型本身就应该是业务方和技术方讨论需求的基础（如下图所示）。照此来看，这是不是就意味着统一语言是多余的呢？

通过图示可以看出，直接使用模型作为统一语言在实际操作中效果并不理想。

我们可以先从业务方和技术方各自所站的视角来分析一下。

如果站在研发人员的角度，直接使用模型有诸多好处。但是如果将视角切换到业务方，模型就不够直观了，业务方难以直接把模型和他们对系统的理解关联到一起。这主要是因为业务方看待系统的角度与开发人员不同。

业务方大多习惯从业务维度（Business Perspective），比如流程、交互、功能、规则、价值等出发去描述软件系统，这是业务方感知软件系统的主要途径。而模型则偏重于数据角度，描述了在不同业务维度下，数据将会如何改变，以及如何支撑对应的计算与统计。那么，业务的维度就被模型的抽象隐藏了，业务方是无法从模型中直接感知到业务维度的。

此外，模型是从已知需求中总结提炼的知识，这就意味着模型无法表达未知需求中尚未提炼的知识。但是，单纯使用模型一定会有无法表述的需求，因而我们需要一个相对允许歧义与未知的隔离层，来帮助我们发现和反馈模型的不足。

更确切地说，我们需要一种能与模型关联的共同语言，它既能让模型在核心位置扮演关键角色，又能弥合视角差异，并提供足够的缓冲。

从模型中提取的统一语言，覆盖了领域模型中的概念与逻辑，还提供必要的补充，以帮助业务方理解模型。同时，统一语言也扮演了试验田的角色，其中出现的未被提取的知识，将作为触发提炼知识的循环，逐步完善模型。因而，如下图所示，我们会在提炼知识的循环中使用统一语言，而不是模型。

再进一步说，不直接使用模型，也是领域驱动方法对其他模型驱动方法的反思。同样兴起于 20 世纪末 21 世纪初的模型驱动架构（Model Driven Architecture），就建议业务人员直接使用模型描述需求，然而并没有取得成功。因为相对于模型的精确，统一语言的模糊反而更能满足人与人之间交流的需求。

因而，虽然理论上说可以不需要，但统一语言并不是多余的，它源自一线实践者的经验，是一种实证的智慧。

当然，经过了多年实践之后，最终我们还是借由不同的建模方法，能够几乎仅仅使用模型作为统一语言了，我会在第 7-9 讲为你介绍。不过在那日到来之前，我们还需要知道，统一语言带来的好处与改变，怎么提取统一语言，以及现在它是个什么样子。

修改代码就是改变统一语言

我们在讲关联模型与软件实现时，提到它的最终目的是达到这样一种状态：修改模型就是修改代码；修改代码就是修改模型。要知道，统一语言是从领域模型中提取的，包含了领域模型中的概念与逻辑。那么改变了模型，实际上也就改变了统一语言。

于是，修改代码就是修改模型，改变模型就是改变统一语言，修改代码等于改变统一语言。这是一个强而有力的推论，因为它描述了这样一种可能的场景：不是因为需求变更，而是因为代码重组与重构引起的代码修改，最终会反映到统一语言中，反映到我们应该如何理解并沟通的需求上去（如下图所示）。

这相当于让开发方来定义业务问题，并要求业务方按照开发提出的模型来描述业务和需求。这听起来匪夷所思，但实际上这种情况每时每刻都在上演，只是身在其中的人并不自知而已。

想想看，一旦将软件实现与领域模型关联，那么对实现的简化，也就是对领域问题的简化；从实现中抽取的抽象概念，也就是从问题域中抽取的抽象概念。

通常的情况是，技术方通过代码不断修改实际的业务模型，但这种变化很难有效传递给业务方，于是这种变化就成为了“隐秘的角落”。同时，也很难让业务方以技术方改变后的业务概念去思考问题。最终导致双方互看不爽，分歧越来越大。

因此，看起来匪夷所思的情况，实际上一直都在发生。技术方一直都在定义业务，只是没有合理的途径让业务方了解并接纳而已。

不过统一语言及其背后的领域模型从观念上改变了这一状况，它将大家从各自的领地，也就是业务与技术中，拉到了一个中间地带。统一语言与领域模型既不完全属于业务，也不完全属于技术，而是双方共有的部分。于是技术方与业务方就有了差不多的话语权，至少有了可以沟通和协同的空间。

一旦业务方接受了统一语言，实际上就是放弃了对业务 100% 的控制权，也意味着统一语言在业务上能够赋予开发人员更大的控制权。这或许是出于 Eric 的故意设计，抑或是源于 Eric 对敏捷价值观的深刻认同，在有限的几次接触中，我并没有跟他求证过。但可以肯定的是，统一语言实际上赋予了技术人员定义业务的权利。

你可能会有疑问，业务人员放弃对业务的 100% 控制权话语权不是一件坏事吗？

正如前面所讲，在实际工作中，修改代码引起模型改变无可避免。因而无论业务人员是否愿意，他们对业务其实都没有 100% 的控制权。差别仅仅在于业务人员是否知晓这种改变，是否有机会验证这种改变在合理的方向上。

可以说，统一语言给予了双方一种沟通和协商的途径。只不过，统一语言在给技术方带来额外的权利同时，也隐含着额外的义务，即借由统一语言，在提炼知识的循环中，接受业务方的监督与反馈。如果业务方不同意技术方修改的模型，那么在后续提炼知识的循环中，可以修正这种误差，也就是技术人员也丧失了对代码 100% 的控制权。

看到这儿，你可能就明白了。除了将“隐秘的角落”公之于众外，模型还通过统一语言将开发视角的介入业务领域，为业务方带来有益的补充。毕竟跨行协作是创新的土壤，不是吗？

总结来说，统一语言提供了一种更好的协同方式的可能性。在业务方大多强势的环境中，难能可贵地建立了技术反馈业务的途径，降低了知识消化过程失败的风险。

听了这么多好处，我猜你肯定希望看到一个例子，以便直观地理解什么是统一语言。那么我在这里就展示一个简单的例子。

一个简单的统一语言提案

统一语言本身的形式并不重要，或者说统一语言并没有统一的形式，它甚至可以是任意一种形式。但是，当且仅当，统一语言与领域模型关联，且多方认可并承认对统一语言的集体所有权时，统一语言才能成为真正的统一语言。

统一语言必须是团队的共识，是团队成员愿意在工作中使用的语言。在经由那个神圣的社交时刻（social moment）之前，我更愿意将它称作统一语言提案，以示与真正的统一语言的差别。毕竟在业务方与技术方都认可之前，它并不是统一语言。

统一语言可以包含以下内容：

源自领域模型的概念与逻辑；

界限上下文（Bounded Context）；

系统隐喻；

职责的分层；

模式（patterns）与惯用法。

我们仍然以极客时间专栏的模型为例，看看能提取出怎样的统一语言。由于目前这仍然是个很简单的模型，提取出来的语言可能无法包含上述所有内容，但能帮助你理解它，这就足够了。

根据这个模型，我们可以从中提取到对应的领域概念，也就是领域模型中的实体：

用户（User）是指所有在极客时间注册过的人；

订阅的专栏（Subscription）是指用户付费过的专栏。

同时由于 User 与 Subscription 之间的关系，我们可以提取一个业务逻辑：

用户可以订阅多个专栏。

除了源自领域逻辑的核心概念之外，界限上下文、系统隐喻等其他几项都可以看作对业务维度的补充与展开。将它们引入通过统一语言后，可以帮助业务方更直观地理解模型。

比如系统隐喻就是在价值与业务模式维度上的补充与扩展。我们经常会听到某某产品说，我们要做某某领域的淘宝，或者我们是某某行业的滴滴。这样言简意赅地表示了产品的愿景，也就是价值定位与核心模式。这些信息也应该放进统一语言中。类似的，责任分层关注“稳定性”，哪些是稳定而哪些是易变的；模式与惯用法是业务规则、流程与实现模式。

稍微需要额外提及一下的是界限上下文（Bounded Context）。界限上下文是围绕某些模型设置的边界。所有人对于如何利用边界中的模型有清晰明确的想法，这个想法借由这个边界保持一致，不受外界信息的干扰。

这个定义稍微有点不好理解，实际上界限上下文是现实中某种决定在模型上的反映。它可以是源自业务领域本身的，这时候界限上下文的范围就和子域（Subdomain）等概念所表示的范围重合了。比如，我们说这是订阅子域，因而存在一个叫做订阅的界限上下文。我们可以将界限上下文的名字放入统一语言：

订阅。

此外，界限上下文还可以表示其他一些有的没的。比如，如果用户（User）源自另外一个服务，或者遗留系统，我们没法修改它。于是我们可以建立一个界限上下文，叫“动不了”，用来解释为什么会做出一些古怪的设计决定：“因为有的地方动不了”。我们需要在统一语言中加入这个界限上下文的名字，因为当我们说到“动不了”的时候，可能会指代这个上下文。

总的来说，界限上下文是万能的，应用之妙存乎一心，关键在于你会不会用。我建议你把它当个筐，不好解决的问题都扔进去就好了。

言归正传，我们现在抽取的语言是这样的：

用户（User），指所有在极客时间注册过的人；

订阅的专栏（Subscription），指用户付费过的专栏；

用户可以订阅多个专栏；

订阅。

通过定义与解释，我们使这些词语在其所使用的上下文中没有歧义。再通过这些基础词汇，去描述业务的行为或者规则，慢慢就可以将其确立为跨业务与技术的统一语言了。要始终记得，统一语言是在使用中被确立的。

那么这个语言该怎么用呢？除了在日常沟通中口头使用之外，比如还可以通过它来编写用户需求（以用户故事的形式）：

作为一个用户（User），当我查阅购买过的专栏（Subscription）时，从而可以看到其中的教学内容。

或是用它来描述行为驱动开发（Behaviour Driven Development）的测试：

当用户（User）已购买过了某个专栏（Subscription），那么当他 / 她访问这个专栏时，就不需要再为内容付费。

或是实例化需求（Specification by Example，SbE）的说明，等等。

重点是，在所有可能的地方使用这个语言。只有当所有工种角色都接受它，使用它去描述业务和系统的时候，它才会真正成为统一语言。

小结

我们来简单地总结一下。统一语言特指根据领域模型构造的业务方与技术方都使用的共同语言。虽然在理想中，我们希望直接使用模型作为统一语言。但从实际出发，直接使用模型的效果并不好。

主要是两点：

模型将业务维度隐藏了，对业务方显得不够直观；

对于未提取的知识，超出了模型的表达能力。因而统一语言是非常必要的。

需要强调的是，统一语言提供了一种更好的协同方式的可能性。统一语言与其背后的领域模型赋予了研发人员通过重构定义业务的能力，在业务方大多强势的环境中，难能可贵地建立了技术反馈业务的途径，降低了知识消化过程失败的风险。

到这里，我们的“两关联一循环”就还剩最后一个环节：凝练知识的循环。我们下节课再见。

思考题

通过两节课的介绍，我想你大概对于知识消化与领域驱动设计有了自己的理解，那么你会怎么跟别人介绍领域驱动设计？你认为领域驱动设计到底是什么呢？

欢迎把你的思考和想法分享在留言区，我会和你交流。同时，我也会把其中不错的回答在留言区置顶，供大家学习讨论。

03｜我们要怎么理解领域驱动设计？

你好，我是徐昊。今天我们来聊聊领域驱动设计中的提练知识的循环。

在上一讲，我们介绍了什么是统一语言，讲了它为什么是领域驱动设计的必要实践，以及为什么统一语言提供了一种更好的协同方式的可能性。

在回答这些问题的过程中，我们强调了统一语言和它背后的领域模型，赋予了研发人员通过重构定义业务的能力。这是从技术方的角度来理解的统一语言。那么从业务方的角度来看，要如何利用统一语言去影响研发方呢？

那就要谈到“两关联一循环”里的“一循环”了：提炼知识的循环。它是领域驱动设计的核心流程。

将提炼知识的循环看作开发流程

上节课已经说过，提炼知识的循环大致是这样一个流程：

首先，通过统一语言讨论需求；

而后，发现模型中的缺失或者不恰当的概念，然后精炼模型，以反映业务的实际情况；

接着，对模型的修改会引发统一语言的改变，再以试验和头脑风暴的态度，使用新的语言以验证模型的准确。

如此循环往复，不断完善模型与统一语言。示意图如下：

如果我们仔细思考一下上面的场景，会发现当模型中出现不恰当的概念时，提炼知识的循环就和重构（Refactoring）的过程有诸多相似之处：

发现坏味道（Bad Smells）以明确改进方向：头脑风暴与试验，通过统一语言描述需求，发现模型中存在不恰当不准确的概念；

尝试消除坏味道以改进目前状况：修改模型，提炼知识到模型中；

通过坏味道是否消失判断改进是否成功：提取统一语言，头脑风暴与试验，验证新的模型是否准确。

而发现存在缺失概念的过程，也与测试驱动开发（Test-Driven Development）的过程相去不远：

构造一个失败测试表明需求的变化：头脑风暴与试验，是否存在统一语言无法描述的业务；

修改代码实现：修改模型，提炼知识到模型中；

通过测试以证明需求变化完成：提取统一语言，头脑风暴与试验，验证新的模型是否准确。

你看，无论我们将提炼知识的循环看作通过统一语言对模型的重构，还是通过统一语言对模型的测试驱动开发，我们都可以把它看作以模型为最终产出物的研发流程。如果是重构，业务方就是那个发现坏味道的人；而如果是测试驱动开发，业务方就是构造测试的那个人。

让我们看一个例子，以便对这个流程有个更直接的认识。比如在上一讲极客时间的例子里，我们的统一语言是：

用户（User）是指所有在极客时间的注册过的人；

订阅的专栏（Subscription）是指用户付费过的专栏；

用户可以订阅多个专栏；

订阅。

并使用这个语言描述了需求：

作为一个用户（User），当我查阅购买过的专栏（Subscription）时，从而可以看到其中的教学内容。

当用户（User）已购买过了某个专栏（Subscription），那么当他 / 她访问这个专栏时，就不需要再为内容付费。

我们可以听到用户反复提及专栏与内容。而在我们的语言中，只存在购买过的专栏（Subscription）。如果业务方想要描述用户没有购买的专栏，该怎么办？比如有这样一个需求：

作为一个用户（User），当我对某个专栏的内容感兴趣时，我可以购买这个专栏，使其成为我购买过的专栏（Subscription）。

于是在这条业务描述中，就出现了我们无法表达的含义：专栏和内容。这表示着我们的统一语言存在无法表达的业务，我们的模型存在漏洞。

当然，想弥补漏洞也是很简单的。我们可以联合团队一起进行头脑风暴，提取缺失的概念，修改已有的模型，并由业务方判断这个修改后的模型是不是足够准确地表达了业务概念与逻辑。那么最后，我们可能会得到这样一个新的模型：

然后我们可以按照第二讲讲过的内容，从领域对象与逻辑、界限上下文、系统隐喻等角度，重新提取统一语言。这里，我们假设在模型上又提取了课程发布的界限上下文。于是我们的统一语言就变为一个更丰富的形式：

用户（User）是指所有在极客时间注册过的人；

专栏（Column）是一组付费内容（Content）的集合，由极客时间签约的作者（Author）提供；

付费内容（Content）是课程的承载，可以是文字、视频或者音频；

作者（Author）是极客时间寻找的在某些领域有经验与专长的实践者；

订阅的专栏（Subscription）是指用户付费过的专栏；

用户可以订阅多个专栏；

专栏中可以包含多个付费内容；

同一作者可以发布多个专栏；

订阅；

课程发布。

于是之前无法被描绘的业务逻辑变成了这样：

作为一个用户（User），当我对某个专栏（Column）的内容（Content）感兴趣时，我可以购买这个专栏，使其成为我购买过的专栏（Subscription）。

在这样的业务描述中，就没有统一语言之外的概念了。至此，我们就通过修改领域模型，改变了统一语言，结束了提炼知识的循环。然后我们就可以根据修改后的模型，去改变对应的软件实现。这样，就可以保证我们的模型与软件实现在提炼知识的循环之后，仍然是关联的。

研发方与业务方的协同效应

如果我们将提炼知识的循环看作以模型为最终产出物的研发流程，那么此时可以再考虑一下模型与软件实现的关联：对代码的修改即是对模型的修改；对模型的修改也就是对代码的修改。

要知道，提炼知识的循环最终会反映到具体的软件实现上，那么业务方实际也就更深入地参与到了研发流程中。如果说统一语言与模型的关联赋予了技术方定义业务的权利，那么提炼知识的循环也就赋予了业务方影响软件实现的权利。

讲到这儿，我们就可以综合统一语言，从一个更全面的角度来看待双方的权利与义务。

对于技术方来说，通过统一语言获得了定义业务的权利，但是同时也必须承担在提炼知识的循环中接受业务影响实现的义务；

对于业务方来说，提炼知识的循环赋予了他们影响软件实现的权利，同样也有接受研发方通过统一语言定义业务概念的义务。

就这样，知识消化以一种权责明确的方式，让业务方与技术方参与到对方的工作中。同时也在整体上，给予了业务方和技术方一种更好的协同方式。

你可能会问，为什么让业务方与技术方参与到对方的工作中，就是一种更好的方式呢？首先，软件开发的核心难度就在于处理隐藏在业务知识中的复杂度。

想要处理这种复杂度，首先需要打破知识壁垒（统一语言），如果双方对彼此的知识域有基础的了解（模型），那么知识传递与沟通的效率就可以变得更高。

其次，对于复杂的问题，需要快速的反馈周期（提炼知识的循环）试错。复杂的问题也就是没有现成答案的问题，只能快速试错。双方参与到彼此工作中，在流程中就可以给予对方反馈，而不是等到产生最终结果之后再评价。那么反馈速度自然就更快捷，同时浪费（研发时间、无效代码）也更少。

因而，让业务方与技术方参与到对方的工作中，就在双方之间带来了更好的协同效应（Synergy Effect），也就是 1+1>2 的效果，这正是知识消化最吸引人的地方。

那么，在我们完全了解了知识消化的“两关联一循环”之后，你会发现它们是一个有机的整体：

统一语言与提炼知识循环重新定义了业务方与技术方的权责，并倡导了一种更好的协同方式；

模型与软件实现关联是保证这种协同方法的前提条件和实现保证。

三方中缺少任何一个，“知识消化”都会失败。

所以我们要怎么理解领域驱动设计？

至此，我相信你对领域驱动设计以及“知识消化”法就有了相当的认识和了解。然而只要你搜一搜网上的讨论，很快又会陷入到深深的疑问中：我到底懂没懂？为什么别人说的跟我想的不太一样呢？

我们知道，“领域驱动设计”至少可以指代一种建模法，一种协同工作方式和一种价值观，以及上述三种按照随意比例的混合。因此，为了不让自己陷入诸多疑问的泥沼，我们还需要明辨“当我们谈论领域驱动设计的时候，我们到底在说什么”。

迭代式试错建模法

在理解了“两关联一循环”之后，也就理解了我们最开始提及的知识消化是一种迭代式试错法。然后你就会惊奇地发现，领域驱动设计作为一种名字里有设计方法，本身却很少谈及具体的模型设计，反而更偏向依靠交互来协同试错。

那么我们不禁要问，怎么说来说去都没有提到“怎么才能保证模型的成功”这个问题呢？

比如刚才讲解的极客时间专栏的例子，同样是发现专栏（Column）与内容（Content）的缺失，那么我们可以将订阅（Subscription）看作专栏的一个特殊状态。于是，似乎如此来建模也可以接受：

我们始终要记住，模型是对问题的抽象，没有对错，只有角度不同。上图里，我们把它当作一个对象的不同的状态。而在之前的例子里，我们把 Subscription 当作一种关联对象（Association Object），表示某个动作，也就是“订阅 Subscribe”的结果。我们还可以沿着这个思路建模：

那到底哪个模型更好呢？不好说，要看在具体需求上哪一个能更好地应对变化。

当然这也是所有试错法都存在的问题。正如 Eric Evans 自己所说：“知识消化是一个探索的过程，你不可能知道你将会在哪里停止。”其实后面还有他没直接说的一句话：“你可能还不知道当你停止时，得到的是垃圾还是宝藏。”这部分只能交给建模者的抽象能力，然后希冀一个好的结果。

总结来说，领域驱动设计之所以变成了如此不靠谱的建模法，是因为它尝试解决的问题域是复杂问题，也就是没有现成答案的问题。那么迭代式试错法就是唯一“可行（Viable）”的方式。

具有协同效应的工作方式

在这节课，我们花了大量的篇幅来解释为什么统一语言与提取知识的循环可以构成一种具有协同效应的工作方式。

简单总结一下，首先是权利与义务的对等构成了协同的基础。技术方通过统一语言获得了定义业务的权利，但是同时也必须承担在提炼知识的循环中接受业务影响实现的义务；而业务方呢，通过提炼知识的循环获得了影响软件实现的权利，但是同样，也有接受技术方通过统一语言定义的业务概念的义务。

在这种基础之上呢，权责明确的方式就形成了一种没有绝对主导的合作关系，让业务方与技术方都能参与到对方的工作中，从而产生 1+1>2 的协同效应。

不过这种协同方式是否能够发挥作用，很大程度上依赖于团队对模型，以及与其对应的统一语言的理解与接纳。这部分就依赖于建模者的变革管理能力。正如我们前面看到的，建模者可以很容易地从模型中提取统一语言提案，只要给所有概念明确的定义和边界就可以了。

而能否真正地变成统一语言，则需要团队接纳并逐步在工作中使用它。这是行为改变，需要变革管理去推动。而知识消化希望通过头脑风暴与试验的方法，简化这种变革。不过实际结果，仍然是因人而异的，没办法保证百分之百的成功。

价值观体系

如果我们看一下领域驱动设计实践背后的价值观，那么会发现：

领域驱动设计是一种模型驱动的设计方法：模型应处在核心；

两关联一循环：业务与技术围绕着模型的协同。

只要接纳这两条价值观的都可以声称自己采用了“领域驱动设计”。从这个角度来说，“领域驱动设计”是一个如此宽泛的定义，以至于我们可以使用“领域驱动设计”去讨论任何事情。

这是一个完美的例子，告诉我们要慎重对待抽象。毕竟到了一定的抽象程度，差异就被完全隐藏了。因而我会更建议你使用“知识消化”来代替泛化的“领域驱动设计”，毕竟在知识消化中，除了“模型应处在核心”以及“业务与技术围绕着模型的协同”外，它还框定了具体的实践架构：两关联一循环。

所以当我谈论“知识消化”的时候，我们就可以更好地去探讨这些问题：怎么更好地构建“富含知识的模型”？如何持续保持模型与软件实现关联？怎么有效地提取领域语言？如何推动业务方更主动地参与提炼知识的循环？

因而在这个章节后续的课程中，当我谈到了“领域驱动设计”，如果没有特别强调，那么我所指的就是“知识消化”。

小结

“领域驱动设计”至少可以指代一种建模法，一种协同工作方式和一种价值观。作为建模法，领域驱动设计是迭代改进试错法。这是一种保底可行，但可能效率不高的建模方法。如果对所处理的领域没有更高的认知，那么使用它起码不会错，只要给予足够的时间，总会成功的。

作为一种协同工作方式，领域驱动设计提供的思路是相当精彩的，所以也对行业产生了深远的影响，特别是统一语言。

而作为价值观来讲，则过于宽泛了，领域驱动设计之滥觞大抵源于此吧。

对于领域驱动设计和知识消化，多年来行业也多有诟病，主要集中在这么三点：

第一，随着 ORM 的流行，关联模型与实现慢慢成为行业内的默认做法，于是大家的关注点也逐步过渡到怎样才能更好地建模。但是迭代试错效率真的不高，特别是技术方与业务方没有足够的信任的时候，可能就迭代不起来了。

第二，统一语言虽然赋能了技术侧，使其有了话语权，但是也给了技术人员不去学习业务的借口。比如技术可以硬拗很多奇怪的东西给业务方，以此来逃避对业务的真正学习。

第三，统一语言并不容易实现，技术人员不习惯去驱动这样的改变。而一旦无法形成真的统一语言，提炼知识的循环也就无法进行了，那么整个方法也就失败了。

除此之外，我们也可以发现，知识消化法其实更适合敏捷团队。无论是通过统一语言协同交互，还是提炼知识的循环，都需要对这种跨工种协同以及渐进式改进方式有足够的认同。在敏捷方法实施较好的团队，这些都不是问题。而在其他情况下，则会有较大的变革成本。

目前，领域驱动设计和知识消化法更多地被当作一种框架性方法来使用，而其中的具体的实践，在行业中也多有改进。这些我们后续将会讲到。

思考题

通过前三讲的学习，你可能已经发现了，知识消化是一个逻辑性很强的领域驱动设计方法，任何一个环节出现纰漏都可能造成整个过程的失败。那么在关联模型与软件实现这个环节上，你能想象会出现什么困难吗？如果是你，会怎么处理？

欢迎把你的思考和想法分享在留言区，我会和你交流。同时，我也会把其中不错的回答在留言区置顶，供大家学习讨论。

04｜跨越现实的障碍（上）：要性能还是要模型？

你好，我是徐昊。今天我们来聊聊通过关联对象（Assocation Object）建模聚合（Aggregation）。

在前面三节课，我们学习了领域驱动设计中的“两关联一循环”：模型与软件实现关联；统一语言与模型关联；提炼知识的循环。其中，统一语言与提炼知识的循环作为一种更为平衡的权责关系，促进了业务方与技术方更好的协作。而这一切又是以模型与软件实现关联为基础。

然而落地到实践中，关联模型与软件实现总有一些让人纠结与苦恼的地方。引起这些苦恼的主要原因是架构风格的变化。我们已经从单机时代过渡到了多层单体架构，以及云原生分布式架构，但我们所采用的建模思路与编程风格并没有彻底跟上时代的步伐，这种差异通常会以性能问题或是代码坏味道的形式出现。

如果我们想要真正发挥出领域驱动设计的作用，就需要在不同架构风格下，找到能够维持模型与软件实现统一的办法。这也是这个领域常看常新，总能产生新实践的原因。

因而接下来，我会用三节课来介绍一组实现模式，帮助我们应对从单机架构过渡到多层架构，保持模型与软件实现的关联。这些模式也是我们后面学习在微服务和云原生时代，实施领域驱动设计方法的基础。

今天这节课，我们就先从关联对象这一方法开始讲起。关联对象是一个古老的设计 / 分析模式，Martin Fowler 在《分析模式》中讨论过它。Peter Coad 将它视为一种常见的业务构成模式，并应用到业务分析中。而我大概从 2005 年开始，使用它建模领域驱动设计中的聚合与关联关系，以解决领域模型（Domain Model）中对技术组件的封装问题。

不过，在讲解到关联对象的具体做法之前，我们需要先看一下领域驱动设计中的聚合关系在具体实现中存在哪些问题。了解了问题所在，我们才能更有针对性地去寻找解决办法。

无法封装的数据库开销

在应用领域驱动设计的时候，聚合与聚合根（Aggregation Root）是构成“富含知识的模型（Knowledge Rich Model）”的关键。通过聚合关系，我们可以将被聚合对象的集合逻辑放置于聚合 / 聚合根里，而不是散落在外，或是放在其他无关的服务中。这么做可以使得逻辑富集于模型中，避免“逻辑泄露”。

不过落到具体实现上，我们经常会遇到这样一个挑战，即：这些被聚合的对象，通常都是被数据库持久化（Persistent）的集合（Collection）。也就是说，数据库系统的引入，网络 I/O 与其他性能开销无法被接口抽象隔离。而将具体技术实现引入领域模型，则有悖领域驱动设计的理念。

接下来，我们就通过一个例子，在具体的上下文中看一看存在什么问题。然后，再看一下这个问题在建模思路上产生的根源，以及要如何修正我们的建模思路。

集合遍历与 N+1

让我们再回到在第二讲里展示的极客时间专栏的例子：

class User {

   private List<Subscription> subscriptions;

   // 获取用户订阅的所有专栏
   public List<Subscription> getSubscriptions() {
     ...
   }

   // 计算所订阅的专栏的总价
   public double getTotalSubscriptionFee() {
     ...
   }
}

class UserRepository {
  ...
  public User findById(long id) {
    ...
  }

按照面向对象和领域驱动设计提倡的做法，User 作为聚合根，需要管控其对应的 Subscription。现在我们有一个简单且常见的需求：在页面上，显示对用户已经订阅过的专栏进行分页处理。那么，我们大概会这么写：

user.getSubscriptions().subList(from, from + pageSize);

可以发现，在这段代码里，我们假设所有的 Subscription 都在内存里，因此所有的计算与逻辑都可以在内存中完成了。那么，我们就需要在读取 User 的时候，将它对应的所有订阅过的专栏信息都从数据库读取到内存中，以便后续使用。

如果是少量订阅的场景，比如几个或者几十个，那这段代码并没有什么问题。但如果有个特别好学的人，买了几万甚至几百万个专栏呢（先假设专栏有无限个）？此时将所有订阅过的专栏都读取到内存里，这就意味着会有巨大的网络 I/O 开销和内存占用。

当然，这里你可能会说，JPA/Hibernate 等 ORM 提供了延迟加载啊。是的，但这又会引入经典的性能瓶颈 N + 1 问题。因为随着延迟加载集合的遍历，其中的 Subscription 对象会被依次加载。

延迟加载的实现流程是这样的：

先执行一条查询获取集合的概况。比如总共有多少条记录之类的信息。

然后根据概况信息，生成一个集合对象。这时候集合对象基本上是空的，不占用什么内存空间。

随后，当我们需要集合内的具体信息的时候，它再根据我们需要访问的对象，按需从数据库中读取。

理论上讲，这是为了避免一次性读入大量数据带来的性能问题，而提出的解决办法。

然而，如果需要获取所有的数据，那么我们总共就会有 N+1 次数据库访问：1 次是指第一次获取概况的访问，N 次指而后集合中 N 个对象每个一次。而每一次加载，都伴随着对数据库的访问，自然就会带来 I/O 与数据库的开销。特别是频繁地对数据库访问，可能会阻塞其他人，从而造成性能瓶颈。

在这种情况下，我们其实没有什么好的选择：要么是一次性读入全部数据，避免 N + 1 问题；要么是引入 N+1 问题，变成钝刀子割肉。

为了避免这两种情况，很自然地，你会想起这样一种做法：通过查询语句一次性定位所需分页，这样只需要一次查询就能解决问题。不过这么做的困难在于，分页查询的逻辑要放在哪个对象上，才能保持模型与软件实现的关联。

一种做法是为订阅（Subscription）构造一个独立的 Repository 对象，将逻辑放在里面（也是 Spring 推荐的做法）：

interface SubscriptionRepository extends ... {
  public Page<Subscription> findPaginated(int from, int size);
}

这种做法的问题就是会导致逻辑泄露。Subscription 被 User 聚合，那么 User 所拥有的 Subscription 的集合逻辑应该被封装在 User 中，这样才能保证 User 是“逻辑丰富的模型”，因为非聚合根提供 Repository 是一种坏味道。

如果聚合到 User 上是可行的吗？其实也不行。因为这么做会将技术实现细节引入领域逻辑中，而无法保持领域逻辑的独立。代码如下：

public class User {
    public List<Subscription> getSubscriptions() {
      ....
    }

    public List<Subscription> getSubscriptions(int from, int size) {
       return db.executeQuery(....);
    }
 }

造成这种两难局面的根源在于，我们希望在模型中使用集合接口，并借助它封装具体技术实现的细节。因为在我们的概念中，内存中的集合与数据库是等价的，都可以通过集合接口封装。但实际情况是，我们无法忽略数据库带来的额外作用与开销，内存中的集合与数据库并不是等价的，封装失败。

那么，为什么在我们的概念中，我们会认为内存中的集合与数据库是等价的，是可以通过集合接口封装的呢？这就要从面向对象技术的开端——Smalltalk 系统说起了。

Smalltalk 中集合与数据库是等价的

鲜少有人了解的一个事实是，集合与数据库的等价，在早期面向对象系统 Smalltalk 中是一个基础概念。

这其实不难理解，如果对象中包含的状态可以映射为数据库表中的一列，那么一组对象自然就可以映射成一张表了。有了一张表，自然也就可以看作是一个最小的数据库了。

Smalltalk 作为面向对象系统内置的一套面向对象数据库（Object Oriented Database）。于是在使用 Smalltalk 系统时，集合与数据库是无差别的。如下图所示，展示了 Smalltalk 系统的构成。

如上图所示，Smalltalk 中除了虚拟机（Virtual Machine）之外，还有虚拟镜像（Virtual Image，VI）。虚拟镜像相当于虚拟机内存的持久化保存。每次虚拟机启动的时候，都会把虚拟镜像中的信息读入到内存中以恢复状态，而 Java、.NET 虚拟机并不会保留之前内存中的信息。

打个比方。Smalltalk 虚拟机的启动过程，类似于 VMWare 虚拟机上的 Pause，内存中的所有状态都被持久化保存了，那么我们可以继续从之前暂停的地方开始。而其他语言的虚拟机则是彻底地重启（Restart），并不会保留之前的结果。

在 Smalltalk 的虚拟镜像中，存储着所有的代码和数据。我们称呼 Smalltalk 为一个面向对象系统，而不仅仅是面向对象语言。因为它既是一门面向对象语言，也是一个虚拟机的操作系统，还是一个面向对象数据库。

那么在这样的系统中，Smalltalk 中的集合（无论是 Array、List，还是什么）就等同于一个数据库。只要这个集合被创建出来，除非显示化地通过垃圾回收销毁，否则它就会被虚拟镜像持久化。垃圾收集和持久化一样，都可以被看作是对象生命周期（Object Lifecycle）的一个状态。

因此，数据库中的数据和非数据库中的数据，都可以通过 Collection 表达，我们不用再去区分它。而在 Smalltalk 中，这种等价关系深刻影响了我们的思路和建模习惯，毕竟我们所熟知的面向对象语言都从 Smalltalk 中借鉴了大量的概念。而早期的建模者也大都来自 Smalltalk 社区，比如 Peter Coad、Kent Beck、Martin Fowler 等（当然我也是）。所以我们学习建模方法的时候，或多或少有一些源自 Smalltalk 社区的习惯。

然而随着时代的发展，这种习惯在多层架构（Mulit-tier Architecture）下遇到了挑战。那么接下来我们就看看多层架构是怎么彻底割裂了集合与数据库的。

多层架构彻底割裂了集合与数据库

Smalltalk 在中国未曾真正地流行过，大多数人是从 C++ 甚至是 Java、.NET 开始接触面向对象编程的。而无论是 C++、Java，还是后来的面向对象编程语言，其实都不是完整的面向对象系统。

集合作为面向对象中的重要概念，被吸纳入这些语言后，就去掉了与数据库的强关联（毕竟这些语言默认不带有数据库模块）。因此，集合虽然在概念上仍然等同于数据库，但如此简单粗暴的建模，就会遇到我们前面提到的“是否 N+1”的两难选择。

那么 Java、.NET 这些语言为什么要这么做呢？它们为什么不能像 Smalltalk 一样，内涵完整地面对象数据库呢？这其实是架构风格演化的结果。

我们明显可以发现，Smalltalk 是典型的单机单体架构。从操作系统、数据库到应用程序，都在一个虚拟机上，就好像是一台小型机一样。这样的架构结构无法水平扩展（horizontal scaling），只能垂直扩展（vertical scaling）。这就意味着我们需要不断提高单点的计算能力，才能提高整个的容量与吞吐。这并不符合架构的整体发展趋势。

进入 21 世纪后，随着对系统容量要求的增加，易于水平扩展的三层架构逐渐成为行业主流。而数据库作为一个独立组件，则不再与应用合体。示意图如下：

于是，Smalltalk 的面向对象系统在这种架构下分别变成了：应用程序中间件（对应 Smalltalk 虚拟机）和数据库（对应 Smalltalk 虚拟镜像的持久化数据部分）。

多层架构彻底割裂了对象集合与数据库，这对我们实现领域模型建模提出了挑战，对 Collection 逻辑的建模也就难以摆脱具体实现细节了。那就是我们必须明确哪些是持久化的数据，并对它的一些逻辑区别对待。这就是原味面向对象范型（Vanilla Object Oriented），在架构风格演化过程中遇到的挑战。

几乎所有实施过 DDD 的人，都在这个问题上挣扎过。如果对于如此常见且如此简单的分页功能，我们都无法将模型与软件实现关联的话。那么，面对更复杂的问题时我们要怎么办呢？这是大多数怀揣理想准备实施 DDD 的人，碰到的第一个阻碍。

关联对象就是一种解决这个问题的设计模式。

关联对象

关联对象，顾名思义，就是将对象间的关联关系直接建模出来，然后再通过接口与抽象的隔离，把具体技术实现细节封装到接口的实现中。这样既可以保证概念上的统一，又能够避免技术实现上的限制。

现在让我们再来看看极客时间专栏的例子，如果使用关联对象，如何帮我们避免 N+1 和逻辑泄露的问题。

使用关联对象实现聚合关系

首先我们需要定义关联对象，因为我们需要表达的是 User 与 Subscription 间的一对多的关系，那么最简单的命名方法是将两个对象名字组合，从而得到关联对象的名字 UserSubscriptions：

public interface UserSubscriptions extends Iterable<Subscription> {
   List<Subscription> subList(int from, int to); //分页
   double getTotalSubscriptionFee(); //获取总共花费
   int count(); //获取总订阅数
   Iterable<Subscription> sort(...);
   ....
}

当然，我们最好是从业务上下文出发，寻找更具有业务含义的名字，毕竟我们是要形成统一语言的，源自业务的名字总是更容易被非技术角色所理解。

比如这里我们要表达的是用户已经订阅的专栏，或者是用户已经购买的专栏，那么 PaidColumn，甚至 MySubscriptions，都不错，看哪一个更容易被团队接受了。我这里选择 MySubscriptions，于是 User 对象就变成了这样：

public interface MySubscriptions extends Iterable<Subscription> {
...
}

public class User {
  private MySubscriptions mySubscriptions;

  public MySubscriptions getMySubscriptions() {
     return mySubscriptions
  }
}

那么之前对于分页和计算的调用也就相应地变为：

user.getMySubscriptions().subList(0, 10);
user.getMySubscriptions().getTotalSubscriptionFee();

很明显，我们没有逻辑泄露，User 是 Subscription 的聚合根，那么与之相关的逻辑也仍然被封装在 User 的上下文中，当然是进一步被封装在关联对象中。

那么我们怎么解决持久化的问题呢？怎么从领域对象中，移除掉对技术实现的依赖呢？秘诀就在于接口与实现分离。代码如下：

package model.impl.db;

public class MySubscriptionsDB implements MySubscriptions {
  ...
  private User user;

  public List<Subscription> subList(int from, int to) {
    return db.executeQuery(...);
  }
  ...

在这里，我们将与数据库访问相关的逻辑毫不避讳地封装到 MySubscriptionsDB 中。不过，作为领域对象的 User 类，并不会知道它使用了数据库，因为它仅仅通过 MySubscriptions 接口，访问它所需要的功能。此时我们可以通过简单的分包的策略：

从上图中包的架构上看，模型的包中有 MySubscriptions、User、UserRepository。与数据库相关的代码的包里有 MySubscriptionsDB 和 UserRepositoryDB。于是，我们成功地将核心的领域逻辑与实现细节分开了。

当然最后还有一个问题：如何将 MySubscriptionsDB 与 User 结合在一起？最直接的做法就是这样：

public UserRepositoryDB implements UserRepository {
  ...
  public User findBy(long id) {
      User user = .....;
      return setMySubscription(user);
  }

  public List<User> findBy(....) {
      List<User> user = .....;
      return user.map(user -> setMySubscription(user));
  }

  private User setMySubscription(User user) {
      user.setMySubscriptions(new MySubscriptionDB(db, user));
      return user;
  }
}

因为 User 是聚合根，从数据库中读取的 User 对象都需要从 UserRepository 中获取。那么，在 UserRepository 的具体实现中为 User 对象设置 MySubscription 对象，是再自然不过的选择了。

当然更简洁漂亮的做法，是通过 Java CDI API 或是框架提供的生命周期实践监听器，来完成关联对象的设置。

隔离技术实现细节与领域逻辑

通过引入关联对象，我们可以更好地隔离领域逻辑与技术实现细节。在这里，我们还是用极客时间专栏的例子来解释。

如果这时候 Subscription 信息并不是存在数据库中，而是通过 RESTful API 从另一个系统中获取的。那么，我们只需提供另一个 MySubscriptions 的实现就可以了：

package model.impl.api;

public class MySubscriptionsAPI implements MySubscriptions {
  ...
  private User user;

  public List<Subscription> subList(int from, int to) {
    return client.findSubscriptions(....);
  }
  ...

这种改变并不会传递到领域逻辑层，对于分页和计算的调用仍然为：

user.getMySubscriptions().subList(0, 10);
user.getMySubscriptions().getTotalSubscriptionFee();

RESTful API 的性能瓶颈和需要调整的地方与数据库不同，这种变化都被关联对象的接口封装隔离了。

从面向对象编程的角度来说，我们很容易理解为什么关联对象可以带来如此多的好处。在诸多面向对象的最佳实践中，有一条是说要尽可能避免使用原始类型（primitive type）。因为原始类型没有对概念进行提取，也缺乏封装，所以我们应该尽可能地使用自定义的类去替换它们。

不过如果我们把语言内提供的集合类型（List 等）也当作原始类型的话，关联对象就是对这一条建议自然的扩展：使用自定义关联对象，而不是集合类型来表示对象间的关联。

通过集体逻辑揭示意图

关联对象除了可以帮助聚拢逻辑、隔离实现细节之外，还能从概念上帮助我们获得更好的领域模型，因为关联对象是对集体逻辑的直接建模。

所谓集体逻辑，是指个体不具备，而成为一个集体之后才具备的能力。哪怕是同一群个体，组成了不同的集体，就会具有不同的逻辑。

我们仍然回到极客时间专栏的例子上，来帮助你理解。比如我们现在的模型是这样的：

public class User {
   private List<Subscription> subscriptions;

   ....
}

public class Column {
    private List<Subscription> subscriptions;

    ....
}

在这段代码中，User 中的 List表示用户已订阅的所有专栏，而 Column 中的 List，则表示所有订阅了专栏的用户。虽然同为 Subscription 的集合，但是当它们组成集体时，在不同的上下文中则具有不同的含义。

那么如果显式地表达为关联对象，可以进一步澄清我们的意图，得到揭示意图的接口（Intention Revealing Interface）。代码如下：

public class User {

   public static interface MySubscriptions extends Iterable<Subscription> {
   ...
   }

   private MySubscriptions mySubscriptions;

   ...
}

public class Column {

    public static interface MyReaders extends Iterable<Subscription> {
    ...
    }

    private MyReaders myReaders；

    ..

在这段代码中，我们通过引入关联对象，可以将这两个不同的集体变更为 User.MySubscriptions 和 Column.MyReaders，然后在各自的上下文去定义不同的集体逻辑。

比如我订阅的专栏可以计算我一共付了多少钱，而在我的读者中，可以计算订阅者对专栏的平均打分情况，示意代码如下：

public static interface MySubscriptions extends Iterable<Subscription> {
  double getTotalFee();
}

public static interface MyReaders extends Iterable<Subscription> {
  double getAverageRating();
}

小结

我们来简单地总结一下。随着架构风格的演化与改变，面向对象技术中的集合，从与数据库完全等价，变成了与数据库完全割裂的东西。

然而在概念上，我们仍留有通过集合封装内存中的对象与数据库中的数据的习惯。这使得我们在使用领域驱动设计的时候，特别是使用聚合关系的时候，变得左右为难：要么放弃性能，获得更好的模型；要么泄露逻辑，以得到可接受的性能。

但是关联对象，则可以让我们在更加明确揭示意图的同时，去解决性能与逻辑封装的问题。我个人从很早就开始将关联对象作为实现聚合关系的默认方法了，它从未让我失望（it never fails me）。我建议你也试一试，这样我就不用给你解释什么叫惊喜了。

思考题

关联对象实际上是通过将隐式的概念显式化建模来解决问题的，这是面向对象技术解决问题的通则：永远可以通过引入另一个对象解决问题。那么在领域模型中还有哪些隐式概念呢？这些概念会给我们带来什么麻烦呢？

欢迎把你的思考和想法分享在留言区，我会和你交流。同时呢，我也会把其中一些不错的回答置顶，供大家学习。

05｜跨越现实的障碍（中）：富含知识还是代码坏味道？

你好，我是徐昊。今天我们来聊聊如何有效地分离领域模型中的上下文。

上节课我们介绍了关联对象，以及如何通过关联对象建模聚合 / 关联关系，从而在不影响性能的情况下，更好地维持领域模型与软件实现的统一。

今天，我们来看另一个在关联领域模型与软件实现时经常遇到的问题：上下文过载（Context Overloading）。所谓上下文过载，就是指领域模型中的某个对象会在多个上下文中发挥重要作用，甚至是聚合根。一来这个对象本身会变得很复杂，造成模型僵化；二来可能会带来潜在的性能问题。

于是，将过载的上下文进行有效的分离，就成了我们现在需要面对的问题。方法上也有很多不同的选择，我主要介绍角色对象（Role Object）和上下文对象（Context Object）这两种。这两个方法与我们上节课讲的关联对象一脉相承，都是通过将隐含概念显式建模来解决问题。

同样地，在看到具体的解决方法之前，我们需要深入了解上下文过载会产生哪些问题。

因富含逻辑而产生的过大类

为了帮助你理解上下文过载会产生哪些问题，我还是借助极客时间专栏的例子来说明一下。当然，首先我们需要扩展这个领域模型。

在这个扩展了的模型中，包含了三个不同的上下文。

订阅：用户阅读订阅内容的上下文，根据订阅关系判断某些内容是否对用户可见；

社交：用户维护朋友关系的上下文，通过朋友关系分享动态与信息；

订单：用户购买订阅专栏的上下文，通过订单与支付，完成对专栏的订阅。

按照这个模型，我们很容易得到与之对应的“富含知识的模型”：

public class User {
    private long id;

    // 社交上下文
    private List<Friendship> friends;
    private List<Moments> moments;

    // 订阅上下文
    private List<Subscription> subscriptions;

    // 订单上下文
    private List<Order> orders;
    private List<Payment> payments;

    // 社交上下文
    public void make(Friendship friend) {
      ...
    }

    public void break(Friendship friend) {
      ...
    }

    // 订单上下文
    public void placeOrder(Column column) {
      ...
    }

    // 订阅上下文
    public boolean canView(Content content) {
      ...
    }
}

如果对代码坏味道敏感的同学，估计已经发现问题所在了：一个对象中包含了不同的上下文，而这恰好是坏味道过大类（Large Class）的定义。

那么过大类会带来什么问题呢？首当其冲是模型僵硬。想要理解这个类的行为，就必须理解所有的上下文。而只有理解了所有的上下文，才能判断其中的代码和行为是否合理。

于是，上下文的过载就变成了认知的过载（Cognitive Overloading），而认知过载就会造成维护的困难。通俗地讲，就是“看不懂、改不动”，代码就变成“祖传代码”了。

但是我们不要忘了，这是与模型关联的代码啊！改不动的代码就是改不动的模型！改不动的僵硬的模型，要怎么参与提炼知识的循环呢？

当然，过大类还容易滋生重复代码、引入偶然耦合造成意外的缺陷等编码上的问题。不过这些问题与模型僵硬比起来，似乎有点不值一提。

另一个问题没那么严重，但同样值得重视，这个问题仍然与性能有关。在不同的上下文中，我们需要访问的数据也不尽相同。

比如在维护朋友关系、发布动态信息时，我们不太可能访问订阅与订单信息。类似的，当我们访问订单信息的时候，也不太可能关注朋友信息。那么，当所有的上下文都汇聚于 User 时，我们怎么才能获得一个高效的聚合加载策略呢（当然通过关联对象我们能缓解这个问题，具体请看上一课）？

这里的吊诡之处在于，我们并没有做什么坏事，反而是因为严格遵照“富含知识的模型”的原则，才导致了上下文过载。

我们来分析一下。User 在不同的上下文中都处在聚合根的位置：

在订阅上下文中，User 是 Subscription 的聚合根；

在订单上下文中，User 是 Payment 和 Order 的聚合根；

在社交上下文中，User 是 Friendship 和 Moments 的聚合根。

根据构建“富含知识模型”的做法，被聚合对象的集合逻辑放置于聚合 / 聚合根里，而不是散落在外，或是放在其他无关的服务中。这么做可以使得逻辑富集于模型中，同时有利于通过聚合关系管理一组对象与领域概念的映射，保持领域概念与逻辑的完整性。

那么，每当 User 在不同的上下文中处于聚合 / 聚合根位置的时候，与之相关的逻辑都会进入 User 中，User 会不可避免地膨胀。这就是上下文过载。

又要再说一句题外话了。在使用原味面向对象范型（Vanilla Object Oriented）的时候，不自觉地就会产生过大类，这其实也跟 Smalltalk 有关。正如我们上节课所讲，Smalltalk 是一个面向对象系统，其中还包含一个 IDE：System Browser。

在 Smalltalk 中，源代码并不以文本格式作为主要表达形式，而是通过一个图形化的界面，让我们有指导性地、片段式地浏览对应的代码。上图中左起第三个窗口就是自定义的上下文。也就是说，即使对象中存在再多的上下文，也不会影响我们对代码的理解和修改。毕竟上下文再多，我们也不会一次看到多个方法的具体实现代码。

如果你用过 Eclipse IDE 开发 Java 项目的话，那么这里肯定有一个不那么有用的古怪的视图，让你百思不得其解。但如果你对这张图和上面 Smalltalk System Browser 有点了解的话，就能从布局中看出一些端倪。

这其实是 Eclipse 对 Smalltalk 的致敬。毕竟，Eclipse 也算是传奇 Smalltalk 程序员 Kent Beck 的作品。但当我们以文本作为源代码的主要形式，以类作为源代码的主要组织方式时，仅仅通过视图的切换是无法达成 Smalltalk 的效果的。于是，过大类的上下文过载就成了一个严重的问题。

逻辑汇聚于上下文还是实体？

上下文过载问题最根本的症结在于，逻辑是需要汇聚于实体（User）还是上下文（订阅、社交与订单）。

原味面向对象范型（也是领域驱动设计的默认风格）的答案是汇聚于实体，但是缺少有效分离不同上下文的方式。而 DCI 范型（Data-Context-Interaction，数据 - 上下文 - 交互）要求汇聚于显式建模的上下文对象（Context Object），或者上下文中的角色对象（Role Object）上。如果按照 DCI 范型的思路，我们可以如下图这样建立模型：

这其实也很容易理解。在不同的上下文中，用户是以不同的角色与其他对象发生交互的，而一旦离开了对应的上下文，相关的交互也就不会发生了。

这与我们生活中的体验是一致的。上班时，以员工的身份发生的工作活动与交互，下班之后其实可以不用发生了。因为已经脱离了工作这个上下文，不再扮演这个角色了。同样的，在生活中的以其他身份发生的活动与交互，在工作中也不该发生。

也就是说，在订阅上下文中的读者（Reader），不会以读者的身份与订单上下文中的支付信息发生交互。而买家（Buyer），也不会以买家的身份去社交上下文中去建立朋友关系。

从 DCI 的角度看待聚合与聚合根关系，我们可以发现，并不是 User 聚合了 Subscription，而是订阅上下文中的 Reader 聚合了它。同时，并不是 User 聚合了 Friendship 与 Moments，而是社交上下文中的 Contact 聚合了它们。可以说，User 只是恰好在不同的上下文中扮演了这些角色而已。

理清了这一层关系，我们也就发现了上下文过载的根源：实体在不同的上下文中扮演的多个角色，再借由聚合关系，将不同上下文中的逻辑富集于实体之中，就造成了上下文过载。

明白这一点，解决方案也就呼之欲出了：针对不同上下文中的角色建模，将对应的逻辑富集到角色对象中，再让实体对象去扮演不同的角色，就能解决上下文过载的问题了。

当然，理想永远是清晰且美好的，在实践中却没有这么简单，主要是如何在代码中实现这种实体与角色间的扮演关系。

通过角色对象分离不同上下文中的逻辑

一个最简单的想法是，将角色抽象成接口：

//阅读上下文
interface Reader {
    List<Subscription> getSubscriptions();

    boolean canView(Content content);
}

//订单上下文
interface Buyer {
    List<Order> getOrders();

    List<Payment> getPayments();

    void placeOrder(Column column);
}

//社交上下文
interface Contact {
    List<Friendship> friends;

    List<Moments> moments;

    void make(Friendship friend);

    void break(Friendship friend);

然而，这实际上并没有解决什么问题：

public class User implements Reader, Buyer, Contact {
    // 社交上下文
    public void make(Friendship friend) {
      ...
    }

    public void break(Friendship friend) {
      ...
    }

    // 订单上下文
    public void placeOrder(Column column) {
      ...
    }

    // 订阅上下文
    public boolean canView(Content content) {
      ...
    }
}

通过实现接口去完成角色扮演，得到的代码其实没什么差别。虽然借助当代 IDE，我们可以较容易地判断行为来自哪个上下文，但实际得到的改善极其有限。既然接口不好用的话，另一个思路就是通过装饰器（Decorator），我们可以构造一系列角色对象（Role Object）作为 User 的装饰器：

public class Reader {
    private User user;
    private List<Subscription> subscriptions;

    public Reader(User user) {
        ...
    }

    // 订阅上下文
    public boolean canView(Content content) {
      ...
    }
}

public class Buyer {
    private User user;

    private List<Order> orders;
    private List<Payment> payments;

    public Buyer(User user) {
        ...
    }

    // 订单上下文
    public void placeOrder(Column column) {
      ...
    }
}

public class Contact {
    private User user;

    private List<Friendship> friends;
    private List<Moments> moments;

    public Contact(User user) {
        ...
    }

    // 社交上下文
    public void make(Friendship friend) {
      ...
    }

    public void break(Friendship friend) {
      ...
    }

我们很容易地将不同上下文中的行为与逻辑富集到对应的角色对象中，并且与领域模型中的概念完全对应起来。然而这里还有最后一个问题，就是如何构造这些装饰器对象。

从代码中我们可以看到，除了行为之外，我们将对应的聚合与关联关系也都富集于角色对象里了，而这些被聚合的对象极大可能是被持久化在数据库中的数据。所以，简单的调用构造函数，并不能保证我们能从数据库中有效地读取这些数据：

User user = repository.findById(....);

Buyer buyer = new Buyer(user); // 内存中构造角色对象
Reader reader = new Reader(user); // 并不会从数据库中读取
Contact contact = new Contact(user); // 其上的关联关系

所以，我们需要将构造角色对象的逻辑放置在能够触发技术调用的地方。那么最自然的一个选择，就是 Repository 对象了。

public interface UserRepository {
    User findById(long id);

    Buyer asBuyer(User user);

    Reader asReader(User user);

    Contact asContact(User user);
}

public class UserRepositoryDB implements UserRepository {
    public User findById(long id) {
        return db.executeQuery(...)
    }

    public Buyer asBuyer(User user) {
        return new Buyer(user, db.executeQuery(...));
    }

    public Reader asReader(User user) {
        return new Reader(user, db.executeQuery(...));
    }

    public Contact asContact(User user) {
        return new Contact(user, db.executeQuery(...));
    }
}

于是，我们获取角色对象的方法也就可以变成：

User user = repository.findById(....);

Buyer buyer = repository.asBuyer(user);
Reader reader = repository.asReader(user);
Contact Contact = repository.asContact(user);

到此为止，通过角色对象我们得到了还不错的结果——几乎就是完美的方案了。这个方案的优势有以下几点。

首先，我们把不同上下文中的逻辑分别富集于不同的角色对象中。

当我们需要改动代码时，只需要找到对应上下文中的角色对象，在角色对象中进行修改即可。这一方面解决了认知过载的问题，避免了模型僵化。另一方面，因为角色对象的封装，代码的改变并不会进行传播。

比如如果我们需要修改订阅上下文中的逻辑，只需找到与订阅上下文相关的角色对象 Reader 就行了。因为我们只改动了 Reader，而 User、Buyer、Contact 都没有任何变化，所以这个修改没有传播到其他的上下文中。

与之对比的是，如果没有角色对象，我们就需要在 User 中修改，那么修改就会被传播到所有的上下文中。

其次，从实体对象转化到角色对象经由了显式的方法调用（asBuyer、asReader、asContact），这实际上表示了上下文切换。

也就是说，当我们调用 asBuyer 时，我们的上下文就从无限定上下文，切换到了订单上下文。那么在这个上下文中，我们对影响性能的集合加载就有了更明确的答案（比如订单历史预加载、支付历史延迟加载等等），进一步避免因为性能问题，破坏模型与软件实现的一致性。

不过你应该注意到了，我刚才说这“几乎是完美的方案”。那是因为我们还可以在揭示意图与技术解耦上做得更好。

通过上下文对象分离不同上下文中的逻辑

如同我们之前在关联对象中讲到的，获取关联集合的方式可能是异构的，也就是不止从数据库中读取这一种方法。如果社交上下文中的朋友关系，是通过服务接口调用，从其他社交平台获取的呢？这个时候，我们将 asContact 方法置于 UserRepositoryDB 之内，就显得不那么合适了。

我们来换个思路。我们可以遵循关联对象的思路，将上下文直接建模出来，并通过接口隔离具体实现：

interface SubscriptionContext {
    interface Reader {
        boolean canView(Content content);
    }

    Reader asReader(User user);
}

interface SocialContext {
    interface Contact {
        void make(Friendship friend);
        void break(Friendship friend);
    }

    Contact asContact(User user);
}

interface OrderContext {
    interface Buyer {
        void placeOrder(Column column);
    }

    Buyer asBuyer(User user);

那么，我们就可以将上下文对象的获取放置在 UserRepository 上，对它进行改写：

interface UserRepository {
    User findUserById(long id);

    SubscriptionContext inSubscriptionContext();
    SocialContext inSocialContext();
    OrderContext inOrderContext();
}

public class UserRepositoryDB {
    //通过依赖注入获取不同的上下文对象
    @Inject private SubscriptionContext subscriptionContext;

    @Inject private SocialContext socialContext;

    @Inject private OrderContext orderContext;

    ...
}

最后的使用方式就成了：

UserRepository users = ....;

User user = users.findUserById(...);

Buyer buyer = users.inOrderContext().asBuyer(user);
Reader reader = users.inSubscriptionContext().asReader(user);
Contact contact = users.inSocialContext().asContact(user);

现在这就是完美的方案。通过增加上下文对象，我们获得了诸多好处。

第一，借由上下文对象的封装，不同上下文中的技术可以是完全异构的。也就是在不同的上下文中，我们可以使用不同的技术方案。而这些技术的细节，不会暴露给外在的使用者。

比如订阅上下文可以是数据库的，社交上下文是通过三方 API 与外界服务整合的，订单上下文则是通过消息系统（Messaging System）与企业内进销存系统整合的。

第二，软件实现、模型与统一语言更加紧密地关联在了一起。

我们知道界限上下文的名字是统一语言的一部分。在模型中虽然不以实体的形式出现，但总归还是有它的标识（虚线的上下文边界）。如果没有上下文对象，那么它不会是软件实现的一部分，总是觉得不够理想。而有了上下文对象，统一语言、领域模型和软件实现就彻底一致了。这种舒服的滋味实在难以言表啊！

第三，我们更加清楚地揭示了领域知识的意图。特别是在对照这领域模型，参看 Repository 接口的时候：

interface UserRepository {
    User findUserById(long id);

    SubscriptionContext inSubscriptionContext();
    SocialContext inSocialContext();
    OrderContext inOrderContext();
}

通过这个定义，我们清晰地知道，User 在三个不同的上下文中扮演不同的角色。而没有上下文对象的时候，这些信息则没有被披露，会增加理解的成本和负担。

上下文间的依赖关系

不过这里还有一个问题，上下文对象在处理复杂问题的时候，还能继续保持简洁和优雅吗？让我们再来看一个略微复杂的问题。比如我们要做个新业务，将我所订阅的专栏赠送给我的朋友。

直觉告诉我们，这需要跨越两个不同的上下文，才能构建这个业务。也就是说，在这个业务中，处在订阅上下文中的 Reader，也是处在社交上下文中的 Contact。单纯从实现它来说，问题也不难解决：

User user = users.findUserById(...);
User friend = users.findUserById(...);

Contact contact = users.inSocialContext().asContact(user);

Reader reader = users.inSubscriptionContext().asReader(user);
Subscription subscription = reader.getSubscriptions(...);

if（contact.isFriend(friend)) {
    reader.transfer(subscription, friend);

这里的问题是，“只有朋友间才能赠送”属于领域逻辑。我们希望它被富集到模型中，而不是在模型外，那么我们要怎么处理呢？答案出乎意料得简单，在上下文对象中加入依赖就好了：

public class SubscriptionContextDB implements SubscriptionContext {
    @Inject SocialContext SocialContext;

    public Reader asReader(User user) {
        //在这里将依赖的上下文传到角色对象中去就好了
    }
}

而实现这个业务代码就成了：

User user = users.findUserById(...);
User friend = users.findUserById(...);

Reader reader = users.inSubscriptionContext().asReader(user);
Subscription subscription = reader.getSubscriptions(...);

reader.transfer(subscription, friend)

通过显式建模上下文对象，我们不光将业务逻辑富集到领域模型中，而且在跨域多个上下文实现某个业务时，上下文之间的依赖关系还成了实现细节，被封装于上下文对象中了。多么美妙啊！

小结

我们来简单地总结一下。当我们严格遵照“富含知识的模型”的原则，将聚合关系有关的逻辑富集到领域模型上时，很容易产生上下文过载的问题，其根源在于实体在不同的上下文中扮演了多个角色。

而上下文过载会导致模型僵化，也就是“看不懂改不动”的代码变成了“看不懂改不动”的模型，这样提炼知识的循环就无法展开了。因而我们需要将上下文从领域对象上剥离，解决办法就是角色对象和上下文对象。

需要强调的是，虽然仅仅使用角色对象也能解决问题，但是配合上下文对象一起，它们能在揭示意图的同时，使模型、统一语言与软件实现更紧密地关联在了一起，是非常有用的模式。

思考题

关联对象、角色对象和上下文对象都属于结构模式（Structural Pattern），帮助我们组织对象中的数据。那么从行为模式（Behavioral Pattern）上看，我们还会遇到什么问题呢？

欢迎把你的思考和想法分享在留言区，我会和你交流。同时呢，我也会把其中一些不错的回答置顶，供大家学习。

06 | 跨越现实的障碍（下）：架构分层就对了吗？

你好，我是徐昊。今天我们来聊聊如何有效地基于领域模型构造合理的架构。

到目前为止，我们学会了通过关联对象解决聚合 / 关联关系；利用角色对象分离不同上下文中的交互；并使用上下文对象完成实体对象到角色对象的扮演。这些模式通过结构上的优化，更好地组织了对核心数据的访问逻辑，使得我们可以在兼顾架构约束的同时，将领域概念与逻辑有效地转化为领域模型。

然而当我们把眼光从构造领域模型，扩展到利用领域模型构建整个应用或系统时，就会遇到新的问题：如何组织领域逻辑与非领域逻辑，才能避免非领域逻辑对模型的污染？

通常我们会使用分层架构（Layered Architecture）区分不同的逻辑以解决这个问题。然而由于领域层被人为赋予了最稳定的特性，破坏了分层架构间的依赖关系。所以我们需要修正分层，才能有效地围绕领域模型来构造软件架构。

那么今天这节课我们就看看分层架构的问题在哪儿，以及如何通过能力供应商（Capability Provider）模式获得一个更好的架构愿景。

领域层的“不正当关系”

分层架构是运用最为广泛的架构模式，它将不同关注点的逻辑封装到不同的层中，以便扩展维护，同时也能有效地控制变化的传播。

在使用领域驱动设计时，我们通常会将系统分成四层：

展现层(Representation Layer)：负责给最终用户展现信息，并接受用户的输入作为功能的触发点。如果不是人机交互系统，用户也可以是其他软件系统。

应用层（Application Layer）：负责支撑具体的业务或者交互流程，将业务逻辑组织为软件的功能。

领域层（Domain Layer）：核心的领域概念、信息与规则。它不随应用层的流程、展现层的界面以及基础设施层的能力改变而改变。

基础设施层（Infrastructure Layer）：通用的技术能力，比如数据库、消息总线等等。

领域驱动设计使用分层架构，主要是因为各层的需求变化速率（Pace of Changing）不同。分层架构对变化传播的控制，是通过层与层之间的依赖关系实现的，因为下层的修改会波及到上层。我们希望通过层来控制变化的传播，只要所有层都单向依赖比自己更稳定的层，更易变依赖不易改变的，那么变化就不会扩散了。

伴随着 David Wheeler 那广为误解的半句名言：“计算机科学中的所有问题都可以通过引入一个间接层解决”，这就让分层几乎已经成了架构的默认法门。然而，正如不那么广为人知的后半句“除了有太多的层以外”所预言的，领域驱动设计分层架构的问题，恰恰就在于多了一层。接下来我们就来分析一下这个问题。

通用技术能力和领域概念谁稳定？

大部分谈论领域驱动设计架构的书或文章，都会拿出这样一张分层依赖图，用来说明应该如何构造合理的层与层之间的依赖关系：

这张图通过依赖关系表示了每一层的变化速率：展示层最易变，其次是应用层，然后是领域层，最后是基础设施层。那实际情况是这样吗？有一部分说对了，而另一部分则值得商榷。

展现层的逻辑，的确是最容易改变的：新的交互模式，不同的视觉模版，甚至单纯就是怕用户看烦，想修改一下保持新鲜感，都会带来展现层逻辑的改变。

而应用层的逻辑，会随着业务流程以及功能点的变化而改变。比如流程的重组与优化、新功能点的引入，都会改变应用层的逻辑。

领域层是核心领域概念的提取。从理论上说，如果通过知识消化完成模型的提取，由模型构成的领域层应该就是稳定态了，不会发生重大改变。因为一旦领域层出现重大改变，就意味着重大业务调整，整个系统都可以推倒重来了。因而在软件系统有限的生命周期内，我们可以认为领域层应该是不变的。

这也是为什么领域驱动设计受到行业热捧的一个原因，寻找到一个在软件系统生命周期内稳固的不变点，由它构成架构、协同与交流的基础，帮助我们更好地应对软件中的不确定性。当然，这里的“不变”指的是已经存在的领域概念和核心逻辑不会改变，但是仍然可以有新的概念和逻辑加入到领域层中。

基础设施层的逻辑由所选择的技术栈决定，更改技术组件、替换所使用的框架，都会改变基础设施层的逻辑。因而基础设施层的变化频率跟所用的技术组件有很大关系。越是核心的组件，变化就越缓慢，比如在选定数据库系统后，不太可能频繁地更换它。而如果是一个缓存系统，那么变化的频率就会快很多。

但是，基础设施层还可能存在不可预知的突变。如果我们历数过往的诸多思潮，NoSQL（Not Only SQL）、大数据（Big Data）、云计算（Cloud Computing）等等，都为基础设施层带来过未曾预期的突变。

此外，周围系统生态的演化与变更，也会给基础设施层带来不可预知的突变的可能。比如，所依赖的消息通知系统从短信变成微信，支付方式从网银支付变成移动支付，等等。

这里就有问题了。从概念上看，领域层是绝对稳定的。它既不随应用逻辑，也不随技术能力的变化而改变。唯一能够促使它变化的，是真实世界业务中的核心概念与逻辑。换句话说，领域模型不会被赛博世界（Cyber World）内的任何变化影响，它就是赛博世界中的孙悟空，跳出了三界外，不在五行中。

所以，跟领域层相比，基础设施层就不够稳定（其实不光基础设施，所有层跟它相比都不稳定），那么我们怎么能让绝对稳定的领域层，去依赖不够稳定的基础设施层呢？这不是违背了分层架构的依赖原则吗？

领域模型对基础设施的态度是非常微妙的：一方面，领域逻辑必须依赖基础设施才能完成相应的功能；另一方面，领域模型必须强调自己稳定性，才能维持它在架构中的核心位置。而作为被人为认定为的绝对稳定，它不能依赖任何非领域逻辑（除了基础库）。任何对其他逻辑的依赖都会带来修改的传递，会使领域层变得不稳定。

明明有实质性的依赖关系，却碍于“绝对稳定”的身份而不能承认。我想每个人都会想起一些不那么美好的词汇，来形容这种“不正当关系”吧。

可悲的是，作为架构师，我们还必须得帮助领域层来实现这种“不正当关系”（然后我劝你停止联想，不然人通过劳动改造自然的同时，劳动也会改造人，架构师就没法维持 IT 一族老实人的美好人设了），这样才能确立领域层的“绝对稳定”和“绝对核心”的位置。

理解了这点，你可以再看一下我们在关联对象那节课中看到的一段代码。现在，我想你对它的丑陋之处会有更深刻的认识了：它暴露了领域层对基础设施的直接依赖。

public class User {
    public List<Subscription> getSubscriptions() {
      ....
    }

    public List<Subscription> getSubscriptions(int from, int size) {
       return db.executeQuery(....);
    }
 }

基础设施不是层

产生这种“既要用，又不愿意承认”的依赖关系的根源在于，基础设施本身就不是层。或者更严格地说，以变化速率作为分层依据，那么基础设施就是无效的分层。如果你仔细看一下分层架构，可以发现两个相关而又不同的维度：变化上，从易变到稳定；逻辑上，从使用视角到实现视角。

从使用视角到实现视角，是另一个可以理解分层的方式：

展示层最接近最终用户，完全表示对最终用户的信息展示和输入收集。同时它也使用了应用层、领域层与基础设施层。

应用层，从业务流程与交互体验上支撑了展示层逻辑的实现。同时它也使用了领域层与基础设施层。

领域层，从业务概念和核心逻辑上支撑了应用层流程和体验的实现。同时它也使用了基础设施层。

基础设施层，从技术能力上支撑了领域层概念和逻辑的实现。

从这个角度看，我们也更能理解为什么展示层比应用层易变，应用层比领域层易变。因为层与层之间是使用与实现的关系，但是这个关系在领域层与基础设施层之间被打破了。

因为我们人为地规定了领域层最稳定，那么用以实现领域层的基础设施层，就不能比领域层更稳定。因此我们的选择只有两个：要么承认领域层并不是最稳定的（也就是领域层是“在特定技术栈上的领域模型实现”）；要么就别把基础设施当作层来看。

说句题外话，其实我始终推荐不要过分强调领域层的绝对独立性，心里坦然接受领域层并不是无约束的理想化实现，而是受特定技术栈与技术生态环境约束的实现，就没那么多烦恼与纠结了。

这么做的话，对于领域驱动设计的优点，我们也并没有损失什么。除了需要做点儿心理建设来接纳它之外，唯一真正的不足，是可能要在构造测试上投入更多的成本。比如测试领域层逻辑时，需要构建基础设施的假替身（Test Double，Fake），并且还需要将领域层与基础设施层一起测试。

第一个迈出这一步且被广泛接受的框架是 Ruby On Rails。它对领域驱动设计的颠覆，绝不小于它对 Web 开发的颠覆。特别是，既然使用了基础设施层，就大大方方地承认，提前准备好相关的测试工具，也没什么不好嘛。

然而对于大多数实践领域驱动开发的人而言，领域层的“绝对稳定”是如同光速一般的恒常，“孙悟空”是心中的白月光，“又当又立”是圣杯。否定掉领域层的稳定性，心理建设成本过高。

那么我们只能承认基础设施不是层，需要从不同的角度构建一种架构模式，使得领域模型既可以隐含地使用基础设施，又不暴露对它的依赖。

能力供应商模式

如何才能取消基础设施层，但仍然不影响领域模型的实现呢？我会推荐使用能力供应商（Capability Provider）模式。能力供应商模式是面向对象基础原则 SOLID 的综合应用，网上关于 SOLID 的讨论汗牛充栋，我就不在这里展开了。

从基础设施到有业务含义的能力

让我们通过一个案例，看一下如何构造能力供应商。还是极客时间的例子，这次我们来看订单部分。假设目前需要通过网银来支付订单，并通过邮件将订单状态更新并发送给客户。模型如下：

那么在忽略具体实现细节之后，代码可能是这个样子的：首先调用银行网关，然后根据银行网关返回的结果，生成支付记录并通知客户。

public class Order {

    public void pay() {
        BankResponse response = bank.pay(....);
        if （response.isOk()) {
            payments.add(new Payment(response...));
            status = ....;
            email.send(....);
        } else {
            email.send(....);
        }
    }
}

如前所述，这样的代码从领域驱动设计角度来看并不算好。因为我们在领域层中，直接依赖了基础设施层中的网银网关客户端和邮件通知客户端。但是想改正这个问题，并没有那么容易。

因为领域层被认为定义为绝对稳定，它不能依赖任何非领域逻辑（除了基础库）。而我们又要使用网银客户端和邮件通知客户端来完成功能，那该怎么办呢？我们只好将网银客户端和邮件通知客户端移动到领域层内。

但是我们不能直接移动，毕竟领域层中只能是领域概念与逻辑，与具体业务无关的概念是不能进去领域层的。于是我们需要 ** 将对基础设施层的依赖，看作一种未被发现的领域概念进行提取，这样其实就发挥了我们定义业务的权利，从业务上去思考技术组件的含义。

一种有效的方法是将技术组件进行拟人化处理。比如网银转账这个行为，如果在业务中有一个人去做这个操作，那么会是谁呢？通知用户订单状态发生转变了，这个人又会是谁呢？通过拟人化，我们就能很清楚地看到技术组件到底帮助我们完成了什么业务操作。

在我们这个例子里，转账的是出纳（Cashier），通知用户的是客服（Customer Service）。于是我们的模型就能转化为：

可以看到，我们将具有业务含义的能力抽象成接口纳入领域层，而使用基础设施层的技术能力去实现领域层的接口。也就是说，基础设施层成为了能力供应商。代码如下：

//领域层内
interface Cashier {
    ...
}

interface CustomerService {
    ...
}

public class Order {

    public void pay(Cashier cashier, CustomerService staff) {
        try {
            Payment payment = cashier.collect(...);
            payments.add(payment);
            staff.tell(owner, ..);
        } catch(....) {
            staff.tell(owner, ..);
        }
    }
}

//领域层外
public class BankPaymentCahsier extends BankPaymentClient implements Cashier {
    ...
}

public class EmailCustomerService extends EmailNotificationClient implements CustomerService {

你肯定觉得，就这？这不就是抽个接口改个名吗？是的，从实现技法上看，这是简单得不能再简单的面向对象方式了：从具体实现方法中寻找到一个抽象接口，然后将从对具体实现的依赖，转化为对接口的依赖（SOLID 中的里氏替换原则）。

但是与之前相比，我们现在的做法有两个不同：

领域模型与软件实现关联；

统一语言与模型关联。

没错儿，就是领域驱动设计的“两关联”。我们再复习一下，通过两关联，任何词汇都可以成为领域概念，而与概念提取的发起方无关。

也就是说，领域概念可以不是领域方提取的。只要我们从技术组件中提取的词汇具有业务含义，且被业务方认可，那么它就是领域概念。因此我们并不是改了个名，而是提取了领域概念。要时刻牢记自己的权利！

总结来说，通过从技术组件抽象具有业务含义的能力，我们就能将基础设施转变为具有这种能力的供应商。于是，我们就能帮助领域层实现了它希望的那种“不正当关系”，既使用了基础设施，又对它没有依赖：我们依赖的是领域层内的能力接口（SOLID 中的接口隔离原则），而不是基础设计的实现（SOLID 中的倒置依赖原则）。

将基础设施转化成为能力供应商后，我们的分层结构中也就不再存在基础设施层了。于是我们的架构愿景，也就发生了改变。那么该怎么使用能力供应商，来形成真正多层架构呢？我们接着往下看。

使用能力供应商的多层架构

我们可以将基础设施，看作对不同的层的扩展或贡献（SOLID 的开闭原则）。它虽被接口隔离，但却是展示层、应用层和领域层的有机组成部分。在每一层中留有能力接口，基础设施则作为这些能力接口的供应商，参与层内、层间的交互。

那么这样的架构，无论从变化的频率还是使用实现关系上就形成了统一。除此之外，通过能力供应商，我们还解决了分层架构里的另一个难题：层与层之间是单向依赖关系，那么如果需要上一层参与下一层的交互与逻辑，层与层之间就会形成双向依赖关系。

不过，通过能力与能力供应商，层与层之间出现了另一种交互的可能：上一层作为下一层的能力供应商，参与到下一层的业务与流程中去。而这种参与，并不会带来额外的依赖。示意图如下：

比如在前面支付的例子里，除了邮件通知以外，我们还希望触发一个流程去为用户做一下支付失败原因的回访。这属于应用层逻辑的一部分。那么我们只需要在应用层中实现对应的逻辑，并成为领域层的能力供应商就可以了。示意代码如下：

public class FollowUpWorkflow implements CustomerSerivce {
    private WorkflowEngine engine;

    public void tell(.....) {
        this.engine.startFlow(....);
    }
}

我不知道你是否觉得能力供应商这个模式有点眼熟？没错儿，它就是关联对象、角色对象和上下文对象的元模式（Meta Pattern）。事实上，我们第 4 讲、第 5 讲所学的内容，都可以看作是能力供应商模式的一种特殊表现形式。它们所表示的能力，与在特定技术约束下如何组织领域模型的数据相关。比如关联对象，就是如何在数据库不在内存中的情况下读取大量数据，并维持逻辑一致性的能力。

通过这个元模式，我们还可以衍生出很多有用的模式，比如全局数据对象（Global Data Object）模型，用类似 Users 这样的领域概念表示系统全局范围内所有的用户对象。

这里我就不一一展开了，我相信如果你理解了能力供应商，并认真学习了前几节课的例子，你自然可以收发随心地使用恰当的模式来解决遇到的问题。

小结

我们来简单总结一下。分层模式并不能很好地帮助我们构建以领域模型为核心的系统架构，主要问题就在于如何处理领域层与基础设施层的关系上。而我们对领域模型的执念，使得我们无法承认基础设施层更稳定（本身也有突变风险）。所以我们应该重新思考分层架构是否正确，以及怎么分才更合理。

我个人建议分成三层：展示层、应用层与领域层。不仅要将基础设施作为能力供应商配合其他层来使用，同时通过能力供应商模式，来实现层与层之间的双向交互，这样就不用担心会带来额外的依赖了。

最后，能力供应商模式是一个元模式，关联对象、角色对象和上下文对象可以看作它的具体应用。熟练掌握这个模式，你就可以根据需要发明自己的领域驱动实现模式了。

思考题

能力供应商模式在处理大的架构问题上，唯一的不足就在于将显式的依赖关系，转化为了隐式依赖关系，这就对知识管理有了更高的要求。请你思考一下这种架构模式对项目交付的挑战，以及需要怎样的知识管理手段才能保证知识传递的顺畅？

如果你在学习过程中有任何问题或者心得，欢迎在留言区和我交流互动。

07｜统一语言可以是领域模型本身吗？

你好，我是徐昊。今天我们来聊聊如何把领域模型作为统一语言。

在第二讲，我们谈论到为什么统一语言是必要的：一是因为业务维度在领域模型中被隐藏了；二是我们需要一个缓冲，去发现模型中不存在的概念。不过这里有一个隐藏的前提假设：最终建模结果将是原味面向对象范型的模型。

而我们在介绍上下文对象的第五讲里，已经见到一种不同于原味面向对象模型范型的思路：DCI 范型。围绕角色与上下文对业务进行分解，而不仅仅是实体与关系。那么我们是不是可以通过不同的建模范型，将领域模型本身当作统一语言呢？

答案是肯定的，而且其中最关键的就是如何将隐藏在模型中的业务维度展开。不同的建模范式，展开业务维度的方式与逻辑也不尽相同。我们首先需要更仔细地理解一下业务维度是如何被隐藏在模型中的，然后再看看不同的建模方法将会如何展开业务维度。

业务是模型的隐藏维度

我们仍然以你已经很熟悉的极客时间专栏领域模型为例，看看在订阅这个上下文中（模型如下图所示），原味对象范型的模型是如何将业务维度隐藏的：

作为技术人员，我们可以很容易地从数据角度理解业务行为：当用户订阅了一个专栏之后，就会产生一个新的 Subscription 对象，它会记录用户具体订阅了哪个专栏。而我们通过对 Subscription 对象的检视，就能知道用户一共订阅了多少个专栏。下图展示的是我们将模型实例化后的结果：

需要说明的是，在这个过程中，我们并不关心 Subscription 是如何产生的。无论是用户自行购买的、朋友送的、抽奖中的、首单减免白得的，只要最终产生了 Subscription 对象，用户就能访问专栏内对应的内容。

比如，在 2020 年 10 月 16 日，用户“爱学习的鱼玄机”自行购买了专栏《用户体验设计》，就会产生一个 Subscription 实例，将用户与专栏关联起来。而只要这个 Subscription 对象存在，用户就可以访问专栏内的课程内容。

同样，在 2021 年 4 月 2 日，“爱学习的鱼玄机”接受朋友“会聊骚的黄庭坚”的馈赠，得到了专栏《Harmony OS 应用开发》，这样也会产生一个 Subscription 实例。它将用户与专栏关联起来，让“爱学习的鱼玄机”可以看到这个专栏里的内容。

从数据变化的角度看，自行购买、朋友送、抽奖中、首单减免白得等流程，都被隐藏在模型的抽象之中了。一方面，这正体现了模型抽象能力的强大。只要产生相似的数据关联，业务逻辑就可以被保证，而细节则可以被忽略。而另一方面，其实也增加了业务方理解模型的难度。

如果订阅专栏的流程如下图所示，包含不同的步骤与操作，业务方要如何才能明白模型是怎么支撑这个流程的呢？

可以看到，业务方很难从领域模型中感受到业务维度（流程、交互、功能、规则、价值等），所以这也正是我们需要额外构造统一语言的原因。

但是如果有办法可以将隐藏在领域模型中的业务维度展开（哎呀呀，低维展开了呢），使得业务方能够理解不同的业务流程是如何作用于领域模型的。那么，业务方就能够判断领域模型是否准确捕捉了业务概念，以及领域模型是否能够有效支撑整个系统（也就是判断技术方是否真正理解了需求）。这时候我们就不再需要额外的统一语言了，因为模型本身就能起到统一语言的作用。

那么我们将业务方可以理解的领域模型称作业务友好与可读（Business friendly and readable）的模型，这类模型可以作为统一语言使用。于是将隐藏在领域模型中的业务维度展开，最终的目标就是通过在模型中还原业务维度，使得模型变得业务友好与可读，从而将模型变成统一语言。

将模型变得业务友好与可读的方法有很多，但总体而言主要有两个方向：

催化剂建模法及其变体（Catalysis Modeling and its variants）；

事件建模法（Event-based Modeling）。

今天我们先讲催化剂建模法，因为它是一个简单直接的方法，非常容易使用。下节课再介绍事件建模法。

催化剂建模法

催化剂建模法（以下均称“催化剂方法”）是一种尝试将流程视角引入对象建模的方法。它的历史比领域驱动设计还要古老，大约成形于 20 世纪 90 年代中期，也就是 1996 年前后。它跟领域驱动设计有着相似的价值观和实践手段，比如在催化剂方法中，有一个称作共享词汇表（Shared Vocabulary）的核心实践。没错，它就是催化剂版本的统一语言。

不同于统一语言，共享词汇表只包含领域模型的名字，其余的业务逻辑都通过模型自身来表达。从这一点，我们也可以看出催化剂方法对其产出模型的信心。

这也是催化剂方法跟知识消化法不同的地方。催化剂方法更强调对模型本身的构造，而不是依赖一个试错的过程去逼近真相。当然，这也使它看起来更像一种重设计的方法（Big Upfront Design）。然而生不逢时，发表后没几年，恰好赶上敏捷运动（Agile Movement）如火如荼，就迅速被人遗忘了。

当催化剂方法再度进入我们视野的时候，是因为我们需要寻找一种更具业务表现力、可作为统一语言的模型。借由这个原因，催化剂方法又被重新拾起，作为一种将业务维度在领域模型上展开的方法，重新被引入到领域驱动设计中。但我们还是嫌弃它颇为厚重的建模流程，最后被逐步轻量化，产生了很多不同的变体（怎么感觉身世凄苦得和 Casper David Friderich 有一拼）。

从建模上讲，催化剂方法最大的特点在于，将交互（interaction）显式地建模到对象模型中。如下图所示，为催化剂方法建模的概念关系图：

交互既可以代表业务流程，也可以是用例或者系统功能。通过使用关系（uses）表示在流程或功能点上使用了哪些对象模型，相当于通过角色和交互为对象模型打了标签（tag），从而将隐藏在模型中的业务维度（流程、功能点）展开了。

如果我们使用催化剂方法重新建模极客时间的例子，结果大概是这个样子：

通过这个催化剂方法建模的模型，我们明确可以看到：订阅专栏是通过建立 Subscription 对象实现的；送朋友专栏也是通过 Subscription 对象实现的。再配合简单的说明和解释，我们就可以逐步让业务方理解：系统的功能大致是如何被实现的，哪些模型对哪些业务流程有影响。而在领域模型的粒度下，如果业务方能理解系统是如何实现的，那对双方的协同合作将会有巨大的好处。

我想你肯定经历过这样的场景：因为开发人员发现需求修改的部分涉及到核心模块，给出了较大工作量的评估；而业务方怀疑开发人员是故意保留工作量弹性，于是便不断挑战工期进度。

因此借由领域模型，双方能就需求的具体范围、风险和成本形成一定的共识，从而避免猜忌性施压与扯皮。这样的话，业务方与开发人员也都能把这套模型当作统一语言，去澄清需求，以及评估实现的风险与成本了。

对于业务的讨论、需求的澄清，通常是在给定的流程与业务上下文中，需要讨论涉及的模型信息是否足够，概念是否清晰。比如在上面这个图里，我们可以明显发现一个信息不足的地方，就是我们系统中并没有存在朋友关系，那么送朋友专栏要怎么操作呢？

一种做法是修改领域模型，增加朋友关系的信息。比如这样：

另一种可能的做法是，在领域模型中引入一种特殊的订阅关系：可转让的订阅。也就是说，虽然是我订阅的专栏，但目的是转送别人这样一种订阅关系。至于转送的渠道，可能是微信、短信、邮件或是其他具有朋友关系的平台：

至于究竟哪种方案更好呢？不知道。这是需要业务方与技术方共同讨论并确定产品方向的时刻：是引入社交属性，还是保持纯粹的定位。不过当业务方理解了在背后支撑流程的模型，这种讨论就能变得更加顺畅自然。

至于对风险和成本的评估，也是同样的道理。当需要引入新的功能点时，可能会牵扯到对背后模型的修改，那么我们就可以根据要修改的模型来判断成本和风险。

就以上面两个不同的模型为例。同样是在模型中增加一个新概念，朋友关系比可转让的订阅风险要小得多。毕竟订阅处在模型的中心，承载了太多的业务，这一点可以根据有多少功能点使用了这个模型，以及使用这个模型的功能点的重要程度进行综合判断。

而朋友关系相对来说就没有那么重要，所以风险要小得多。但为了维护朋友关系，我们可能还需要引入另外一些功能：申请好友、好友验证之类。功能范围可能会大一些。

但不管选择哪种方案，更为重要的是，在充分理解这些成本与风险之后，业务人员可以做出更为准确的判断，看看要采用什么样的方式来实现这个功能，既能使项目可控，又能满足系统发展的需要。

所以借由这个例子，我们可以看到，展开了业务维度的领域模型完全可以胜任统一语言的功能。此外，使用模型还可以揭示更多技术方的信息，通过它能让业务方大致理解功能实现的思路，为双方的有效沟通打下基础，增进信任。

角色 - 目标 - 实体法（Role-Goal-Entity）

催化剂方法将角色与交互直接建模到了领域模型中，从业务维度展开的角度上讲，的确带来了很多优点。然而在领域驱动设计的语境下，它也存在难以克服的缺点：交互被直接建模到模型中了，那么我们要怎么关联模型与软件实现呢？我们是选择性地忽略交互与角色，还是将交互实现成某种特定的组件呢？

要知道，将交互直接实现为组件，在 2005 年左右看起来特别有道理。因为那时候无论是 J2EE 还是.NET 的多层架构，都有一个在概念上跟交互差不多的组件。在 J2EE 里是 Session Bean，在.NET 中是业务逻辑接口层。但是随着架构轻量化的趋势，采用这种做法的人越来越少。

这时出现在领域模型中的交互就变得很尴尬了。一方面，不能缺少交互这部分信息，否则业务维度又被隐藏了，领域模型也就无法担任统一语言；另一方面，交互又没有软件实现与它关联，并且严格意义讲它也不是领域逻辑，慢慢就变成了一种“你看它不爽，但是又干不掉它”的存在。

于是角色 - 目标 - 实体法出现了。角色 - 目标 - 实体法不再将流程建模到模型中，而是作为发现领域概念的指导。角色 - 目标 - 实体法通过一张表格来梳理系统中的角色，他们想在系统上实现的目标，以及在实现这个目标的过程中会接触哪些领域模型中的实体。我们仍然以极客时间的专栏为例，可以得到这样一个表格：

一旦我们理解了催化剂方法，那么就能看出角色 - 目标 - 实体法只不过是换了一种形式，它通过一个额外的表格将领域模型中的业务维度展开了。所以在很大程度上，我们可以将角色 - 目标 - 实体法当作催化剂方法的简化版。

角色 - 目标 - 实体法除了能够得到一个更“纯净”（也就是不包含无法与实现关联的模型）的领域模型之外，还提供了一种收集需求，结构化进行领域建模与构建统一语言的流程：

首先与业务方沟通，明确参与系统的所有角色。

围绕着这些角色，澄清他们希望通过系统达成的目标。

将目标中提及的“实体”（名词、概念）提取出来，问清楚含义与内涵。

围绕这些实体建立领域模型，并与业务方阐述这些模型是如何用以实现目标的。

当业务方理解了模型之后，模型就成为了统一语言。

这个流程有个额外的好处：可以提高业务方对领域模型的认同感。

不同于催化剂方法，角色 - 目标 - 实体法更像一种共创方法，它由研发人员建立领域模型，然后再解释给业务方。双方通过协作共同梳理系统中的用户与其目标，共同澄清领域概念。然后技术人员再根据澄清的实体，建立领域模型。这样得到的领域模型，可以更自然地成为统一语言。

正是由于这样的好处，角色 - 目标 - 实体法在领域驱动设计中得到了广泛的认可和实践。不过我们仍然要明白，它与催化剂方法之间千丝万缕的联系。而且还要知道，我们之所以放弃催化剂法，是因为在今日架构的趋势中，没有与流程直接对应的组件。

但是随着架构风格的变化，比如，随着 BFF（Backend For Frontend）的日趋流行，我们能在软件实现中又能找到一种与交互对应的组件了，说不准哪天我们又会开始结合使用催化剂法与角色 - 目标 - 实体法了。

小结

我们来简单地总结一下。将领域模型作为统一语言的一个难点在于如何有效地展开业务维度。因此各种领域建模方法的优劣高下，都可以在这个维度下进行比较与取舍。

这节课讲了两种方法，分别是催化剂方法和角色 - 目标 - 实体法，它们通过直接在领域模型中引入交互，解决了业务维度展开的问题，可以有效地将领域模型变成统一语言。

此外，角色 - 目标 - 实体法还提供了一种共创统一语言的流程，在梳理需求的同时实现了将领域模型转换为统一语言。

下节课，我们将介绍另一种不同的业务维度展开的方法：事件建模法。在开始之前，我希望你可以思考下面的问题。

思考题

催化剂方法和角色 - 目标 - 实体法可能会存在哪些不足？如果让你改进这个方法，你会从哪些角度入手？

欢迎把你的思考和想法分享在留言区，我会和你交流讨论。我们下节课再见！

08 | 什么办法可以在讨论中自然形成统一语言？

你好，我是徐昊。今天我们来聊聊事件建模法（Event-based modeling）。

对于大多数人而言，业务建模中最难的一步并不是获得模型，而是说服业务方接受模型作为统一语言。虽然我们上节课讲到可以把角色 - 目标 - 实体法当作一种共创方法，但在实际操作的过程中，角色 - 目标 - 实体法仍然存在收集 - 建模 - 说服这三步。那么，有没有一种方法，可以在讨论的过程中，更自然地完成模型共创呢？

答案是肯定的。事件建模法就是这样一种更易于模型共创的方法。不同于原味面向对象范式关注实体之间的关联与交互，事件建模法通过事件捕捉系统中信息的改变，再发掘触发这些改变的源头，然后通过这些源头发现背后参与的实体与操作，最终完成对系统的建模。

目前有两种比较有代表性的事件建模法，一种是目前 DDD 社区热捧的事件风暴法（Event Storming），另一种是我从 Peter Coad 的彩色建模中演化出的四色建模法。这节课我们先来学习事件风暴法，下节课我再展开讲解四色建模法。

不过在学习这两种具体的建模方法之前，我们有必要先了解事件建模法的 2 个基本原则，分别是通过事件表示交互，和通过时间线划分不同事件。

事件建模法的基本原则（1）：通过事件表示交互

在催化剂方法中，我们将交互直接以对象的形式引入到了领域模型中。这虽然在一定程度上展开了隐藏在领域模型中的业务维度，但是在多数情况下，结果的粒度仍嫌太粗。这也是为什么催化剂方法只能用于简单项目，或是项目的初始阶段。

而且随着项目的深入进行，它能提供的信息就变得相当有限了。我们回顾一下之前的催化剂模型，订阅专栏这个流程被表示为了一个交互对象。

或许对于简单的流程来说，催化剂方法已经足够了。但如果订阅专栏流程本身就包含很多步骤，那么展开的信息就不足以帮助业务方作出有益的决定了。如下图所示的流程，就是从专栏列表中挑选心仪专栏，查看详情后通过手机购买的过程。

那你肯定会有疑问了。如果只有一个交互对象，业务方仍然不知道在具体的步骤中，接触到了哪些领域模型，以及在具体的步骤中，领域模型所包含的信息是否能够支撑业务的实现。那么我们要怎样展开业务流程，才能让它以恰当的粒度自然地融入领域模型呢？

这里的难点有两个：第一个是恰当的粒度；第二个是融入领域模型。毕竟我们上节课已经讲过了，对于催化剂方法的一大诟病就是交互的对象难以直接与软件实现关联。

我们先来解决第二个问题，因为第一个问题的解法与它有关。第二个问题的核心困难在于该如何通过对象模型表达流程。如前面所讲，模型偏重于数据角度，描述了在不同业务维度下，数据将会如何改变，以及如何支撑对应的计算与统计。而业务维度中的流程、交互、功能点，则更关注行为。

那我们该怎么弥合数据与行为之间的差异呢？事件（Event）是一种行之有效的办法。

我们可以把事件看作行为的印记。比如支付这个行为。我们不需要直接描述支付这个行为，而是通过捕捉这个行为前后的事件：支付发起（Payment Started）和支付完成（Paid）。要知道，事件自身能表达的含义有限，但是将一系列事件按照发生顺序排列起来，就能还原发生过的行为。

因而事件发生的时间点就是事件最重要的属性。我们会在模型中画一条时间轴，并依据事件发生的时间点，按先后顺序将事件标记在时间轴上。这样的话，一前一后成对出现的支付发起与支付完成，就暗示着在两个事件之间，存在一个成功的支付行为：

接下来就可以根据事件去寻找背后的领域模型了。根据我自己的经验，我们可以按照记叙文六要素去想：时间、地点、人物、起因、经过和结果。

经过是我们需要表示的行为，不需要出现在模型里；

时间可以是事件顺序的依据；

地点、人物、起因、结果，则是寻找领域概念的依据。

比如上面支付这个例子：

地点在这里不太需要；

人物就是买家（Buyer）；

起因是订单（Order），因为需要要支付的是订单，那么我们可以把起因事件（Payment Started）和订单关联；

结果是支付凭证（Payment），我们将它与结果事件（Paid）关联。

当我们再补全模型之间的关联，就能得到这样一个更完整的模型了：

那么我们只需要根据交互流程或者是业务流程，寻找出对应的事件，并以事件为指引，就能找到事件背后的领域模型了。下面是一个示意图，表明这个过程和结果。

通过这个例子，我们可以看到事件建模法与其他面向对象建模法的差别，这些差异带来了三个优点。也正是这三个优点，使得事件建模法更容易与业务方完成模型共创。

第一，虽然看起来多出了一个额外的寻找事件环节，但对于大多数项目而言，建立业务流程或交互流程都是必需的。如果是从头开始的绿地项目（Greenfield），就需要建立流程以明确需求；如果是存量项目，从流程开始梳理功能也是必备的步骤。

那么在梳理流程的同时发现事件，或者是直接通过事件描述流程，都是惠而不费的操作。顺便说一句，目前更常见的做法是直接通过事件描述流程。这种做法也是一种预热，让业务方可以提前适应从数据角度来描述流程。

第二，通过记叙文六要素去寻找领域概念，很自然地就关联了业务维度，提高了业务方对统一语言的接受度。当我们按照“时间、地点、人物、起因、经过、结果”还原“领域情节”，从中去寻找业务概念的时候，这些业务概念很自然地就连带了业务的维度。

毕竟我们的大脑善于记忆情节，如果某个概念出现在有情节的场景中，那么我们的大脑不光会记住这个概念，还会记住与这个概念相关的情节。当我们回想或是再听到这个概念时，就会下意识地回忆起相关的情节。

那么对于通过领域情节获取的领域概念，业务方不光会记住领域概念，也会记住领域情节。这种连带记忆，让我们很自然地把业务维度展开到了模型里。

第三，事件建模对于最终得到的领域模型并没有什么额外的限制，不会因为我们使用了事件建模法，就得到奇怪的模型。它只是一种更自然地发现领域概念的方式。而领域概念与领域逻辑的组织，还是交由建模者自行掌控。

事件建模法的基本原则（2）：通过时间线划分不同事件

通过事件的记叙性，我们已经将流程性的业务维度在模型中展开了。但是如果事无巨细地将所有交互都转化为事件，那么对模型似乎会有较大的干扰。这也是我们之前提到的问题，到底怎么才能找到合适的粒度将业务维度展开到领域模型中？

事件建模法有一个巧妙的办法解决了这个问题：引入多条时间线。一种常见的做法是，引入一条名叫领域事件的时间线，其上发生的都是对领域模型有重要意义和影响的事件。那么之前的时间线也就相应地被称作交互事件时间线：

交互事件我们很容易理解，也就是通过交互流程产生的事件，那么什么是领域事件呢？领域事件其实指的就是发生在领域中且值得注意的事件。而领域事件通常意味着领域对象状态的改变，因此这也是我们判断一个事件是否是领域事件的重要依据。能引起领域对象变更的就是领域事件，否则就是交互事件。

比如前面例子的两个事件：支付发起和支付完成。支付发起不会引起领域对象的变更，而支付完成则意味着会增加一个支付凭证。那么如果在这两个事件中去区分的话，我们更有理由选择支付完成作为一个领域事件，而将支付发起看作交互事件。

还有一个办法也可以用来判断某个事件是否是领域事件，就是能否将这个事件看作某个领域逻辑的等价接口。如果我们把订单上下文和支付上下文单拿出来看：

当支付完成的时候，支付网关希望通知订单上下文构建支付凭证。因为支付凭证（Payment）被角色买家（Buyer）聚合，那么支付网关需要调用 Buyer 的逻辑构建凭证。而如果把订单上下文当作一个整体，还可以发送事件支付完成（Paid）到订单上下文，通知订单上下文构建支付凭证。这时发到上下文上的事件就和聚合根的行为接口等价了。因而可以起到这种等价作用的事件，就是领域事件。

按照这两个办法过滤出的领域事件，在逻辑上就是一个恰当的粒度，可以帮助我们将业务中的流程维度在领域模型上展开。

除了能够划分交互事件与领域事件之外，我们还可以通过引入时间线帮助我们细化事件与上下文的关系，这是一种很有效的建模技巧。特别是当涉及跨多子域协同的时候，按子域分离时间线可以帮助我们更好地理解子域间的交互：

凡可归类为事件建模的建模方法在底层逻辑上都是类似的，都是通过寻找事件，以及事件背后的领域概念，完成对领域概念的挖掘和建模。

不同方法之间的差异就在于两点：以何种逻辑发现事件；事件如何与模型结合。抓住了这两点差异，我们就能很好地理解不同的事件建模法了。事件建模法是一种元方法（Meta Methodology），当你熟练使用之后，可以根据业务需要发明自己的方法。当然在那之前，你还需要参考学习别人是如何使用事件建模的，然后来构造自己的建模法。

接下来，我们就看一下事件风暴法（Event Storming）。它是目前最为流行的事件建模法了。请你着重体会我刚才说的这两点差异，看看它是以何种逻辑发现事件的，事件又是如何与模型结合的。

事件风暴法

事件风暴是意大利人 Alberto Brandolini 在 2012 年创造的一种事件建模方法。在形式上，事件风暴是一种互动式建模工作坊，通过将不同背景的项目参与方汇聚一堂，集思广益，从而形成有效的模型。这也是事件风暴名称的由来——因为它本质上就是一种头脑风暴（Brainstorming），按照欧美人士习惯的俏皮式命名法，将头脑（Brain）改成其建模法的关键元素事件（Event）从而得名。

从建模方法上来看，事件风暴是一种事件建模法。它以响应式编程（Reactive Programming）作为范式，通过事件、命令与策略之间的响应关系，组织逻辑。事件风暴内各主要概念的相互关联如下图所示：

乍一看这张图里有很多概念，大部分应该可以望文生义，但为了明晰概念，我还是解释一下：

行动者（Actors）是系统的使用者。这里使用者是一个相对模糊的概念，可能是现实中的人也可能是别的系统；

命令（Command）是由行动者发起的行为。它代表了某种决定，通常是事件的起因，也称作行动者触发命令（AIC，Actor Initiated Command）；

事件（Event）就是我们前文讨论过的事件；

聚集（Aggregate）就是领域驱动设计中的聚合，可以看作一组领域对象，在头脑风暴阶段可以泛指某些领域概念，不需要细化；

系统（System）指代的是不需要了解细节的三方系统。因为不需要了解细节，所以我们可以将它们看作一个整体；

阅读模型（Read Model）用以支撑决策的信息。通常与界面布局有关；

策略（Policy）是对于事件的响应，通常表示不属于某些聚集的逻辑。通过策略可以触发新的命令，由策略触发的命令，被称作系统触发命令（SIC，System Initiated Command）。

对于图中如此多的颜色我也要多说一句，事件风暴法是一种头脑风暴的形式，头脑风暴自然离不开 3M 便条纸（Post-It）。而我们尽可能多地通过色彩的维度来表示不同概念，是头脑风暴的一个窍门。因此，事件风暴法推荐使用 7 种不同的颜色，来表示这 7 个不同的概念。

事件风暴建模的整体流程是这样的：

首先通过头脑风暴寻找领域事件；

根据事件寻找触发它的命令与行动者；

通过事件，寻找策略以及由策略触发的 SIC；

根据命令与事件，寻找产生了变化的聚合，以及新生成的阅读模型；

根据寻找到的聚合、阅读模型、事件，开始完善、细化领域模型。

可以看到，事件风暴法的建模流程遵循事件建模的大体框架，它的特点就在于通过头脑风暴发现事件，再依赖触发与响应寻找事件间的关系，通过聚合与阅读模型寻找领域模型。下面让我们仍然以极客时间专栏订阅为例，展示一下事件风暴建模法的大致过程。

我们的场景是这样的：用户发现了想看的内容，但是因为没有订阅专栏看不了，于是下单订阅专栏，完成支付之后，再次访问之前内容，就能看到了。

首先我们从事件入手，根据上面的流程，很容易就能通过头脑风暴得到关键的领域事件：内容请求（Content Requested）、访问拒绝（Access Denied）、订单确认（Order Placed）、订单支付（Order Paid）、内容被访问（Content Viewed）。

这里我们不难发现一个规律，事件都是以“名词 + 动词被动式”表示的。在寻找到事件之后，我们需要判断哪些是 AIC，哪些是 SIC。AIC 由命令触发，而 SIC 则由策略触发。在上面这些事件里，访问拒绝和内容被访问是 SIC，其他都是 AIC。我们可以根据这个，寻找到它们各自对应的触发源：

你会发现，命令的命名与事件正好相反，是“动词 + 名词”以表示对应事件的源头。策略的命名则按对应的业务逻辑命名即可。在这里我们称之为内容访问策略（Content Access Policy）。

确定了事件的源头，我们就可以开始寻找事件发生过程中，访问了哪些聚集，事件完成后生成了哪些阅读模型。

聚集在当前阶段并不需要细化为具体对象，可以泛指与某些功能有关的领域概念集合。比如在 Pay Order-Order Paid 这一组命令与事件中，我们可以知道它一定访问并修改了与订单（Order）相关的领域概念，那我们可以使用聚集 Order 泛指这些概念，而无须细化。

这个过程并不一定需要按照顺序，哪些事件对应的聚集比较明显，就可以先从它们入手。比如在上图中，与内容访问策略相关的阅读模型并不是非常明显，那不如先不考虑它，而是从其他更容易得出结论的部分入手：

Place Order-Order Placed 产生的阅读模型是订单（Order），Pay Order-Order Paid 产生的阅读模型是订阅（Subscription）和支付（Payment）。这里我们会发现阅读模型与聚合有重合的地方。订单在 Pay Order-Order Paid 中成了聚合，而在 Place Order-Order Placed 里成了阅读模型。

阅读模型是比聚合更为宽泛的概念，可以看作聚合的超集。它既可以是聚合，也可以是为特定界面或报表生成的特定数据信息。这种做法是随着 NoSQL（Not Only SQL）兴起的一种反思——命令与查询职责分离（CQRS，Command Query Responsibility Separation）。

那么写入数据（Command）与查询数据（Query）一定要用同一个模型吗？原味面向对象的回答：是，应该一样。而越来越多的实践者则发现，分开可能更好。毕竟两者对于逻辑一致性的诉求不同，分开处理能得到更好的结果。因此在事件风暴建模法中，阅读模型是包含写入与查询的所有数据形态的总集，而聚合只是阅读模型中符合对象风格的子集。

当我们找到一些阅读模型后，再回过头去看之前留白不易处理的部分，就豁然开朗了。内容访问策略需要使用订阅阅读模型，决定了哪些内容对哪些用户可见：

在获取了聚合和阅读模型之后，我们就可以开始细化领域模型了。这时候方法与原味对象方法就没有什么不同了，我就不再赘述了。

通过这个简单的例子，相信你对事件风暴法的基本流程已经有所了解了：它通过头脑风暴发现领域事件，以“对于事件的响应”为主要维度寻找事件间的关联；它通过阅读模型和聚集发现事件与领域模型之间的关联。不难发现，事件风暴法是一种简单明快的事件建模方法。

小结

这节课我们先讲述了事件建模法作为一类方法的共同性质，也就是通过事件表示行为，再根据事件顺序去组织业务逻辑。并总结了这么做的好处：通过记述性描述发掘需求，不知不觉间构造领域模型，并完成统一语言。

之后我们以事件风暴法为例子，看到了这些原则在具体方法中是如何应用的。那么下一节中，我们来讲解另外一种事件建模法——四色建模法，在那之前我希望你思考以下问题。

思考题

事件的表现形式只能是事件吗？既有事件又存在阅读模型不是一种冗余吗？除了“事件 - 响应”外，还有什么办法可以展开维度？

欢迎把你的思考和想法分享在留言区，我会和你交流讨论。同时，我也会把其中不错的回答进行置顶。我们下节课再见！

09｜怎么才能更有效地获得事件流？

你好，我是徐昊。今天我们来聊聊四色建模法（4 color modeling）。

在事件建模法中，获取事件流（Event Stream）是建立领域模型的切入点。而事件流在多大程度上可以反映业务的现实，影响了最终模型的质量与功用。不同的事件建模法，在获取事件流的方法上也有所不同。

上一节课我们讲解的事件风暴法，是通过头脑风暴的形式来获取事件流。今天我来介绍另一种事件建模法：四色建模法。它的起源有点复杂。最早的源头是 Peter Coad 在 1992 年研究对象模式时，为了提取可复用的对象组件而发明的四原型法。

在 1997 年的时候，作为一种补充策略，Peter Coad 开始提倡在模型中引入不同的颜色，从而将模型的动态交互展示在静态的模型中，形成了彩色建模法（Color Modeling）。

而后，我自己在 2005 年左右，将事件建模与彩色建模法结合形成了四色建模法，用以获得更具业务含义的模型，以便直接使用模型作为统一语言。

毋须讳言，四色建模法是一种我偏爱的方法，今天我们就来看看它的特点。我会先讲一下使用头脑风暴获取事件流的不足，然后再介绍四色法做了哪些改进。

头脑风暴法的不足：成功取决于收敛逻辑

在大多数事件建模法中，通过头脑风暴获取事件流是主流的做法。比如我们在第 8 讲的事件风暴法，就是这样一种以头脑风暴为主的事件建模法。

头脑风暴是一种通过发散 - 收敛荟萃信息的方法。它可以分为发散和收敛两个阶段：

在发散阶段，主持人（Facilitator）通过引导团队成员，收集他们的想法，产生事件；

在收敛阶段，主持人则需要根据某种逻辑过滤收集到的事件，形成领域事件。

如下图所示：

要想更加有效地获取领域事件，关键在收敛而不在发散。在发散阶段，所有参与者可以天马行空。不过这样的方法在产生有效信息的同时，也会产生大量的噪音。但是在收敛阶段，则会按照某一逻辑主线，合并相似概念，过滤无用信息。那么我们可以很容易地想到，如果主持人采用不同的逻辑去收敛事件，最后获得的结果也可能不尽相同。

然而在事件风暴法，以及其他以头脑风暴为主的事件建模法中，对于用于收敛的逻辑大多语焉不详，因而极度依赖主持人的经验与自觉，最终结果自然就会存在一定的随意性。这也使得事件风暴法变成了那种“一学就会，一用就废”的方法。有经验的老手越用越顺手，而初学者往往不得要领。

所以有一些事件建模方法就干脆彻底抛弃了发散 - 收敛这种头脑风暴式的弱分析方法。既然最终事件流的质量取决于收敛逻辑，那么我们为什么不直接从收敛逻辑出发，通过引导 - 分析直接获取事件流呢？的确可以这么做，而四色建模法也正是这样一种从收敛逻辑出发的强分析法。

四色法的核心逻辑：从收入流与成本结构中寻找事件

四色法主要通过三个关键逻辑寻找领域事件。这三个逻辑源自企业运营与管理实践：

如果有现金收入，表示承担了义务。拿钱办事，需要收集证据，说明义务履约成功。

如果有现金支出，表明拥有权利。花钱消灾，需要检查对方是否按时履约。

对于没有现金往来的，可以通过目标 - 实际对比，产生类似履约的约束。所谓目标 - 实际对比，就是设立一个目标 / 计划，然后追踪实际执行的结果。

作为四色建模法的创始人，我可以给你仔细讲一讲我为什么会选择这三个逻辑作为发现领域事件的主要逻辑。

这三个寻找事件的逻辑并不是从技术视角臆造的，而是在现实业务中真实存在的，并且还是商业规则的底层逻辑。所以从它们出发，可以更容易地得到与现实业务相符合的事件流。

其中前两条（现金收入承担义务和现金支出表示权利）源自现金日记帐（Cash Journal），与收入流（Revenue Stream）直接相关，最后一条则与成本结构（Cost Structure）有关。

这里我们需要有一点点基础的财务背景知识，帮助我们理解企业在运营与管理时的视角。不过我们并不需要知道所有的细节，能够在宏观上有所了解就可以了。

在现实中经营一段真实的业务，财务是基本的视角。在企业财务制度中，有“管帐的”和“管钱的”两种分工。会计是通常所谓“管帐的”，无论是总账会计还是明细账会计，都是负责为企业的经营管理与决策提供核算资料；而出纳则是“管钱的”，主要负责管理企业票据、货币资金、有价证券的收付与核算。

日记帐（Journal）是出纳用以记录收付与核算的帐本，现金日记帐就是与现金有关的日记帐。企业与顾客之间、企业与企业间的财务往来，都通过现金日记帐体现。可以说，现金日记帐是收入流的重要组成部分，所有与收入流相关的业务活动，最终都会在现金日记帐上有所体现。因而也是我们需要重点关注的问题域。与之相对的是总账（General Ledger），它则根据不同的科目，对业务单据进行汇总和核算。

在理解了日记帐和总账的区别之后，我们就能明白该怎么从财务角度来看待业务：

业务活动中可以有多种多样的行为，其中大部分行为从财务角度来说都无关紧要；

但是如果出现与现金有关的活动，就必须记录在日记账中；

一旦记录在现金日记账内，那么其关联的原始单据，也会根据科目设置进入总账。换句话说，日记帐内记录现金结果，总账内记录重要的过程单据。

最后再由总账形成试算平衡表。

业务活动通过日记帐和总账反映到试算平衡表的过程如图所示：

从事件建模的视角不难发现：日记帐中记录了关键事件，总账中记录了这个事件的缘起和后续。由此为依据，我们就能从运营业务而不仅仅是设计系统的角度，去发掘关键的领域事件。因此抓住了日记帐上的事件，就能围绕它建立起前后关联的事件流。这也是为什么，我会将现金权责作为四色建模法主要逻辑的原因。也就是我们开始讲的三个逻辑中的前两条。

事实上，在实际经营业务时，除了财务视角，还有流程视角。管理理论认为，企业可以将其核心经营行为，转化为流程（无论是对外的业务活动，还是对内的工作流）。那么流程中产生的经营开支（Opex）就构成了成本结构中很重要的一部分。然后再通过为流程的执行设定 KPI，控制企业经营的效率。大致流程如下图所示：

那么很自然，作为设定 KPI 的人需要知道 KPI 是否被严格地履行，而 KPI 的责任人也需要不断地收集证据，表示自己的进度。

而 KPI 的设定，是源自对于成本结构的优化。支撑这种考核与收集，是很多软件系统的本质目标，也是我们需要关注的领域。所以围绕 KPI，我们同样可以建立起前后关联的事件流，也就是由所有可以证明 KPI 的证据串联起来的事件。

因而，在与总账和日记帐关联不紧密的领域，我选择了目标 - 实际对比，作为发现、串联事件的主逻辑。

总之，我们寻找领域模型，提取统一语言，做分层与隔离，目的都是为了有效地控制变化和传播。那么我们更应该从源头入手，找到业务中相对稳定的部分去建模，而不是选择一段变化的业务去稳定地构造它的模型。毕竟我们期待领域驱动设计能够为我们带来的一切优点，都依赖于尽早地从业务侧隔离开稳定与易变的部分。

那么什么是更稳定的部分呢？答案是业务模式（Business Model）。对于一家步入正轨的企业而言，在其生命周期内剧烈变化核心业务模式是小概率事件（甚至是求死之路，更多地会通过构建新的实体去承载新的业务）。

因而业务模式在企业生命周期内，是相当稳定的。而业务模式主要通过收入流和成本结构来体现。四色建模法的核心逻辑就源自收入流和成本结构，通过现金权责识别与收入流有关的事件，通过目标 - 实际对比发现与成本结构关联的事件。

这是四色建模法与其他建模法的主要不同：完全拥抱业务视角，以业务的内在逻辑作为发现事件的主要逻辑。如果业务会从财务和流程效率的角度去考虑问题，那么我们也要从这个角度出发去建模。某种程度上说，这是一个对技术人员不太友好的建模方法。这种彻底的业务化视角，可以算作我留给这个行业的一个重要遗产吧。

四色建模法的具体操作流程

下面让我们来看看四色建模法的具体操作流程。四色建模法之所以叫四色建模，主要是因为它里面存在四种主要的对象原型，且每一种原型都有一种颜色与之对应，一共有四种颜色，由此而得名。

这四种对象原型分别是：

有了前面课程的基础，你会发现这些概念并不怎么陌生，在其他建模方法中都有类似的对应。在了解了这四种基础原型之后，四色建模的具体流程是这样的：

可以看到四色建模法对最终模型的构造有更为明确的要求：

凭证必须围绕现金往来或 KPI；

凭证间必须通过关键数据项明确关联关系。

这也是为什么我说四色建模是一种强分析方法的原因：可经由明确的逻辑推倒，不需要依赖于发散 - 收敛式的探索。

那么就让我们继续以极客时间专栏为例，看看四色建模法能帮助我们得到什么不一样的模型吧。首先我们需要思考一下在极客时间专栏的模型中，有哪些现金往来（Payment 和 Commission Payment）？很容易想到的就是读者购买专栏以及专栏作者分成这两项，构造的模型如下：

如图所示，我们构造了两个凭证去表示这两个现金往来，然后分别列出它们的关键数据项：时间点和金额。接着就可以开始追溯前序凭证了。

我们先看一下这两个凭证中相对简单的一个：读者购买专栏的凭证。

它的关键数据项金额（amount）显然不可能由读者输入，而应该由前序凭证提供。想想看，支付的前序凭证是什么呢？对，是订单，因为是针对订单进行支付的。因此支付的金额应该与订单的金额相等。

而订单的金额显然也不可能是由读者输入的，那么订单的前序凭证是什么呢？是专栏报价，订单的金额应该与专栏报价相等。那报价哪里来的呢？是由编辑输入的。到此为止，我们就完成了一条完整的追溯，如图所示：

在这里我们不光记录了凭证的先后顺序，还记录了模型间的计算逻辑和时标顺序逻辑。计算逻辑自不必说，时标顺序也是非常重要的领域逻辑。

举个例子，如果某专栏目前定价 199 元，但是在今天下午 14:00 以后，会开始促销打折变成 99 元。那么读者在 13:59 分下单，根据约束 Order.placed_at > Column Quote.available_at，他必须支付 199 元。

我们记录了凭证的时间节点，如果这位读者不满意，我们可以根据追溯信息有理有据地告诉他，我们并没有占他的便宜。要知道，所有牵扯到财务往来的业务，都必须经受得起这样的考验，这样才能保证业务经营的顺畅。这也是为什么我们要在事件发生阶段就明确追溯关系。

在寻找完前序凭证之后，Payment 有什么履约凭证吗？那我们要看一看，从客户那里收到的钱表示了什么权责。根据业务上下文，读者购买的是阅读专栏的权利，那么我们就需要一个凭证表示读者可以在后续阅读对应专栏里的内容。

于是我们可以引入凭证订阅（Subscription），它具有关键数据项开始时间（started_at），也就是从何时开始，用户可以阅读对应专栏的内容。我们知道这个时间必须晚于支付完成，不能允许读者在支付完成前就阅读专栏，哪怕早一毫秒都不行：

现在我们再来看专栏作者分成，这里的难点在于关键数据项金额（amount）。显然不可能让作者自己输入要提取多少分成。这个分成通常是按某个规定好的时间段提取，比如每月一次，或者每季度一次。

那么我们可以写出 amount 的计算逻辑（伪代码）。按照账期找到所有账期内读者购买的凭证，求出总销售金额，然后再按约定好的分成比例，计算作者的分成：

amount = sum(payment.amount) * percentage where payment.paid_at in (last paid, payment days)

不过这里就有一些数据项来路不明了：分成比例（percentage）、账期日（payment days）和上次分成付款日（last paid）。那么如果我们往前追溯，可以发现包含分成比例、账期日的凭证，是专栏撰写合同，而上次分成付款日则是通过计算得到的（分成支付凭证中最晚的那个。如果没有，则默认是合同签约日）。于是我们可以得到：

到此为止，我们获得了一条由通过关键数据项彼此关联的凭证链，同时也是领域事件流。这条凭证链表示了从作者签约、专栏定价、读者付费、作者分成的全过程，捕捉了极客时间专栏这项业务的收入流，我们称之为业务的脊梁（Backbone of the business）。

不过在继续进行下一步之前，我们可以先来验证一下这个业务的脊梁的有效性，看业务能否顺畅进行。首先我们可以将所有的凭证，转化为真实的单据；然后通过角色扮演游戏，让业务方模拟业务的经营。如图所示：

在验证的过程中，除了正常流程之外，我们主要需要模拟的是各种业务异常引起的追溯的例子，看看被业务脊梁捕捉的数据是否能够保证我们对权责的证明。比如我们之前讲到专栏定价修改、读者不满意的情况。类似的还有作者对分成不满意，质疑分成结果，等等。

这是四色建模另一个不同于其他建模方法的地方：它认为软件系统是业务流程的加速和优化，但是模型是要用以支撑业务运营的，并不仅仅为了构造软件而设计的，因而天然地更容易被业务方接受，成为统一语言。

找到并验证了业务脊梁，剩下的事情就简单了。首先寻找与凭证相关的角色，然后再寻找能够扮演这些角色的参与者，以及与凭证相关的标的物，如图所示：

我们只需要按照关键数据项间的关联，将模型连在一起，稍加润色，补充一下描述对象，就能得到领域模型了：

围绕 KPI 的建模流程大同小异，这里就不再给出例子了。你可以留作思考题自行练习。

小结

这节课我们讲述风暴式获取事件流的隐藏弊端，那就是在没有明确的收敛逻辑下，得到的事件流具有较大的随机性。而且从实际操作的角度上看，也会让风暴法过度依赖主持人的经验与能力，结果难以保证。

而后介绍了由我发展创立的四色建模法，以及它是如何从业务的基础逻辑，也就是收入流和成本结构出发，建模业务的经营，并从中提取领域模型的。多年来，我收到过很多对四色法的反馈。其中最多的声音，就是建模建出了职业经理人的感觉。因为它更容易从纯业务角度说服业务方，采纳模型，作为统一语言。

通过第 7、8、9 这三节课，我们讲解了为什么我们希望使用模型作为统一语言，以及在这个方向上行业作出的努力和尝试。总体的思路就是在模型中展开业务维度、通过头脑风暴共建、严格依照业务经营逻辑建模等。每个方法各有优劣，请荟萃使用。

思考题

请你围绕专栏运营的 KPI：用户转化率（经过营销活动最终购课的比例）为例，建立相应的模型。

相信通过建立模型，你会对这节课的知识掌握得更牢固。同时，也欢迎把你建立好的模型分享出来。当然，如果有什么疑问或想法，也可以在评论区留言，我会和你交流。

10 | 将模型实现为RESTful API（上）

你好，我是徐昊。今天我们来聊聊如何将模型映射为 RESTful API。

通过前面九节课的学习，你应该已经学会了如何通过几种设计模式避免架构约束对模型的影响（4-6 节），也学会了几种不同的建模方法以构造业务模型（7-9 节）。然而我们之前讲的内容，主要的着眼点在于如何获得模型，以及如何组织代码。接下来，就是通过模型获得 API，从而将模型作为业务能力，暴露给其他消费者调用。

在前面的课程中，我们一直都在强调领域驱动设计受到行业热捧的一个原因：寻找到一个在软件系统生命周期内稳固的不变点，由它构成架构、协同与交流的基础，帮助我们更好地应对软件中的不确定性。

API 作为对外暴露的接口，也是需要保持高稳定性的组件。我们希望 API 最好能像领域模型一样稳定。于是通过领域模型驱动获得 API 的设计（Domain Model Driven API Design），就成了一种非常自然的选择。

那么今天我们就来讲一讲怎样构建领域驱动的 API 设计，以及为何我们会选用 RESTful API。

什么风格的 API 适合作为模型 API？

想要实现领域模型驱动 API 并不困难，但是我们需要选择恰当的 API 风格：要从数据角度，而不要从行为角度去构建 API，以保证构建的 API 能够和领域模型结合得更加紧密。

从行为角度和数据角度构造 API 存在什么差异呢？让我们通过一个例子来解释它，我想你也猜到了，对，还是极客时间的例子。我们在讲解催化剂方法时，展示过它：

如果从行为角度进行 API 设计，那么我们的着眼点就应该从人机交互上入手，寻找系统提供了什么服务，并针对这些服务进行 API 设计。

于是在这个例子中，我们就应该从“送朋友专栏”“订阅专栏”“用户注册”“阅读内容”“专栏评分”这些交互入手，建立对应的 API。

我们很自然可以想到，为每一个交互定义一个接口，从而得到服务的定义（以 gRPC 为例）：

service GeekTime {
    rpc SendGift(SendGiftRequest) returns SendGiftReply {}

    rpc SubscribeColumn(SubscribeColumnRequest) returns SubscribeColumnReply {}

    rpc RegisterUser(RegisterUserRequest) returns RegisterUserReply {}

    rpc ReadColumn(ReadColumnRequest) returns ReadColumnReply {}

    rpc RateColumn(RateColumnRequest) returns RateColumnReply {}
}

message SendGiftRequest {
...
}

..

这种 API 风格被称为 RPC 风格（Remote Procedure Call Style）。RPC 的雏形出现于 1960 年代末，在 1980 年代逐渐成为分布式系统的默认风格（1981 年，被 Bruce Nelson 正式命名为 Remote Procedure Call）。它是我们最熟悉，也是最古老的一种远程调用风格。RPC 风格的优点是简单直接，提供什么功能都在接口里说清楚。

然而从模型的角度上来讲，除非你严格地使用催化剂方法，否则 RPC 风格跟模型之间有所隔阂，也就是说结合得不够紧密。主要表现在如下两个方面。

首先，RPC 的方法名并不来自于领域对象。已经学过催化剂方法的你应该知道，交互是一种我们将隐藏在领域模型中的业务维度展开的方式，目的是让业务方能够理解不同的业务流程是如何作用于领域模型的。因而实际上，交互是业务维度在领域模型上的补充。所以，我们从交互入手，获得的 RPC 风格 API 并不是源自领域模型的，而是源自其他业务维度的。

其次，RPC 风格的 API 的消息与返回信息通常也与领域模型无关，而是围绕着交互给出的具体反馈。比如，上面例子中阅读内容这个 API 接口（ReadColumn），其返回值 ReadColumnReply 很大可能是仅仅包含对应篇章的内容，而不会包含与之关联的作者信息。

可以看到，在 RPC 这种风格的 API 中，方法名与领域模型无关，消息与返回值也与领域模型无关。因而我们可以说，它就不是领域模型驱动的 API 设计。

当然严格意义来讲，我们可以说 RPC 风格的 API 是统一语言驱动的 API 设计。不过经过 7-9 节的学习，你应该已经相信，我们能够使用领域模型取代统一语言了，于是通过领域模型而不是统一语言驱动 API 设计，便是我们努力的方向。

如果我们从数据角度构建 API 会得到什么不一样的结果呢？这个时候我们就要思考，在不同的交互行为中，需要分别修改哪些数据，以及我们要如何表示这些数据。

以最简单的用户注册为例，我们实际上是构造了新的 User 对象，并将其存储到数据库中。而订阅专栏，则是针对某个特定的 User，在其聚合 User-Subscription 中，增加了新的 Subscription。

我们永远都可以从另一个角度看待交互行为：在操作结束之后，对交互背后的模型带来了什么样的改动。这就是从数据角度去建模行为。也正是从这个角度，为我们提供了一种与模型紧密相关的构造 API 的方法。

那该怎么表示这些 API 呢？我们可以先使用 URI 表示所需修改的对象，然后再说明我们希望这个对象发生什么样的改变。

比如在用户注册的场景中，我们可以使用/users表示系统中所有的用户。那么注册新用户，就是在 /users 中增加一条新的数据。我们可以使用标准 HTTP 方法（HTTP Method）中的 POST 方法来表示。于是注册用户的 API 就变成了POST /users。

类似的，对于订阅专栏而言，我们可以使用/users/{user_id}/subscriptions表示针对某个用户的User-Subscription聚合。其中{user_id}是可以被替换的模板，我们可以将它替换为具体的用户标识符。比如/users/爱学习的鱼玄机/subscriptions，就表示“爱学习的鱼玄机”的所有订阅。

那么订阅专栏也就是在这个聚合中增加一条数据，同样，我们可以使用标准 HTTP 方法中的 POST 方法来表示。于是订阅专栏的 API 就变成了POST /users/{user_id}/subscriptions。

相应的，其他的几个 RPC 风格 API 也可以被转化为对应的数据风格 API：

阅读内容：读取Author-Column-Content聚合。GET /authors/{author_id}/columns/{column_id}/content{content_id}。

送朋友专栏：通过User-Subscription聚合，转让订阅。POST /users/{friend_id}/subscriptions。

专栏评分：通过Author-Column聚合修改评分。UPDATE /authors/{author_id}/columns/{column_id}。

可以看到，通过 URI 与标准 HTTP 方法，我们就从数据角度构造了与 RPC 等价的 API。但是不同于 RPC 方法，我们的 URI 是直接从领域模型中转化得来的，我们与 URI 沟通的方式是依赖于标准的 HTTP 方法的。也就是说，我们是从领域模型出发，驱动出的 API 设计。

其实这一点也不奇怪。我们在第 7 节就已经讲过：模型是从数据变化的角度描述业务，具体的业务流程反而是被隐藏的。那么从数据的角度去构造 API，自然比从行为的角度出发更符合领域模型的特点。因而我们讲，领域模型驱动的 API 设计需要从数据角度去构建 API。

看到这里，你可能会有一个疑问。这种操作看上去很像是直接暴露对数据库的访问啊，难道这真不是直接暴露两个表 User、Subscription 让别人直接改吗？POST /users 和 INSERT USERS 表看起来很像啊！

这里关键的不同在于，我们是在富含知识的模型之上去暴露 API 的。也就是说，并不是单纯对数据进行读写，而是通过对象间的关系，去触发富含于模型之中的逻辑。

特别是如果你仔细看我们上面讲的行为与数据的对应，就会发现大量的逻辑是通过聚合而不是单独对象实现的（订阅专栏、阅读内容等）。如果换成数据库词汇，就是多表间的逻辑，而不是单独一张表去完成的。因而我们并不能将从数据角度构造 API，等同于直接暴露数据库。

说句题外话（又到了题外话时间），RPC 风格的 API 从根源上讲，是面向过程的编程风格，也就是数据中无逻辑，要通过过程完成对数据的操作。而数据风格的 API，则更多是面向对象风格：逻辑都富集于对象中，对对象的访问就可以触发相应的逻辑。因而这种风格成功的关键与难点，就在于构造富含知识的模型。

当年微软的 ADO.NET Entity Framework 和其之上的 ADO.NET Data Service 就忽略了聚合才是领域模型的核心（而不仅仅是实体），做了一个莫名其妙的框架出来。的确简化了直接暴露数据表操作的成本，但完全没有降低核心复杂度。

好了，言归正传。我们前面描述 API 的方式，比如 POST/users，你看起来一定不陌生。它非常接近我们熟知的 RESTful API 风格。是的，RESTful API 是为数不多的从数据角度去描述 API 的方法之一。因而我们将它作为实现领域驱动 API 设计的不二法门。

为什么是不二法门呢？第一，对于 API，我们希望它易于被其他系统整合。

所谓的 RESTful API，是指符合 REST 架构风格的 API 设计。而 REST 架构风格是对互联网架构（WWW，我们使用的互联网本网）的总结与提炼。通过 RESTful API，我们能够获得相当于互联网的开放性和易于整合性。当然，前提是需要满足 REST 架构属性，比如无状态、可缓存等等。

第二，则是因为没有什么其它数据风格 API 可选。比如事件驱动 API（Event Driven API），对于单体多层架构而言，有点大材小用，所付出的成本与收益不成比例。

因而目前我们讲，在前云时代，RESTful API 是我们实现领域驱动 API 设计的不二法门。

如何将模型映射为 RESTful API？

那么如何将领域模型映射为 RESTful API 呢？总共分为四步：

通过 URI 表示领域模型。

根据 URI 设计 API。

使用分布式超媒体（Distributed Hypermedia）设计 API 中涉及的资源。

使用得到的 API 去覆盖业务流程，验证 API 的完整性。

今天我们先来讲解前两步，后两步则留待下节课再讲。

通过 URI 表示领域模型

我们先来看如何通过 URI 表示领域模型。我们都知道，除去 scheme、host 声明等信息，URI 主要通过路径（path）指示某个资源。而路径可以看作是对某种层次结构遍历的结果。

比如 URI 中有这样的一个路径/users/爱学习的鱼玄机/subscriptions，那么我们可以认为它实际是下图所示的图结构中，对右侧分支遍历的结果：

那么同样地，如果是对左侧分支遍历，就能得到另外一个路径“/users/ 会聊骚的黄庭坚 /subscriptions”。

仔细看这个图，我们会发现它是将User-Subscriptions聚合进行实例化之后，再将实例与领域对象结合在一起，得到的对象图：

那么当我们看到路径/users/爱学习的鱼玄机/subscriptions时，或者它的模板化表示形式/users/{user_id}/subscriptions时，我们就可以还原出路径背后的领域模型，也就是User-Subscription的一对多聚合关系。

通过实例化的方式，我们将领域模型展开成了对象图。然后，再在这张对象图上遍历，就能得到可以表示领域模型的 URI 路径模板。

当然，一旦我们理解了实例化和领域模型之间的关系，我们也可以直接从领域模型出发，设计 URI 路径模板。

从领域模型出发设计 URI，主要依赖的是聚合关系。因此我们需要在领域模型中寻找聚合边界，再根据聚合边界内的关系设计 URI。

比如在下面这个极客时间专栏的领域模型中，存在两个不同的聚合边界：

以 User 为聚合根的User-Subscriptions聚合；

以 Author 为聚合根的Author-Column-Content聚合。

然后我们就可以从聚合中依次设计对应的 API 模板了。比如对于User-Subscription聚合，我们可以得到如下的 URI 模板。

/users User聚合根，表示系统中的全部用户。

/users/{user_id} User的实例化，表示通过user_id标定的某个用户。

/users/{user_id}/subscriptions User-Subscriptions聚合，表示某个用户的全部订阅。

/users/{user_id}/subscriptions/{subscription_id} User和Subscription的实例化，表示通过user_id和subscription_id标定的某个订阅。

这里我们一共得到了四个不同的 URI 模板，其中两个是集合逻辑（1 和 3），两个是实体逻辑 (2 和 4)。当 URI 模板表示集合逻辑时，我们无需实例化，只需要引用模型中的概念即可。而表示实体逻辑时，我们就需要对领域模型进行实例化，将它们从概念具象化到某个特定的个体。

类似的，我们也可以将 Author-Column-Content 聚合表示为 URI 模板：

/authors Author聚合根，表示系统中所有的作者。

/authors/{author_id} Author的实例化，表示某个具体的作者。

/authors/{author_id}/columns某个作者所撰写的所有专栏。

/authors/{author_id}/columns/{column_id}作者撰写专栏的实例化，表示某个特定的专栏。

/authors/{author_id}/columns/{column_id}/contents 某个作者的某个专栏的全部内容。

/authors/{author_id}/columns/{column_id}/contents/{content_id} 某个作者的某个专栏内容的实例化，表示某个特定的课程。

看到这里你可能会问，像 /authors/{author_id}/columns/{column_id}/contents/{content_id}这样的 URI 模板不会太长了吗？我们是否需要简化一下？

答案是不需要。比起 URI 模板的长短，我们更关心这些模板是否与领域模型的结构一致。在模型中，特定的课程是通过Author-Column-Content聚合关系表达的，那么我们就需要通过如此冗长的 URI 模板来表示它。

当然，如果你觉得 URI 模板的长度过长，那么重新审视你的领域模型也是十分有必要的，看看其中是否具有不恰当的聚合关系。如果聚合关系正常，就请坦然接受它的 URI 表现形式。

根据 URI 设计 API

在根据领域模型得到了 URI 模板之后，我们就需要围绕着 URI 进行 API 设计了。这个过程惊人地简单。我们需要做的就是构造这样一张表格，表格中包含：角色、HTTP 方法、URI、业务场景这样一些条目。如下图所示：

然后，我们只需填入角色、URI，然后按照穷举法，填入所有的 HTTP 方法就可以了。比如，对于/users/{user_id}/subscriptions这个 URI，我们可以将常用的四个 HTTP 方法填入表格：

这里有个我个人的小偏好，就是通过模板化表示角色（通常的做法是以统一语言中的角色名加 _id）。比如第一行，实际我的意思是，角色是 user，并且和 URI 中给出的user_id是同一个人。当然，也可以只统一语言中的角色名。

下一步，我们就需要寻找业务方反馈，帮助我们判断对于这些由 HTTP 方法和 URI 组成的行为，是否存在合理的业务场景。假如业务方确定了业务场景的存在，那么表格中的条目就成为了 API 候选。

当然在上图的表格中，我们对表示集合逻辑的 URI 施加了 PUT 和 DELETE 方法。对于集合逻辑而言，PUT 和 DELETE 通常没有任何含义。我们只是为了说明一个过程，通过这样的枚举，不容易遗漏可能的 API 候选。

最后，在这个例子里，我们得到的 API 候选是这样的：

这个过程很简单吧。那我们再来看一个例子，这个例子里我们引入不同的角色。我们通常会将 API 候选看作是 HTTP 方法和 URI 的组合，然而调用方的角色其实也是很重要的。过程我就省略了，方法和刚才讲的一样，我们着重看一下结果：

通常来说，已经订阅的专栏就无法修改了，但是我们假设有赠送功能。于是呢，读者发起的“对于专栏的修改”，那么其实就是赠送；与之相对的，由系统管理员发起的“对于读者订阅专栏的修改”，可能就是处理退订。

看到这里，我不知道你发现没有，这个设计 API 的流程和催化剂法的变体角色 - 目标 - 实体法如出一辙。其中业务场景是目标，而实体则是通过 URI 表示的领域模型。所以这样的 API 建模过程，同时也在帮助我们展开业务维度，更好地将领域模型作为统一语言。

到此为止，我们得到了一系列的 API 候选，接下来就需要使用分布式超媒体设计 API 中所涉及的资源，以完善 API 的设计。以及使用得到的 API 去覆盖业务流程，验证 API 的有效性。这两步，我们下节课再讲。

小结

这节课我们讲述了，实现领域模型驱动 API 设计，需要选择恰当的 API 风格。也就是不要从行为角度而要从数据角度去构建 API。同时我们分析了为什么从行为角度构建的 API 和模型之间存有隔阂，而从数据角度构建的 API 能够和领域模型结合得更加紧密。

然后我们解释了为什么 RESTful API 是在前云时代，实现领域驱动的 API 设计的不二法门。一个由于其自身的特点：源自互联网架构，能提供更好的互联性；另一个则因为也没什么其他更好的选择了。

最后我们介绍了如何将领域模型实现为 RESTful API。分为四步：

通过 URI 表示领域模型；

根据 URI 设计 API；

使用分布式超媒体设计 API 中涉及的资源；

使用得到的 API 去覆盖业务流程，验证 API 的完整性。

我们讲解了前两步，后两步则要留到下节课继续。

思考题

我们在使用 RESTful API 时，经常会遇到这种情况：不同的客户端（比如手机或页面）需要不同格式的数据。那么我们有没有办法，只提供一种格式的数据，但能满足所有客户端的需求，不需要根据客户端的要求而修改，并且还不怎么影响性能？

欢迎把你的思考和想法分享在留言区，我会和你交流讨论。我们下节课再见！

11｜将模型实现为RESTful API（下）

你好，我是徐昊。今天我们来聊聊继续如何将模型映射为 RESTful API。

上节课我们学习了如何将领域模型实现为 RESTful API 的宏观步骤，分为四步：

通过 URI 表示领域模型；

根据 URI 设计 API；

使用分布式超媒体设计 API 中涉及的资源；

使用得到的 API 去覆盖业务流程，验证 API 的有效性。

并着重学习了，如何通过 URI 表示领域模型；并在得到与领域模型对应的 URI 之后，使用类似角色 - 目标 - 实体法的分析方法，获得 API 候选。那么今天我们就继续学习后面两步，通过分布式超媒体设计 API 中涉及的资源，并将使用 API 覆盖业务流程，以验证 API 的有效性。

RESTful API 是一种什么样风格的 API？

到目前为止我们得到的 API 候选，比如 GET /users 等，还不能被称作 RESTful API，而仅仅可以被叫做基于 HTTP 的远程过程调用（HTTP RPC）。那么 RESTful API 到底是一种什么样的 API 呢？

RESTful API 是指符合 REST 架构风格的 API 设计，而 REST 架构风格是对互联网规模架构（Internet Scale Architecture）的总结与提炼。这一切都源于 Roy Fielding 提出的一个问题：既然互联网（Internet）是人类迄今为止构造的最大的软件应用，那么到底是什么样的架构原则，支撑了如此规模的异构且互联的系统呢？我们能从中学习到什么，以帮助我们更好地构建软件？

Roy Fielding 将互联网定义为分布式超媒体信息获取系统（Distributed Hypermedia Information Retrieval System）。也就是由超媒体描述的、分布式的信息系统。在这个系统中，信息分布在不同的服务器中，并由超媒体联通。换句话说，分布式超媒体是互联网的集成策略。

因而我们必须理解这种集成策略给整个系统带来了什么影响，这样才能在实现 RESTful API 时，尽可能发挥它的优势。

在我看来，这种集成策略实现了客户端与服务器之间的渐进式服务消费（Progressive Service Consumption）。而这种渐进式服务消费，在客户端的多样性和 API 的稳定性之间，取得了完美的平衡。

也就是说，它让我们可以用同样一套 API，去对应具有不同诉求的参差多态的客户端们（我将这种 API 称为幸福 API，因为罗素说“参差多态乃是幸福的本源”。那么能够支持参差多态的客户端的 API，也就是幸福 API）。

让我们回想一下，当你访问网络上的信息时，你会通过 URI 指定需要访问的页面。页面中会关联 CSS、JavaSccript 等所需的资源，并包含其他页面的链接。浏览器会下载对应的 CSS、JavaSccript，完成界面的渲染。你也可以通过页面中的链接，去访问其他关联的页面。

在整个过程中，你无需考虑 CSS、JavaSccript 来自哪里，关联的页面是否来自同一个网站。因为从始至终，你都将这些关联的资源看作是一个完整的资源在消费。这是我们习以为常的场景。

然而如果我们从系统集成的角度重新看待这个过程，就会有不同的体会。网页中对其他资源的引用，实际上表示了对其他服务计算结果的整合。一个典型的例子就是 Google Analytics，虽然它是以 js 的形式出现。但当页面引用到了这个 js 资源时，实际上完成的是对 Google Analytics 服务的整合。

而当你点击页面中包含的链接时，浏览器会为你请求对应的网站资源，并将计算结果（大概率是另一个页面）在浏览器客户端里展示。这也可以看作是对其他系统的集成。更具体地讲，也就是浏览器客户端通过延迟（Lazy）调用的方式，整合了其他系统提供的服务。

所以无论是 css、js 这样的资源，还是通过超链接引用的其他页面，都是服务端返回的页面中描述的。而客户端则根据服务端提供的描述，选择恰当的时机，访问这些关联的资源，也就是整合其他系统提供的服务。这样的系统集成方式，我们称之为由服务器指导的客户端侧集成（Server-side guided client-side integration）。如图所示：

由于对于关联资源的访问是由客户端发起的，并不是由服务器端强制的，那么客户端可能出于某些原因（安全、计算资源受限等）选择不去访问某些关联资源。

比如，在智能手机大规模流行之前，大部分手机上的浏览器不具备完整执行 js 的能力，因而很多浏览器选择不加载任何 js 资源。

从这样的角度出发，我们就会发现，CSS、JavaSccript 或者页面中通过超链接引用的其他页面，都可以看作是对当前页面的增强。比如 CSS 增强了当前页面的视觉效果，js 增强了当前页面的交互，页面中的超链接为页面中的内容提供了额外的信息。于是通过超媒体格式，我们不仅可以描述当前的服务，还可以描述增强的服务。

比如，你在极客时间专栏里阅读这篇文章，那么当前的服务是让文章在浏览器中显示，供你阅读，这是默认服务。那么怎么增强它呢？我们可以提供语音的版本，也可以提供视频，还可以提供大字打印版等等。那么我们可以在超媒体格式中，描述这些关联的资源。如下 HTML 代码所示（HTML 是最常见的超媒体）：

<article>
    <title>11 | 将模型实现为RESTful API（下）</title>
    <p>....</p>
    <link href="http://voice.geekbang.org/chapter11.mp3" rel="voice"/>
    <link href="http://video.geekbang.org/chapter11.mp4" rel="video"/>
    <link href="http://easy-reading-print.com/13678328" rel="print"/>
</article>

在这段代码中，我们通过 link 标签，表示了这篇文章与其他资源的关系。可以看出，我们关联了“http://voice.geekbang.org/chapter11.mp3”“http://video.geekbang.org/chapter11.mp4”和“http://easy-reading-print.com/13678328” 。它们与当前文章的关系分布为 voice、video 和 print。此时，客户端可以根据这些额外的信息，在不同的情景下选择加载哪些额外的资源。

假设此时我们有三个客户端，分别是用于阅读的某阅读器，用于听书的某播放器，以及某个打印机。那么这些客户端只需要拿到这段超媒体，然后各自选择对应的服务即可：

阅读器按照 HTML 格式显示内容，忽略其他关联的服务；

播放器从超媒体中提取由voice链接的资源，去加载对应的 mp3，忽略其他 HTML 文本和服务；

打印机从超媒体中提取由print链接的资源，送去打印，忽略其他 HTML 文本和服务。

可以看到，我们使用了同样的 API，支撑起了完全不同的客户端。而这些客户端彼此之间也并不知道对方的存在，它们只关心自己关注的资源。换言之，通过超媒体描述的增强服务，让客户端和服务器之间形成了一种协商与匹配的关系，也就是我们所说的渐进式服务消费。

所谓渐进式，指得是按需索取，而不要求消费全部关联的服务。也正是渐进式的消费方式，促使互联网成为了一个异构、松耦合且开放的系统。同时这也正是我们希望自己所构建的系统能够具有的特质。

所以通过分布式超媒体的设计，实现这种渐进式服务消费，也就是我们从 HTTP RPC 过渡到真正的 RESTful API 的重中之重。

这里再讲三个题外话。第一个，以渐进式思路构造的超媒体格式，是一个被极度低估了的思考模型。

它的一个具体应用——渐进增强（Progressive Enhancement），是开发前端应用的最佳思考方式，同时也是诸多 JavaScript 框架的核心思路。掌握了它，你就能迅速成为资深前端工程师。

我仍然记得我是如何在领悟了渐进增强之后，在 4 个小时之内，从前端白丁成为前端架构师的。以及，多年前那个初秋的午后，郑晔同学（对，就是在极客时间已经写了三个专栏，正在努力写第四个的郑晔同学）在西安高新区某公交站等出租车时，跟我说起前端学习是如何如何之零碎，不如后端知识系统。当我用了 20 分钟给他讲了渐进增强，然后他几乎是马上就全都会了。

第二，如果通过超媒体关联的资源是代码，那么我们就能指引客户端按照服务端设计的行为，去执行和消费超媒体资源了。

比如，我们现在有一个订单，需要通过支付网关支付。而支付网关可能有很多不同的选择，网银啊、微信啊、支付宝什么的。那么我们可以通过超链接关联支付网关的客户端：

<article>
    <title>订单详情</title>
    <p>....</p>
    <link href="http://payment.com/12358921" rel="payment"/>
    <link href="http://payment.geekbang.org/bank.js" rel="payment-client"/>
</article>

这意味着，客户端不需要知道使用的是哪个支付网关，只需要加载payment-client关联的资源，并通过它完成支付即可（当然我们需要提前约定 JavaSccript 的加载策略和入口函数）。于是，服务端进一步控制了客户端的行为，不仅仅是通过超媒体指引了增强的服务，连带如何消费这些服务也提供了指引。

这就是按需代码（Code-On-Demand），通过它，我们能够在将服务器端的计算压力转移到客户端的同时，也不会丧失自己对计算的控制。此外，通过它，也进一步解耦了客户端与服务器之间的耦合。客户端不需要理解服务器端的逻辑，只需要按照指示执行代码即可。可以这么说，按需代码这个技巧，对于需要高灵活度的 API 设计是极其重要的。

第三，这种完全由服务端控制的客户端行为，也被称作 HATEOAS（Hypermedia As The Engine Of Application State），即超媒体作为应用程序状态的控制引擎。在 Richardson 成熟的模型中（Richardson Maturity Model），是最高成熟度的 RESTful API。我想经过今天的学习，你应该完全理解其背后的原因了。

好了，我们言归正传，接下来就看看如何使用分布式超媒体来设计 API 中的资源，以实现 HATEOAS。

如何将模型映射为 RESTful API？

通过分布式超媒体设计 API 中涉及的资源

首先我们需要知道什么样的格式属于超媒体。那么包含超链接的格式都可以看作超媒体，因此 HTML 是超媒体格式。在前面的例子里，HTML 中的 link 是表达关联资源的主要手段，它的构成要素有两个：

指向关联资源的链接（href）；

与主资源是哪种关联关系（rel）。

这两个是构成超链接的必要因素，缺一不可。同时，只要具备这两个要素，都可以被看作超链接。比如如下 HTML 元素，可以当作超链接：

<a href="http://payment.com/12358921" rel="payment"/>

<area shape="rect" coords="184,6,253,27" href="http://some-place.com" rel="center" />

而我们常用的 JSON 和 XML 并没有提供默认的链接格式，所以它们并不是超媒体。于是我们在设计 RESTful API 的时候，要使用 HAL（Hypertext Application Language），而不是原味的 JSON 或者 XML（Vanilla JSON or Plain XML）。

HAL 目前还是 Internet 标准的一个提案，你也可以自定义自己的 XML 和 JSON 超媒体格式，不过我估计最终结果和 HAL 应该差不多，所以还不如直接用 HAL 来得简单。

XML 的 HAL 基本上就是把 HTML 的 link 作为标准标签引入了 XML，而 JSON 的 HAL 则定义了 _links 结构来表示链接。如下所示，就是 XML HAL 和 JSON HAL 规范中给出的同一个资源的示例：

Content-Type: application/hal+xml
<resource rel="self" href="/orders/523">
    <link rel="warehouse" href="/warehouse/56"/>
    <link rel="invoice" href="/invoices/873"/>
    <currency>USD</currency>
    <status>shipped</status>
    <total>10.20</total>
</resource>

Content-Type: application/hal+json
{
  "_links": {
    "self": { "href": "/orders/523" },
    "warehouse": { "href": "/warehouse/56" },
    "invoice": { "href": "/invoices/873" }
  },
  "currency": "USD",
  "status": "shipped",
  "total": 10.20
}

解决了格式问题，我们来看看怎么使用超媒体设计 API 中的资源。这里的关键，正如我们刚才所讲，是如何通过超链接构成渐进式服务消费。

我们仍以极客时间专栏的模型为例：

首先我们看一下聚合根的超媒体描述。对于一个 User 实例而言，我们可以使用如下的 HAL JSON 来描述它：

{
    "_links": {
        "self": { "href": "/users/19" },
        "subscriptions": { "href": "/users/19/subscriptions" }
    },
    "username": "爱学习的鱼玄机",
    ...
}

这段 HAL JSON 中包含两个超链接：self 和 subscriptions。self 表明获取当前资源的 URI，我们会将这个 URI 称作主 URI（Primary URI）。这个 URI 从概念上来讲，和 ID 是等价的，也就是可以用于唯一定位当前资源的标识符。同时，self 也是用以缓存的 URI。

subscriptions 表示了聚合关系 User-Subscriptions，也就是指示了如何寻找被当前 User 聚合的 Subscription。对于聚合根而言，需要为所有的聚合对象提供链接，以指示如何获取这些聚合对象。

你可能会问，self URI 有什么用？如果单独对一个 User 实例而言，可能用处不大。但是在集合资源中，self URI 就是不可或缺的了。比如对于/users，我们使用如下的 HAL JSON 描述：

{
    "_links": {
        "self" : "/users"
    },
    "_embedded": {
        "users": [
            {
                "_links": {
                    "self": "/users/18"
                },
                "username" : "会聊骚的黄庭坚",
                ...
            },
            {
                "_links": {
                    "self": "/users/19"
                },
                "username" : "爱学习的鱼玄机",
                ...
            },
            ...
        ]
    },
    "total": 43215,
    ...
}

可以看到，我们通过_embedded表示了所有的用户。不过这里还有一个需要我们权衡的地方。如果 User 对象有很多属性（比方说包含年龄、家庭住址、个人状态等），那么我们需要在/users的 HAL JSON 中包含所有这些属性吗？

如果包含所有属性，那么这个 HAL JSON 的体积可能会变得非常巨大；而如果不包含所有的属性，那么在客户端需要使用其中某些数据的时候，就会拿不到相应的信息。

这时候就该列表中每一个 User 对象中包含的 self URI 发挥作用了。我们可以把集合资源和其中的独立资源，看作是渐进式的两种不同服务。

也就是说，我们可以把在集合资源中包含的数据，看作是更基础的服务，像常用数据这样，可以满足大部分客户端在通常情况下对用户数据的需求。而其中每一个 User 对象中包含的 self URI 所指示的服务，我们可以将其看作是增强服务，也就是全量数据。

于是，如果某个客户端需要不包含在常用数据中的信息，那么它可以通过 self URI 去获取全量数据。也就是说，我们将常用数据和全量数据设计成渐进式消费的两种不同的服务。并通过分布式超媒体格式，描述了它们之间的关联。

这样我们就可以让同样一份超媒体描述，来支撑不同的客户端了。与此同时，我们也不需要为不同的客户端提供不同格式的数据信息。当然，你可能会问，这么做，难道不会有性能问题吗？答案是有，不过没关系，因为有缓存。

REST 架构大量依赖缓存来缓解性能问题。我们甚至可以说，是否能够有效地利用缓存，会决定 REST 架构的成败。这也是为什么我们强调主 URI 应该是用于缓存的 URI。也就是在构造资源的时候，我们要将缓存当作必须考虑的特性，详加设计。

让我通过一个例子，来看看缓存会怎样影响我们的设计决策。这也是非常常见的一个场景——分页。

你可能注意到了，在前面的 HAL JSON 中，/users一共有 43215 个用户，如果将所有的用户都包含进来，就会产生极大的 HAL JSON 文件，因此我们需要分页处理。好在这并不难：

{
    "_links": {
        "self" : "/users",
        "next" : "/users?page=3"
    }
    ...
}

我们可以通过 next 链接表示后面的一页。那么我们怎么才能通过缓存，使得当有新用户注册的时候，大部分页面的缓存都不失效呢？

一个有效的策略是，永远不缓存/users页面，也就是/users永远表示新近注册的用户。如果它们达到分页上限时（比如 50），就生成新的页面 id，并缓存它，然后继续使用/users等待新注册的用户。如下图所示：

那么前面几页的 HAL JSON 可能就是这样子的（如下所示）。而且你会发现缓存下来的页面，我们永远不需要修改其中的内容：

// 以下为未被缓冲的信息
{
    "_links": {
        "self" : "/users",
        "next" : "/users?page=4"
    }
    ...
}

// 以下为被缓冲的信息
{
    "_links": {
        "self" : "/users?page=4",
        "next" : "/users?page=3"
    }
    ...
}

{
    "_links": {
        "self" : "/users?page=3",
        "next" : "/users?page=2"
    }
    ...
}

那你可能会有疑问，如果客户端与服务端对分页的要求不同，这时候该怎么办？比如客户端需要每页显示 50 条，而服务器端则需要按每页 20 条缓存？

答案是：服务器永远不需要考虑客户端的需求。把客户端的额外需求，当作渐进式服务消费的需求，那我们只需要提供对应的链接即可。

也就是说，只要保证页面间的链接，剩下的就交由客户端自行处理。这就是互联网架构的精髓，将集成与订制推向客户端，从而保持服务端的稳定。

将 API 映射回业务流程

在我们将 API 中涉及的资源都使用分布式超媒体描述了之后，我们就获得了完整的 API 定义。而且，借由分布式超媒体，我们可以达成 HATEOAS，实现渐进式服务消费，从而充分分离客户端与服务端的耦合与依赖。此外，如果你充分考虑了缓存策略，性能也不会构成任何问题。

那么剩下的最后一步，就是将 API 重新映射回所需要支撑的业务流程或者用户旅程之中，与业务方一起验证这些 API 是否能够满足所有的需求。示意图如下：

如果我们将 API 作为模型的另一种表现形式，那么映射会在流程中与业务方验证，也就和磋商形成统一语言的过程一致了。我想你已经有足够的了解了，就不再赘述了。

小结

这节课我们讲述了什么是分布式超媒体，以及为什么它是互联网架构的默认集成模式。以及我们如何通过使用分布式超媒体形成渐进式服务消费。并以此为核心理念，讨论了如何描述 API 中的资源。此外，我们还重点讨论了集合资源，以及如何通过缓存解决分页等问题，并通过一个分页的例子展示了如何将客户端与服务端充分解耦。

至此，你应该学会了在单体分层架构模式下，如何将模型的能力通过 RESTful API 暴露。那么接下来，我们将进入云时代的业务建模，看看新的架构约束到底将会怎样影响我们建立业务模型。

思考题

你是如何理解云平台的？在你看来，云平台的哪些特性会影响我们的业务模型？

欢迎把你的思考和想法分享在留言区，我会和你交流。那么旧约部分到这里就结束了，我们新约篇章再见！

说点题外话01｜好耦和与坏耦和

你好，我是徐昊。今天我们专门来说点题外话（都知道，这门课里的题外话，都不是题外话）。

目前我们正好完成了旧约部分的学习。也就是说：

通过 1-3 讲，我们重新梳理了什么是领域驱动设计；

在 4-6 讲，了解了在落地领域驱动设计时，有哪些模式可以帮助我们更容易地实现模型；

在 7-9 讲，学习了不同的建模方法，以帮助我们展开业务维度；

最后，在 10 和 11 讲，学习了怎么把模型映射为 RESTful 架构。

内容整体来说是比较紧凑的，知识密度也比较大，所以在进入新约部分的学习之前，我希望你能喘口气儿，稍微回顾一下我们学习过的内容。

要知道，反思是学习真正发生的时候。毕竟学习的目的是改变我们的行为和思维，而一味地摄入内容和信息呢，并不一定真的能达到你想要的效果。

那么接下来，我将会集中回答一系列问题，帮助你更好地理解我们在旧约部分讲到的内容。因此，如果你有什么迫切的疑问，请尽快在评论区留言。时间有限，抓紧上车，入股不亏（当然，非官方版本是因为作者突发腰伤，需要休刊）。

左为《如何落地业务建模》作者徐昊，右为日本漫画家富坚义博

今天，我想先来谈一谈耦合的问题。因为我从收到的留言和问题反馈中，发现很多同学对耦合已经有创伤后应激障碍（PTSD Post Traumatic Stress Disorder）了。有同学经常担心是不是会存在耦合，然后开始准备用各种手段消除耦合，等等。

但是我们需要知道，耦合从逻辑上讲，有必然耦合和偶然耦合两种。所谓必然耦合，就是如果你使用支付宝或者微信完成在线支付，那么你必须要跟支付宝或者微信存在耦合关系。这种耦合关系是完成业务的必须。

对于这种耦合关系，你是无法消除的。也就是说，无论你怎么写代码，最终的结果都会转化为对支付宝或者微信的调用。当然，你也可以通过代码的组织，将这种耦和关系变得很隐蔽。比如通过事件驱动风格，神神秘秘地发一个消息，然后呢，后面会有个人帮你去完成调用。

然而消息发送在概念上和接口调用也没什么区别。所以当我们都处在概念层面时（比如我们现在在讨论必然耦合），其实你还是调用了对方的接口，所不同的是调用方法的差异。到底是直接调用，还是发个事件调用而已。

那么偶然耦合就不同了。什么样的情况算是偶然耦合呢？比如你现在要存一段数据，你可以选择把它存成文件，或者存在数据库里，再或者是区块链上链。不过，无论你选择哪种方式，其实都是偶然耦合，除非你有必须要这么存储的道理，否则你也只是随机地、偶然地选择了一种实现需要的方式而已。

所以对于耦合来说，我们本能地认为必然耦合是好的耦合，而偶然耦合是不好的。因为必然耦合中蕴含着必然性，也就是确定性；而偶然耦合里则包含着偶然性，也就是变化性。那么我们就需要处理掉偶然耦合。这恰恰是偶然耦合 PTSD 症状的源头之一。

然而让我们用偶然耦合和必然耦合再去看支付宝或者微信，你就会有另一种感受。我们是必然要使用支付宝或者微信吗？

显然不是！我们必然要使用的是网上支付服务，而我们只是偶然地选择了支付宝或者微信，作为我们实现网上支付的手段而已。

那么有什么办法可以让我们表达这种想法吗？有的。那就是提取业务能力。也就是说，我们不再必然耦合于支付宝和微信了，我们必然耦合于在线支付能力。然后呢，让支付宝和微信成为这种能力的实现。如下图所示：

从做法上来说，我们从业务上找到了业务真正所耦合的能力，然后将其他具体的供应商隐藏在能力之后。这样我们就完成了从偶然耦合到必然耦合的重构。事实上，这么做也深化了我们对业务的理解。而且也明确了我们所完成的功能，到底需要耦合于其他哪些业务能力。

注意，这个操作的关键在于，我们从业务上寻找到了对应的业务能力。从业务上寻找到的能力，是对业务本身理解的深化。而不是你随便编一个什么能力，只要能抽象过去就好。

但是一旦我们疏于理解业务，这个操作就演化成了著名的耦合 PTSD：见不得对具体实现的依赖。于是我们会粗暴地将对接口依赖看做必然耦合，然后把对具体实现的依赖看成偶然耦合。所有具体实现都必须抽象一个接口，也不管它是不是能在业务上找到对应的能力。

这种盲目地对具体实现的恐惧，以及盲目地将所有具体实现都归结为偶然耦合的一刀切的做法，其实是我们行业的一大病，同时也是很多具有迷惑性的烂代码的来源。看起来整整齐齐，所有变化点都考虑到了，实则并没能简化问题，也没能降低理解难度，只是写的人自己爽了而已。

可以说，真正的明白人，只会消除不必要的偶然耦合，通过把对具体实现的偶然耦合，转化为对隐藏业务能力的必然耦合，从而简化对业务问题的理解。

思考题

请从必然耦合和偶然耦合的角度思考一下，领域模型的多层架构，特别是基础设施层，到底应该是偶然耦合，还是必然耦合呢？

然后再回顾一下我们在 4-6 讲谈论的模式，其中哪些模式利用了将偶然耦合转化为必然耦合的技巧？我们又是如何识别偶然耦合的呢？

最后，如果你有什么迫切的疑问，或者想听的话题，请尽快在评论区留言。再说一遍，时间有限，抓紧上车。或许你的疑问，就会成为下一篇题外话的主题。

划重点，如果对课程内容有任何想法或建议，可以加专栏编辑的微信，和她交流，成为专栏的优质读者。微信号是：seekforli。

说点题外话02｜模式并不是解决方案

你好，我是徐昊。今天我们再来专门说点题外话。

说点题外话系列，是我根据评论区的留言，以及不少读者直接给到编辑的反馈中，挑选出来一些值得回答，但又不好直接回答的问题，然后呢，我会讲讲这些问题背后对应的原则。希望你可以感受到我强烈的暗示，在学完之后，不仅要思考，还要主动去寻找一下答案。

除此之外，我也希望给平淡的连载生活带来一定的现场感。比如今天这篇文章是周四零点推送，那么编辑小姐姐会在周三中午 12 点截止反馈收集。然后呢，我会从中挑选要写的话题，从下午两点开始写，然后录音。这样或多或少可以为专栏课程带来一些不可预知的可能性（比如编辑小姐姐一整个下午都在担心专栏是不是会断更，以及一整个下午，都保持着随时待命的状态）。

言归正传，今天我要讲一讲模式（Pattern）。自从 GoF（Gang of Four）在 1994 年发布设计模式（Design Pattern）以来，模式就成了获得可重用的对象模型的重要手段，而模式语言（Design Language），也成了我们描述架构和解决方案的重要手段。

我知道我们这个专栏的读者都有比较长的工作年限，也有比较丰富的工作经验，想来对于模式，你肯定是不陌生的。那么我先问一下，如下图所示是什么模式：

如图所示，某个类 A 将自己的行为委托给某个接口 B，然后某个具体实现类 C，实现了 B 的接口。你能想到什么模式呢？

如果仅从这个结构来看，那么对应的模式有很多很多可能性，我随便可以列出五种：

策略模式（Stragegy Pattern）：B 是策略的接口，C 是具体的策略实现。

适配器模式（Adapter Pattern）：B 是某个接口，C 是将另一个实现转换为 B 的实现。

状态模式（State Pattern）：B 表示状态的接口，C 是某个具体状态。

备忘录模式（Memento Pattern）：B 表示备忘录的接口，C 是备忘录某个特定的数据备份。

装饰器模式（Decorator Pattern）：B 是某个接口，C 是装饰了这个接口的额外功能。

我们会发现，如果仅仅给定最终的实现结构，那对应的模式可能有很多很多种。那么，怎么才能判断我们到底使用的是什么模式呢？

这个时候，我们就要回到模式的定义了。模式至少包含两个部分：问题和解决方案。同样的解决方案可能会对应着不同的问题。那么，同样的解决方案与不同的问题配对，也就产生了不同的模式。

比如说，对于策略模式，问题是对于某个类，需要施加不同的算法以完成不同的功能。那么怎么做才能使得算法可以替换，还不会影响到使用这些算法的类呢？

那么对应的解决方案就是，通过接口表示算法，然后将不同的算法封装到不同的对象中，并让这些对象实现这个表示算法的接口，这样就可以实现算法的替换了。

而对于状态模式而言，问题是某个类的行为随着它内部状态的改变而改变，我们如何将不同内部状态下的行为进行有效地封装，并在状态改变时做出与之对应的变化呢？

对应的解决方案就是，通过接口表示状态，然后将不同状态下的行为，封装到不同的对象中，并让这些对象实现这个表示状态的接口，这样就可以在内部状态改变的时候，实现行为的改变了。

虽然从最后结果上看，我们得到了类似的代码结构，但实际上，两者却是完全不同的两个模式，因为它们要解决的问题完全不同。那么我们不禁要问了，是否能够从解决方案反推它要解决的问题，然后判断我们使用了什么模式呢？

答案是，不行，最好也别这么做！

其实不光是在写代码，与人交往的时候，也最怕从行为贸然推断意图。比如说，哪个小姐姐多看了你一眼，你就觉得人家看上你了。于是批《红楼梦》的脂砚斋就跳出来说，古来穷酸最会替别人取中自己。你看，你和脂砚斋都已经在意图批判了，这样不好。

再比如这道经典送命题：为什么你去喝酒都不陪我，你一定是不爱我了。这也是标准的意图推断，多少不和谐都是从这里来的。

所以如果你深受其苦，那么请反思一下自己，为什么你看到某个具体的实现方案，就会猜测它是不是某个模式呢？我们还是要理解具体在解决什么问题，然后结合解决方案，才能明白具体是什么模式。

因为这样做其实也带来了另一个问题，就是不对问题加以判断，盲目地去套用某个解决方案的结构。这造就了大量看起来很高级，实则一无是处的烂代码，而且这种代码还充满了酸臭的匠气。

所以说，模式与建模是一样的，都是问题先行。模式最难的地方就在于判断当前的问题与模式要解决的是否是同一个问题，这也是为什么我们提倡通过重构获得模式：当问题明显出现的时候（以坏味道的形式），那么最难的一步其实你已经解决了。这时再通过重构去得到具体的解决方案，反而是简单的。

思考题

最后是非常值得我们思考的思考题环节。请问：我们在第 4-6 节所讲的模式，它们各自对应的问题和解决方案分别是什么？

一定记得把你的思考和想法分享在留言区，我会和你交流。同时，也非常期待你能把自己的疑问和想听的话题，写在留言区，或者反馈给专栏编辑（微信号：seekforli）。我们下个题外话再见！

说点题外话03｜银弹可以杀死狼人，但你怎么知道狼人不是你呢？

你好，我是徐昊。今天我们再来专门说点题外话。

在软件开发的黑话里，有一颗银色子弹（并不是滚筒洗衣机）可以解决一切问题，而我们一代代软件人，都在苦苦追求它。每当有新技术出现的时候，就会有人问，XXX 是不是银弹啊？比如说啊，云计算是不是银弹，DDD 是不是银弹，RESTful API 是不是银弹，低代码是不是银弹（并不是，这是行业毒瘤）。

然而有意思的是，银弹这个隐喻被引入软件开发领域中的时候，是源自 Fred Brooks 经典的软件工程论文《没有银弹：软件工程的本质性与附属性工作》（No Silver Bullet—Essence and Accidents of Software Engineering）。

简而言之，Fred Brooks 将软件开发中的工作分为本质性工作（Essential Task）和附属性工作（Accidential Task）。所谓本质工作，就是解决本质性困难的工作。而软件的本质性困难就是：如何从抽象性问题发展出具体概念上的解决方案。也就是如何理解我们要解决的问题，并选择恰当的解决方案。

与之相对的则是附属性工作，也就是将寻找到的解决方案，转化为电脑可执行程序的工作。而在这个过程中遇到的困难，就是附属性困难。

Fred Brooks 说呢，如果我们把具体的软件开发看成由本质性工作和附属性工作构成的混合形态，只要附属性工作不是占据全部工作的 9/10，那么就算我们把附属性工作降到 0，也没法获得十倍的效率提升。

也就是说，理解问题并寻找解决方案的本质复杂度难以降低，它一定会占据软件开发中的大部分时间和精力（比如知识消化的“两关联一循环”）。所以哪怕一行代码都不写，我们也没有办法将生产效率提升十倍。

这告诉我们什么呢？当你思考你的职业生涯时，需要考虑一下，你的本质性技能和附属性技能。然后我想你们都懂本质性技能是什么，因为我们已经强调了无数次了，那就是理解问题、定义问题。

我曾经领导过我司一个技术管培生计划，目的是培养十倍效能的 Thoughtworker，也叫小巨人计划。考虑到我司员工略高于行业平均水平的效能，当时就有人心怀疑虑，在这个基础之上，怎么还能再提高十倍呢？

其实答案很简单，任何人只要学会工作中的时间管理和人格分裂性多人运动，也就是任务分解和测试驱动开发，都能获得三倍左右的效率提升。而想要再进一步，就需要有更好的定义问题的方法，从本质性任务上寻找突破。这就需要我们能更好地理解业务，并且能够快速地学习领域知识。

前者可以通过学习财务、法务知识入手，并通过四色建模以及后续 8X Flow 在工作中的强化，后者则需要通过刻意练习，训练对应的胜任力。这其实不难，但难的是转变思路和行为。正所谓易学难精，这是需要你自己多下功夫的，我在这里就不多展开了。

当然，对于 Fred Brooks 的论断也有一种逆向推理，也就是能不能将附属性工作的比例提高，高到超过 9/10 呢？然后发现方法竟然惊人地简单：不定义问题、随意归因和迷信复用。

不定义问题就是不问要解决什么问题，按着套路推代码就行了。反正最后落到代码上也不过就是 CRUD，那管它到底要 CRUD 啥呢。于是乎，别说 9/10 了，百分之百都能变成附属性工作。

随意归因呢，就是不把问题的根源追溯到业务上，寻找到技术上要解决的问题就停止了。我可以理解技术人员对业务领域的诸多陌生与百般不适，然后就在自己擅长的地方停止了。然而事实是，当我们在做业务系统的时候，最终是由业务为我们的软件买单的。那么如果既不能增效，又不能降本，那要我们有何用？难道是因为好看吗？

最后是迷信复用，它还有另一个和银弹很般配的名字，叫金锤。意思是说，我拿了锤子，然后满眼都是钉子，不问是啥就乱砸一气。实际上，这是在打着技术卓越（毕竟在讲复用了）的名义，不定义问题和随意归因。

Fred Brooks 论文中之所以用到银弹，是因为只有银弹可以杀死狼人。而狼人呢，是从一般人突然变身为恐怖的怪物。

正如软件项目，看着一切正常，突然就超期了，失控了，事故了。所以盲目追求将附属性工作的比例提高，而忽略本质性工作，这不是银弹，这是狼人的血。结果就是没有杀死狼人，反而自己变成了狼人。

好了，关于银弹就言尽于此吧。在进入下一个题外话之前，希望你能认真思考如下问题。

思考题

我们在旧约部分讲的内容里，既有理念，也有技巧。那么请按你的理解来分析一下，其中哪些帮我们更好地完成了本质性工作，哪些解决了附属性工作呢？

请把你的思考和想法分享在留言区，我会和你交流讨论。同时，也非常期待你能把自己的疑问和想听的话题，写在留言区，或者反馈给专栏编辑。我们下个题外话再见！

说点题外话04｜面向对象的原则适用于RESTful API吗？

你好，我是徐昊。今天我们再来专门说点题外话。

前面几期题外话都比较偏向于提供一种不同的角度，主要是因为你们也并没有针对课程的内容，提出什么特别的问题需要我来具体回答。那么作为我们在进入新约前的最后一篇题外话，我想聊一聊关于 RESTful API 的问题。

我记得有位同学在留言区问了这样一个问题：过长的 URI 是否破坏了迪米特法则（Law of Demeter）。这里我们就要搞清楚，什么是迪米特法则呢？

迪米特法则又叫最小可知法则，指的是在面向对象设计中，实体应尽可能少地与其他实体发生交互。为了说明什么是“少的交互”，我们还特别归纳了一组可以认为不违反迪米特法则，并且可以直接调用的对象：

当前对象自己（this，self）；

以参数形式传入的对象，比如函数的形参（parameter）；

当前对象内实例变量引用的对象（instance variable）；

如果实例变量是集合，那么集合中的对象也可以访问（collection，aggregration）；

由当前对象创建的对象（variable declaration in function）。

那么这些场景适用于 RESTful API 调用的场景吗？显然并不太适用。因为在 RESTful API 的场景中，实体只有客户端和 API 提供者，而 API 提供者的内在结构都被 API 层屏蔽了。所以无论怎么调用，都不会出现对于 API 提供者内部结构的依赖。

如果我们把迪米特法则扩展一下，推广到概念层面，暴露了内在的逻辑就算。那么恰恰是 RESTful API，能帮助我们继续遵循迪米特法则。

比如通过超媒体明确地表示资源之间的关联，而不是依靠客户端去拼凑 URI。如果客户端可以拼凑出 URI，则表明客户端对于 API 提供者的内在逻辑存在依赖。而通过 HATEOAS，把所有关联的链接直接提供，就避免了暴露内在的逻辑。

再比如，API 的设计要按照 HTTP 语义约定，而不是客户端与 API 供应者之间的“私约”（private protocol）。PUT 只能修改已经存在的资源，而不能构建新的资源；POST 创建资源成功，需要返回 201，并在 HEAD 中给出新构建资源的 URI；GET 默认都是可以缓存的，无法缓存的查询（而不是通过 URI 遍历信息），需要用 POST 访问等等。这些都是希望通过公约，将客户端中对于 API 供应者的了解降到最低。

所以迪米特法则本身的想法是不错的，但是场景改变了，我们就要重新思考它在新的环境中是如何被应用的。而如果我们真的在乎迪米特法则，那么在 RESTful API 的场景下，关注点就不会放在 URI 有多长上，而应放在客户端与服务器间的知识依赖到底有多少上。

另外一个对于 RESTful API 的质疑在于，通过 HATEOAS 完全以分布式超媒体的方式构成 API，那么客户端看起来越来越像浏览器，而不是针对 RESTful API 的客户端了。

正如我们在第 10 讲中讲到的，RESTful 架构风格是对互联网架构的反思。那么互联网架构的核心在于开放性和扩展性，因而 RESTful 架构风格的核心也是开放性和扩展性。

因为开放，使得 RESTful API 的供应者不会对客户端作出任何假设。就好像互联网服务器并不会假设它的客户端只有浏览器一样，wget、telnet 等等都是可能的客户端；而因为扩展性，RESTful API 只会为客户端提供最基本的功能，大量的计算被分布到了客户端侧进行。

这种架构的假设是不同于企业应用的客户端与服务器架构的，在企业应用架构的语境中，客户端与服务器有更多的耦合。服务端更多地是为客户端提供服务，而不是保持自己开放和稳定。

那么为什么 RESTful API 最终还是成为“行业主流”了呢（虽然真的会，和真的用的人并不多）？

因为从大趋势上来说，将企业内的能力（而不仅仅是后台）构造成开放 API，并围绕着开放 API，形成企业内生态是大势。在这个大势之下，RESTful API、MicroService、企业内生态、能力平台、中台形成了一条清晰的企业架构现代化之路。仅仅服务于某些（或者某个）前台的后台服务，终将会淡出历史的舞台。

因而在这种历史转折的节点，我们更应该清晰地理解不同想法之间的差异，哪怕它们要解决的问题与现行方法是相似的，但是对于这是什么问题，我们要怎么想，还是带来了完全不同的角度。

思考题

请问其他面向对象原则与最佳实践，在 RESTful 架构下有何种体现？

欢迎把你的思考和想法分享在留言区，我会和你交流。我们新约部分再见！

12｜云时代的挑战（上）：弹性边界还是业务边界？

你好，我是徐昊。今天我们来聊聊云原生架构中弹性边界对业务建模的影响。

通过前面旧约部分的学习，我想可能会给你留下这么一种印象，4-6 节和 7-9 节分别给领域驱动设计打了两个大补丁：

通过不同的上下文对象，弥补原生对象模型从单体架构过渡到多层架构时的各种水土不服；

通过不同的建模方法，从业务维度展开入手，以不同的角度寻找可以被建模成对象的领域概念。

最终经由我们打过补丁的业务建模方法，就是一种在领域驱动框架下，从业务角度出发，又兼顾了架构约束的特殊的面向对象建模法（Object Oriented Modeling）。

如果你希望达到如下的诉求：

采用领域驱动设计的“两关联一循环”作为主要沟通协作的方式；

将模型作为统一语言，并用于提炼知识的循环；

在单体分层架构模式下，将模型的能力通过 RESTful API 暴露。

那么之前所学就基本满足你的要求了。但如果你的目标架构不仅仅是单体分层架构，而是更云化的架构风格，比如微服务（Microservices Architecture）等等，有了不同的关注点，那之前打的这两个补丁，就不足以支撑我们在新的架构风格下完成业务建模了。

因为更云化的架构风格有了新的关注点：弹性边界（Elasticity Boundary）。弹性边界是云原生（Cloud Native）架构的核心概念，决定了我们是否能够充分发挥云平台的全部能力。于是我们就需要新的方法来弥补原生对象模型的不足，以满足我们的需要。

从弹性边界理解云原生架构

为了更好地理解如何架构云原生系统，我创造了弹性边界这个概念。它指把弹性作为最优先的考虑要素而划定的系统边界。而我们通过弹性边界，就能判断某个系统是否充分利用了云平台的能力。为了帮助你理解这一点，我们需要从什么是弹性讲起。

在很多年前，当云计算第一次出现在我们视野中的时候，云厂商都非常热衷于讲一个关于弹性（Elasticity）的故事：我们可以根据流量的改变，动态地调整所需要的基础设施。

举个例子。作为一个电商网站，平时可能只有几千用户同时使用，只需要 100 台机器就足以支撑这个系统了；而双十一到来时，用户量可能会猛增几百倍，那么就需要比如说 10000 台去支持这个系统了。

而云平台呢，可以动态调整系统所需要的机器数量，让我们按需使用。这样我们就不需要在闲时投入过高的机器成本，也就是非双十一期间，维持 10000 台机器的开销。但同时也不会错过在业务高峰获取收益的机会，因为云平台会自动帮助我们从 100 台扩容到 10000 台机器。

这种动态调节的能力被称作云的弹性，它是云平台一切美好特质的基础。

而为了实现这种弹性，运行在云平台之上的系统，则需要满足一个条件：这个系统可以通过水平扩展进行扩容（Scaling）。

正如我们知道的，扩容策略（Scaling Strategy）从宏观来讲只有两种：水平扩展（Scale Out）和垂直扩展（Scale Up）。

水平扩展是指通过增加机器数量、构造集群的方法，来满足容量的需求。与之相对的则是垂直扩展（Scale Up），指当遇到扩容需求时，通过更换更强有力的机器，来获得更好的性能，或者满足容量需求。如下图所示：

为什么说水平扩展是弹性的前提呢？事实上，我们也可以在容量到来的时候，进行垂直扩展，将云平台提供的服务器从 a1.large 变成 a1.4xlarge（以 AWS EC2 On-Demand 为例）。

但这么做的问题就在于弹性不可持续。如果你已经使用了 100 台 a1.4xlarge，那么你还能怎么升级呢？如果此时再来更多的用户访问，你的系统要怎么应对呢？

而使用水平扩展就不会有这样的问题，毕竟就算 100 台 a1.4xlarge 不能满足，总是可以使用 200 台 a1.4xlarge 去完成所需的弹性。因为只有水平扩展能够支持持续的弹性能力。所以我们说，水平扩展是云平台弹性的前提。云平台所声称的弹性，是建立在水平扩展的基础上的。

于是我们可以这么说：能充分利用云平台能力的架构，就是能够充分利用水平扩展的架构。那么接下来我们就仔细看看云平台具体是怎么实现水平扩展的。

我们可以回想一下各种基础设施服务云平台（IaaS，Infrastructure as a Service），它们其实只有复制和剪切两个能力：

根据给定镜像（Image），产生符合镜像内容的机器的能力。也就是将镜像复制为机器的能力。

撤销不需要的机器，将其放回资源池的能力。也就是剪切机器的能力。

通过复制和剪切这两个能力，云平台就能对某个镜像提供水平扩展了。这种扩展方案通常被称作弹性负载均衡（Elastic Load Balancing）。而我们一般也会为弹性负载均衡设定一个容量阀值。

当流入的流量超过阀值时，弹性负载均衡会根据镜像生成新的服务器，并加入服务组网中（将镜像复制为机器的能力）。

如果流入的流量小于另外一个阀值，则会从服务组网中去掉已经存在的服务（剪切机器的能力）。

如下图所示，虚化的机器表示通过复制和剪切动态调配的机器：

可以看到，借由弹性负载均衡和镜像，云平台帮我们实现了对弹性的诉求。也就是能够根据流量的改变，动态地调整所需要的基础设施。

那么怎样利用弹性负载均衡提供的水平扩展，才能更有效地架构系统呢？关键在于将弹性需要不同的组件分离。

就以双十一为例。假设你在运营一个在线电商平台，我们可以粗略地将这个电商平台的业务分成产品目录和支付两大块。在双十一期间，肯定会遇到比平时更大的流量，因而需要更高的系统容量去支持业务。

那么问题来了，产品浏览和完成支付两个部分增加的流量是一致的吗？还是其中一个部分比另外一部分获得了更多的流量？

从我们实际参与双十一抢购的个人经验出发，就会发现，在双十一之前，用户对产品浏览对比的需求很可能比平时多；而在双十一当天呢，可能会对支付的需求更多。

那么我们就可以将支付和产品目录两块功能，分别放入不同的组网，制定不同的水平扩展策略，然后由不同弹性负载均衡控制。这样就可以有针对性地在不同阶段为两块功能提供不同的容量了。

如下图所示，产品目录和支付，被分别放入了不同的组网：

在双十一之前，当我们需要扩容产品目录时，并不需要同时扩容支付。同样的，在双十一当天，如果我们选择扩容支付，那么产品目录也不会随着扩容的变化而变化。

于是，我们通过弹性负载均衡控制了不同的组网环境，也就控制了不同逻辑功能在弹性变化上的边界。这就是弹性边界。换个角度来看，我们把弹性作为主要指标，对系统进行划分，将不同弹性变化的组件放入了不同的弹性边界中。

要知道，一旦我们通过弹性边界来划分系统，我们就能将系统划分成许多个具有一致弹性诉求的单元，然后再利用云平台的弹性调度能力，来满足不同单元的容量诉求。

可以说，通过弹性边界，我们实际上是以更细致的粒度，控制了系统运营的成本。这样架构系统的方式，不光可以通过云平台应对业务量峰谷的起伏，同时还获得了更准确的成本控制。

也只有这样，才能真正发挥云平台的能力。所以当我们想要利用云平台架构软件时，寻找合理的弹性边界便是最重要的事情。

拆分微服务，弹性优先还是业务优先？

当我们通过弹性边界架构系统时，很容易联想到微服务架构风格。两者都是将系统划分成更小的组件，以获得期望的架构属性。

事实上，我们可以将微服务架构看作一种以业务上下文为弹性边界的云原生架构模式。也就是说，微服务是云原生架构的一个子集，或者是一种特化（specilization）的形态。

如图所示，我们可以把极客时间模型中不同的上下文放入不同的弹性边界中，就能获得一个微服务架构：

这里要说明的是，虽然微服务并不一定需要以云原生架构作为基础，而且你也可以在具有固定弹性的数据中心，去部署微服务。但是实践经验告诉我们，比起将微服务部署到云平台上，在固定弹性的平台上使用微服务架构，有极高的实施成本。

具体可以参看 Martin Fowler 在 2014 年写的文章《微服务的前置条件》。里面很委婉地表示了，云平台实际上应该是微服务的前置条件。

如果将微服务架构风格看作一种云原生架构风格，那我们可以从弹性边界出发，重新去审视它。这样我们就能获得另外一种视角：从云平台利用度去理解微服务架构风格，并将它与其他架构风格做出比较。

以单体架构和微服务架构为例。单体架构中所有的业务上下文都集中在一个弹性边界内，这意味着我们无法根据实际流量，独立调整它们的容量。所以如果需要扩容，那也只能以整个应用作为单元去扩展。因而在云平台上，单体架构就是一种可行（Feasible），但成本较高的架构模式。

反观微服务架构模式，它将单体中的不同业务上下文拆分到独立的进程中，也就是赋予了它们独立的弹性边界。这时候我们扩容的选择，就变得丰富起来。我们可以根据需要，将不同的业务上下文单独扩展。

因而我们也可以这样说：比起单体架构，微服务架构更好地利用了云平台的弹性。如下图所示，表示了业务上下文在不同架构风格中的分布：

于是从弹性边界的角度出发来看待不同的架构模式，就为我们带来了另一个评价是否需要拆分微服务的思路：是否值得将某个业务上下文放入独立的弹性边界内。

我们都知道，将拆分的服务放入独立的弹性边界具有一定的运营成本，因而“是否值得付出这个成本”就成了需要讨论的问题。想想看，如果两个业务上下文，始终具有同样的弹性需求，那么，是按照业务上下文将它们拆分成两个服务呢？还是遵循弹性上下文，不去拆分呢？

我的答案是“弹性优先”。也就是说在云平台上，弹性永远是第一优先级。如果两个上下文明显具有不同的弹性诉求，那就应该拆分。而如果具有一致的弹性诉求，就可以不拆。

弹性优先原则是一种功利主义架构思路。这里的“功利主义”（Utilitarianism）也可以称为“效益主义”，是由英国哲学家约翰·斯图尔特·米尔（John Stuart Mill）和杰里米·边沁（Jeremy Bentham）等提倡的哲学主张。其实墨子、叶适也有类似的想法，主张看中实际的功用和效果，不要空谈理念。功利主义架构设计也是我近年来倡导的架构设计哲学，弹性优先原则是这一设计哲学在云平台的体现。

因为弹性优先原则用在微服务拆分上，就是在询问这样一个问题：将拆分出的服务放置于独立的弹性边界中，是否能够利用云的弹性，更容易地控制运营成本？如果不能，那么就不要拆。如果能，那么就可以拆。这种基于效益的考量，就是“功利主义架构思路”。

不过你可能会问，微服务的拆分可以使得每个服务独立部署啊，这也是弹性吗？是的！对于系统中软件的变更与部署，同样也可以看作弹性诉求。

那么从弹性诉求角度来看，软件的变更和部署是怎样的呢？如果有 100 台机器部署了 1.0 版本的软件，而现在我需要将它们升级到 1.1 版本的软件。从弹性角度来说，可以看成在一个很短的时间内，出现对于 1.0 版本需要的业务低谷，以及一个针对 1.1 版本的业务高峰。

那么弹性负载均衡会怎么处理呢？

出现业务低谷时，弹性负载均衡会剪切机器；

因为这个低谷足够低，那么最后一台都不剩了；

出现业务高峰时，弹性负载均衡会从镜像开始复制机器；

最后根据流量的需要，重新形成 100 台机器的体量。

因而微服务对不同变更速度的诉求，可以看作弹性诉求，也可以通过弹性边界去控制变更的成本。那么，我们就能用弹性优先原则去分析微服务架构，以寻找服务划分的准则了。换言之，对于以云平台为部署目标的微服务系统而言，微服务的划分要以弹性边界为主，以业务上下文为辅。

这也帮我们回答了微服务架构的第一难题：微服务到底要多微？答案是，微到能够更好地利用弹性控制成本的大小。

最后需要说明的是，弹性优先原则不仅仅适用于微服务架构，而是适用于所有云原生架构。无论是今天的微服务，或是无服务器架构（Serverless Architecture），还是未来可能出现的宏服务（Marco Service，已经有些许苗头了）。这些都是希望通过利用弹性进而更高效地利用云平台的架构模式。因而，这些架构模式都可以通过弹性优先原则去思考和比较。

弹性优先对业务建模的挑战

当我们延续领域驱动设计的思路，以一种模型驱动的视角看待如何在云时代构造软件的话，就会面临两个挑战：

我们的模型该如何体现弹性边界？

在模型中，弹性边界与业务上下文要如何配合？

如果我们回想一下，原味面向对象建模（Vanilla Object Oriented Modeling）中的聚合（Aggregation），就会发现它是一种“一致性优先”的模型结构。所以每当我们看到聚合关系时，都可以将它读作“一致性边界”或者“事务边界”。

然而事实是，我们并没有一种类似的模型结构，可以无歧义地解读为弹性边界。

当然也正如我们前面所讲的，可以把界限上下文（Bounded Context）当作一个筐，找到了弹性边界就圈一下。如下图所示，标识了不同的弹性边界：

假设我们现在希望利用云平台重新构造极客时间的服务，那么请你思考一下：在领域模型中，我们该如何去切分弹性边界呢？

我们一个直观的感觉是，将不同的业务上下文直接当作不同的弹性边界。那就让我们仔细看一下订阅这个上下文中的内容（Content）与订阅（Subscription）的部分吧。

订阅部分处在核心的业务逻辑之上，而且随着业务的增长，同一份内容可能会有成千上万的订阅，因而对于订阅所需的容量，可能会远远大于内容所需的容量。那么我们可以说，内容和订阅这两部分处于不同的弹性边界中。因而，我们可以划分一个界限上下文表示这个弹性边界。

与此同时，我们又会面临另一个挑战：业务上下文要和弹性上下文如何配合？在业务上下文中，业务逻辑具有较强的一致性。而通过弹性上下文将业务上下文分割之后，这种业务逻辑的一致性又要怎么体现呢？

归根到底，在云时代进行业务建模时，我们需要确立一种模型结构以反映弹性边界。同时，在弹性边界切分业务上下文时，还需要确立另一种模型结构，去重建一致性边界。

而这些问题在原味对象建模法，以及我们之前介绍的几种建模方法中，都没有得到很好地处理。因而我创造了新的建模方法 8X Flow 来解决这两个问题。

不过在正式介绍 8X Flow 之前，我们还需要再仔细讨论一致性边界的问题，这正是下节课要讨论的内容。

小结

这一讲我们介绍了云平台是如何通过水平扩展为系统提供弹性的。然后，我们介绍了弹性边界这个概念，以及如何通过弹性边界去衡量软件架构是否真正利用了云平台的能力。也明确了，在云时代架构系统的首要工作就是寻找恰当的弹性边界。

并且我们以微服务为例，讲解了如何通过弹性边界以及弹性优先原则去理解它的架构属性。并给出了一个云原生视角下划分微服务的指导原则。

最后我们讲了弹性边界和弹性优先原则给业务建模带来的新挑战。下节课我们会继续细化这些挑战，以明确新的建模方法到底能为我们带来什么。

最后的最后，编辑小提示：为了方便读者间的交流学习，我们建立了微信读者群。想要加入的同学，戳此加入“如何落地业务建模”交流群>>>

思考题

就好像软件的模块之间会存在依赖关系一样，弹性边界间也会存在依赖关系。那么请思考，当弹性边界间存在依赖时，会带来哪些影响？

欢迎把你的思考和想法分享在留言区，我会和你交流。我们下节课再见！

13｜云时代的挑战（下）：如何保持弹性边界的独立性？

你好，我是徐昊。今天我们来继续聊聊弹性边界对业务建模的影响。

上节课我们介绍了弹性边界的概念，以及弹性优先原则。照此来看，我们是不是按照组件不同的弹性要求，将它们分别放置到不同的弹性边界中，就可以最优化地利用云平台的能力了呢？

答案并没有这么简单。因为除了弹性边界外，我们还需要考虑流量的传播，以及在弹性边界间造成的依赖关系。

今天我们就来讲一讲弹性边界间的依赖关系。包括什么样的依赖关系是合理的，有什么样的依赖关系是我们希望避免的，以及不同的弹性依赖关系又会给业务建模带来什么样的影响。

弹性边界间的依赖与耦合

就好像软件的模块之间会存在依赖关系一样，弹性边界间也会存在依赖关系。不恰当的软件模块依赖，最终会引发散弹式修改（Shotgun Surgery），也就是软件模块的边界并没能隔离变化的传播。换句话说，在一个模块中出现的变化，会传播到其他模块中，引起其他模块的修改。

而弹性边界间的依赖（也就是服务间调用关系，或是数据交换关系），会造成流量的传递。如果上游弹性边界突然需要处理大量的流量，那么这些流量自然也会传递到下游弹性边界中。

让我们举一个生活中的例子，来帮助理解弹性边界间的依赖关系。假设你是一家快餐店的负责人，你雇佣了一个负责点餐的员工。厨房是负责制作餐食的部门，有一套能完成对应操作的设备，以及负责餐食制作的厨师。

当顾客不多的时候，点餐的员工可以从容地完成点餐，厨房也可以在顾客耐心耗尽之前，做好顾客所需的餐食。而如果顾客过多的时候，就会出现大量顾客等待与排队的情况。

这个时候，你可以通过雇佣更多的点餐人员，来减少顾客在点餐时的等待。但与此同时，你还需要扩容厨房，引入更多的设备与厨师，解决取餐的等待。这样才能让顾客真正满意。

看起来我们可以通过分别雇佣点餐人员，和厨房人员去缓解点餐和取餐的等待。但是你仔细想想，点餐与厨房真的是分离的吗？

其实并不是，他们共同构成完整的服务，需要共同协作才能满足顾客对就餐的要求。而其中厨房作为下游服务，它的容量要求源自点餐。那么更多的订单，就意味着需要更多的设备与厨师，这样才能保证在合理时间内，让所有顾客都能吃上饭。

现在让我们把整个厨房搬入赛博世界（Cyber World）。于是我们可以说点餐处在一个弹性边界内，而厨房处在另一个弹性边界内，两个边界内的服务共同完成了整个业务流程。那么随着点餐部分弹性的伸缩，流量就传递到了厨房部分，也引起了类似的弹性伸缩。如下图所示：

我将这种不同弹性边界间流量的传递称作弹性依赖（Elasticity Dependency）。弹性依赖可以帮助我们理解不同弹性边界内容量变化的根源，并制定相应的扩容策略，然后作出最佳应对。

在云原生架构下，只要组件之间存在交互，弹性依赖就不可避免。只不过，云平台更擅长处理依赖于吞吐量（Thoughput）的弹性依赖，但对依赖于响应时间（Response Time）的弹性依赖，就没有什么好办法了。

看到这儿你可能有些不理解，为什么云平台能更好地处理依赖于吞吐量的弹性依赖呢？在讨论这个问题之前，我们先来看看响应时间和吞吐量依赖的差别。还是让我们用餐厅的例子来做说明。

对于响应时间的依赖，你可以把自己想象成快餐店负责点单且传菜的员工（比如上海特调咖啡顶流大店 O.P.S. Cafe 就是这样的模式，点单员需要为顾客解释每一杯咖啡的特点）。顾客直接与你沟通，后面还有源源不断排队而来的人，你必须打起精神尽快帮助每一位顾客点单，并通知后厨完成餐食准备，然后打包交给顾客。

另外你还需要注意，在前一个顾客拿到菜品之前，你不能为后一个顾客服务。有任何的延迟，你不光会被当前的顾客抱怨，还会被后面排队的顾客抱怨。这时候，你的关注点就在于每一位顾客点餐后厨房的进度，也就是厨房的响应时间。因为厨房的响应速度，是你服务质量的重要组成部分。

而对于吞吐量的依赖，只需要改变一点，就是你不负责传菜，而只需将菜品信息交给后厨即可。点过餐的顾客会在一旁等候，当餐食制作完成后，你会取餐并交给顾客。

这时候，显然你也不能让顾客无限等待下去。但是，你不会关注每一位顾客点餐后厨房的进度，而会关注最早点餐的顾客已经等待了多久，厨房内有多少份餐食在做，以及是不是能够在可接受的时间内，服务完所有的顾客。于是你的关注点就从每一单的响应速度，变成了整个后厨的吞吐量。

在这两种情况下，点餐部分的流量都会涌入厨房；在两种情况下，点餐和厨房间都会存在弹性依赖。但是，在第二种情况下，云平台可以作出更好的应对。

这背后的原因在于水平扩展并不能保证改进响应时间，而只能提高吞吐量。也就是说，云平台的弹性并不总能改进响应时间，但一定可以提高吞吐量。这就意味着对于吞吐量的诉求，可以通过弹性来解决。但对于响应时间的诉求，弹性可能无法处理。

比如在微服务架构风格下，我们将产品目录组件放入一个弹性边界内。对于该组件访问请求增大时，弹性负载均衡会通过云平台的复制功能，构造新的虚拟机，处理峰值的请求流量。因而在单位时间内，我们能处理的请求数量就变多了。也就是说，这个弹性边界内组件的吞吐量增加了。

如下图所示，展示了随着请求增大时，通过弹性增加吞吐量的过程：

弹性扩容实际上就是对吞吐量的扩容，因而更大的吞吐量诉求对于云平台来说，就是小事一桩。同时我们也可以观察到，弹性扩容对响应速度没什么影响。如果从始至终，只有一个请求访问该服务，那么无论弹性负载均衡复制多少新的虚拟机，都无法缩短响应时间。

当然严格意义来说，因为请求数量过多，造成的处理延迟（Latency），是可以通过水平扩展消除的。但是很显然，我们现在讨论的是无延迟情况下的响应时间，所以这种情况也就不在我们的考量范围之内了。

那么通过水平扩展缩短响应时间，需要将请求转化为并行算法，从而将计算分布到不同的计算节点上。然而借由阿母达尔定律（Amdahl’s law）我们知道，通过水平扩展产生的加速比，依赖于计算中可并行化的部分。

也就是说，如果某个计算中不包含任何可并行的部分，那么水平扩展就不会缩短它的响应时间。

这里要说句题外话。就算某个计算中包含大量的可并行部分，我们也没有通用算法可以将任意计算都转化为高效的并行算法。当然如果有，也会被编译器迅速吸纳，从而脱离我们的关注。需要知道的是，我们对 Actor 并发模型、函数式编程等新的编程范式的热情，在很大程度上，也是因为这些新的范式能够提供一定程度的自动并行优化，帮助我们缩短响应时间。

言归正传。正因为云平台不擅长处理依赖于响应时间的弹性依赖，我将这类弹性依赖称为弹性耦合（Elasticity Coupling），以表示与依赖于吞吐量的弹性依赖的区别。因为两个弹性边界间存在弹性耦合，通常意味着，将这两个弹性边界分离后，对于云平台利用效率的提高，不足以弥补为了分离它们所付出的成本。

这也是很多微服务拆分失败案例的根源：从业务上看，的确是分离的上下文，但却没能解除业务上下文间的弹性耦合。结果就是费力拆分了业务上下文，却不能获得独立的弹性边界；高昂的成本也没能换回微服务对云平台弹性的有效利用。总感觉拆了个寂寞。

因而我们不仅需要根据弹性优先原则，来划分弹性边界，还需要保证弹性边界间具有恰当的依赖关系，避免出现弹性耦合。

如何避免弹性耦合？

如前所述，我们需要尽可能地避免弹性耦合，才能充分利用云平台的能力。那么，怎么才能避免弹性耦合呢？

最简单的方式，是将组件间的同步调用模式改为异步。这是因为，服务与组件间的同步调用，会带来响应时间的依赖；而异步调用，则能将其改变为吞吐量的依赖。

为了更好地理解同步调用是如何带来响应时间依赖的，我们还是先来看一个例子。

假设我们有一个电子商务网站，已经完全部署到了云平台之上。而且我们按照弹性边界，将这个系统分解成了“订单”“支付”“邮寄”“通知”等组件，并将它们放置到不同的弹性负载均衡控制之下。

那么对于“下单”这个业务功能，我们会：

通过订单组件，先去调用支付组件，完成订单支付。

然后呢，再通知邮寄组件，生成快递单，将用户购买的产品发出。

最后通过通知组件，告知用户订单状态。

如下图所示，展示了不同弹性边界内业务功能间的相互调用：

如果订单组件是同步调用支付组件，那么作为调用方的订单组件，也会消耗资源，等待支付组件的返回。

这里要注意，无论这个等待的时间是 500 毫秒、15 秒或是 5 分钟，可以确定的是，在下游响应之后，调用方才能释放自己的资源。而且，调用方付出了资源等待，那么这个时间就不仅仅是消耗了下游的资源，调用方自己的资源其实也在消耗。

此时，支付组件的响应时间，也就包含在了订单组件的响应时间里面，因而订单组件的响应时间依赖于支付组件的响应时间。这就意味着订单与支付这两个弹性边界间存在着耦合关系。如图所示：

而异步调用的关键，就在于无需消耗资源等待下游返回，就可以将对下游响应时间的要求，改为对于吞吐量的诉求。

比如前面讲的订单与支付的例子。如果是异步调用，作为调用方的订单组件就不需要等待支付组件返回，而是可以继续处理后续的请求。支付组件呢，则只需要在规定时间内处理完请求，并通知作为调用方的订单组件即可。

这个规定时间，无论是 500 毫秒、15 秒或是 5 分钟，其实并不是响应时间，而是吞吐量的另一种表现形式。所以 500 毫秒，其实是每分钟 120 次的吞吐量（60 秒 x1000 毫秒 / 500），15 秒就是每分钟 5 次的吞吐量，5 分钟也就是 1 小时 12 次的吞吐量。

以此来看，当订单侧流量增大时，我们只需要利用云平台对支付进行扩容，保证同样的吞吐量即可。那么这里的关键就在于，异步调用是怎么做到将响应时间变为吞吐量的呢？

我们可以将异步调用想象成在所需调用的组件前，存在一个请求的缓冲。所有的请求都会先进入缓冲之中，而且这个缓冲有个总容量，在到达这个容量之前，总处理时间不会超过某个给定的限定值。

那么因为组件间的直接调用被缓冲隔离了，上下游组件间从直接耦合，就变成了通过缓冲的间接耦合。

而同步调用中对响应速度的要求，就被分解成了处理时间、等待返回时间和请求缓冲的轮转时间。其中请求缓冲的轮转时间，就是吞吐量的另一种表现形式。在我们前面例子里，这三个时间分别对应订单的处理速度、订单的结果返回速度以及支付的吞吐量。

如下图所示，其中吞吐量我使用轮转速度来表示：

由此可以看出，通过将同步调用改变为异步调用，我们就把对于下游响应时间的依赖，改变成对于下游吞吐量的依赖。然后将弹性耦合变成了弹性依赖，使得整个系统可以更好地利用云平台的能力。

因此我们现在面临着一个很有意思的局面。在前云时代（Pre-Cloud Era），同步调用是我们在服务间所采用的调用和整合方式。而到了云时代，这种天经地义的做法反而被认为是有害的。

James Lewis 和 Martin Fowler 在他们的名作《微服务》中，有一个边栏就明确提及“同步调用有害”（Synchronous calls considered harmful）。而行业中也有大量的佐证，表明在云时代，异步调用、事件驱动架构风格会逐渐成为主流的选择。

Netflix 就在拆分微服务的过程中明确提出，需要将同步调用替换为异步；异步风格 API 倡议（Async API Initiative）则尝试通过标准化异步 API 的设计，来取代 RESTful API 风格，使之成为未来服务互联的标准。

我相信这些尝试仅仅是个开始，随着我们对于云平台、弹性以及弹性边界认识的深入，过往的最佳实践都会被打破。而围绕着云与弹性，我们会重新认识什么才是架构软件的最佳方式。

默认异步对业务建模的挑战

为了消除弹性耦合，我们需要放弃默认同步调用的方式。那么当我们延续领域驱动设计的思路，以一种模型驱动的视角来看待这个问题的话，就会面临两个挑战：

如何将异步调用与模型结合起来？

如何处理异步调用对聚合关系一致性的影响？

你可能没有意识到，通过原味面向对象建模（Vanilla Object Modeling）得到的模型，几乎默认都是同步模型。让我们随便从前面的章节里选一个模型：

可以看到在这个模型中，有两个对象，User 和 Subscription；以及一个关联关系 User-Subscriptions。

通过这个模型，我们可以创建 User 对象、Subscription 对象，或者将 User 对象与 Subscription 对象关联在一起。但是从模型中，并没有任何结构支撑我们以异步的方式构造 User、Subscription 对象，或是将 User 与 Subscription 关联起来。

要知道，异步方式意味着，由模型产生的数据可能存在中间状态。比如，我们通过异步方式为 User 增加了新的 Subscription，那么可能处于“请求已发送，但是还没有得到明确反馈的中间态”。

而在同步模型中，要么 Subscription 已经与 User 关联，要么未与 User 关联，并不存在一种中间状态。当然，我们可以通过修改模型，增加中间状态，如下图所示：

通过旧约部分的学习，我想你一定可以知道这里有什么问题。我们将架构约束引入模型，也就是将它们引入到统一语言，进而通过模型定义业务。

想想看，这仅仅是为了架构约束的需要，引入的技术概念呢？还是这种异步的中间态，在业务上存在特殊的含义？这是我们在进行业务建模时，需要思考的第一个问题。

此外，异步的中间态还会影响聚合根对关联关系一致性的控制。正如我们前面所说，在同步模型中，要么 Subscription 已经与 User 关联，要么未与 User 关联。Subscription 关联的一致性由聚合根 User 控制。

然而一旦进入异步模式，会出现尚未确认的关联。这个问题其实不大，不过需要额外注意的是，那些在规定时间内没有得到确认的关联。

比如我们说，为用户增加订阅，需要在 5 分钟内给出确认信息。如果在 5 分钟内未能收到确认信息，那么订阅的增加就失败了。此时，我们就需要将这个处于中间态的订阅，从关联中移除。

那么这种因为异步带来的一致性改变，对业务会产生什么影响呢？这是我们需要考虑的第二个问题。

所以归根到底，为了解决弹性耦合的问题，我们需要将原味面向对象风格中默认的同步模型改为异步。但是我们也知道，在领域驱动设计“两关联一循环”的大框架下，对于模型的改动，最终会反映到我们如何理解业务上。因此我们需要一种方式，将异步模型对应到业务上下文中。

这是我创造 8X Flow 时，希望解决的另一个大问题（第一个大问题是如何在模型中引入弹性边界，上一节课讲过）。理解了 8X Flow 要解决的问题，也就是如何有效地将弹性作为首要概念引入建模，那么你在接下来的课程中，就可以更好地学习它了。

小结

这一讲我们介绍了弹性依赖与弹性耦合，以及它们如何影响了我们对云平台的利用。此外，我们还深入地分析了弹性耦合产生的原因，以及如何通过异步模型将对于响应时间的依赖，转化为对于吞吐量的依赖。最后我们讨论了，异步模型对领域模型的影响。

那么结合上一节课的内容，总结起来看，在云原生时代，我们需要将弹性作为首要考虑的因素，纳入建模的考量。那么弹性边界，就是我们划分系统的重要依据。而且，我们还需要考虑弹性边界间的依赖关系，尽量避免弹性耦合。

对于业务建模来说，为了配合云时代的架构约束，我们需要做到如下几点：

确立一种模型结构以反映弹性边界；

从异步模型的视角，解读业务逻辑；

在弹性边界切分业务上下文时，维护业务一致性；

在异步调用产生中间态异常时，维护业务一致性。

事实上，这些便是我创造 8X Flow 的出发点。我相信，解决了这些问题，我们就能在云时代做好业务建模。

编辑小提示：为了方便读者间的交流学习，我们建立了微信读者群。想要加入的同学，戳此加入“如何落地业务建模”交流群>>>

思考题

这两节课我们讲了云平台带来的架构约束，以及这些约束对于业务建模的影响。那么请你反思一下前云平台，你能找到对应的架构约束，以及它们对于建模的影响吗？

非常期待你能把自己的思考和想法分享在留言区，我会和你交流。我们下节课再见！

14 | 8X Flow（上）：何为业务？何为领域？

你好，我是徐昊。今天我们开始进入 8X Flow 的学习。

8X Flow 是继四色建模法之后，由我发明的另一种建模方法。其实以前四色建模法也叫 8X Flow。

从之前的学习中你会发现，四色法是一种数据流方法——Flow 有了；而我姓徐名昊字八叉，以我的名字命名而得；但后来为了向 Peter Coad 致敬，表示灵感来自他的彩色建模法，就叫了四色建模法。于是今天要介绍的方法，我们内部之前叫 8X Flow 2.0，现在直接叫 8X Flow 就行了。

8X Flow 方法是四色法的一种演进。虽然权责追溯仍然是 8X Flow 的核心理念，但比起四色法，8X Flow 更注重通过权责关系，提取业务模式；并通过引入变化点构建可复用的业务模型。所以对于业务平台或业务中台这类系统来说，8X Flow 可谓是量身定制。此外，由于充分考虑了云时代的架构约束，得到的模型也能很容易地映射到微服务架构上。

那么接下来，我会用三节课来介绍 8X Flow 的具体内容。今天我们先来讲一讲业务系统与领域系统的区别，因为这二者之间有着明显的弹性边界，而根据弹性优先原则，我们首先要对它们加以区分。

模型真的是稳定的吗？

正如我们在“云时代的挑战”这两讲中提到的，进入云时代后，出于更有效地利用云平台的目的，我们引入了新的架构约束：弹性优先原则。也就是说，在云时代架构系统中，弹性是最需要优先考虑的因素。

在具体讨论如何在业务模型中体现弹性优先原则之前，我想你可能会有这么一连串的问题。

对于业务建模而言，我们需要考虑弹性吗？

业务不应该是独立于技术的吗？难道我们从前云时代过渡到云时代，连模型也要改变吗？那么我们怎么还能相信，建模的稳定性？

我们在第 6 讲也说过：“一旦领域层出现重大改变，就意味着重大的业务调整，整个系统都可以推倒重来了。因而在软件系统有限的生命周期内，我们可以认为领域层应该是不变的。”难道说，云会让系统推倒重来吗？

这些都是很好的问题，触及了业务建模最底层的逻辑。而要回答这些问题，归根到底，我们需要弄清楚一件事：业务模型会不会受到技术范型的影响？

答案是：会，但不经常发生。那么什么时候会发生呢？就是在技术范型改变可以带来成本大幅下降，或者带来收入巨幅增加的时候。

说到底，我们做软件，特别是业务软件，并不是为了什么自我表达，而是为了更好地帮助业务方实现业务价值。而业务价值也很简单：产生更多的收入，或者用更低的成本。

那么，当技术范型的改变，可以带来巨幅收入或者成本改变时，业务自身的运营方式，必然也会利用这些技术的改变。所以从源头上，业务就变了，那么模型自然也要响应这种改变，作出对应的变化。

所以实际上，并不是新的技术范型直接改变了模型，而是新的技术范型带来了成本收入上的优势，业务方则为了利用这些优势，便根据这些新的技术范型，改变了业务运营方式，然后引起了背后模型的改变。

那么，能够引起业务重视的新的技术范型，自然不是什么新的编程语言或者新的框架，而是一些能够产生较大影响的技术范型的改变。

比如，企业级数据库系统（Enterprise Database System）的出现，直接将企业应用架构从单体桌面系统（Monolithic Desktop Application）架构转向了两层客户端 / 服务器架构（C/S，Client/ Server）。我觉得没有人会相信，自己在桌面应用中设计的模型，可以无缝过渡到数据库时代吧。毕竟我们需要思考范式、数据冗余、索引这些在之前根本不存在的概念，并在模型中加以体现。

类似的情况，在互联网（Internet）普及的过程中，客户端 / 服务器架构变成了多层架构（Multi-Tiered Architecture）。你在旧约部分能看到，过去十几年我们一直在与这种改变作斗争，让我们的建模方法可以适应架构的改变。

同样的，在 C/S 架构中的模型，也不能无缝过渡到多层架构里。那就需要考虑新的架构约束，重新建模。比如我们在旧约部分讲到的关联对象、角色对象和能力提供者等模式，都是重新建模时采用的手段。

可以说，好的模型可以在软件生命周期内保持稳定，帮助我们在特定的架构风格下高效应对变化。这没错儿。但是，模型不能帮我们渡劫。如果架构风格彻底改变了，那就没办法了，模型必须重建。

非常不巧的是，云计算就是我们现在正面临的这样一场大劫。而它带来的变革之大，也是这么多年来诸多变化之最。因为云改变了成本最低的运营模式，将其从统一模式（Unification）变成了复制模式（Replication）。

统一模式与复制模式出自四种业务运营模型，最早由 IBM 在上世纪八十年代发明用以描述不同业务模式间的差异，以及需要何种 IT 能力来支撑与其对应的模式，后来被商学院教程广泛采纳，就成了一个讨论业务模式与 IT 诉求的非常有用的宏观模型。如下图所示：

对于四种业务运营模型，我在这里还是做个简单的说明。你可以将流程标准化看做是否需要强制使用同样的业务流程；而将流程整合度，看做是否需要使用同一数据源。那么我们就可以这么来理解了：

统一模式：强制使用同样的流程，也需要使用同样的数据源。业务上是强流程强管控模式，对应在软件上的运营模式就是单实例模式（Singleton）。举个例子，比如用户身份系统，在逻辑上我们始终认为只有一种，也就是单实例。当然，这个单实例可以是一片集群或云化的，但只要在概念是统一的，那么它就是统一模式。

复制模式：强制使用同样的流程，但可以使用不同的数据源。业务上是以同样业务模式独立运营的业务单元（Business Unit），对应在软件上的运营模式是多租户模式（Multi Tenancy）。对于每个租户，他们如同独占整个系统一样，但是租户间数据是隔离的。也就是我们说的 SaaS 模式。

协作模式：不强制使用同样的流程，但需要使用同样的数据源。业务上是仅仅在财务流程和关键数据上互通的，更具有独立性的业务单元或子公司模式。对应在软件上，可以看做通过开放 API 提供对于共享数据的访问。

差异化模式：不强制使用同样的流程，也不使用同样的数据源。基本上就是不同的业务实体。对应到软件上，可以将其看作开放生态模式。

这四种业务运营模式，不仅仅是对 IT 系统，对组织结构也有指导意义。而从这个角度来看，我们行业内存在的很多问题，就源自业务组织能力与 IT 能力的不匹配。

比如说，业务组织已经逐步过渡到独立 BU 的形态，而 IT 系统却无法提供与复制模式匹配的运营能力。当然，这种影响也是双向的。毕竟在逆康威定律（Inverse Conway Maneuver）的影响下，最低成本的软件架构模式，也会影响企业对运营模式的选择。

我们知道，在前云时代，统一模式所对应的软件架构也就是单实体模式，是最容易实现，也具有最低的运营成本（在数据中心为主的基础设施上）。所以我们所熟知的最佳实践，都是在统一模式为最低成本的假设前提下，被总结确立起来的。

而在云时代，复制模式代替了统一模式，具有最低的成本。而业务方也在尝试改变运营模式，以获得云平台的红利。所以不仅仅是建模，我们所熟知的一切，都需要重新确认是否仍然是最佳实践。

说到底，我们不可低估云计算带来的改变。不过更重要的是，在这股巨浪中，我们不应保留任何先入之见，而是要去探索背后的核心逻辑。

什么是模型中最明显的弹性边界？

我们知道云是通过降低成本改变运营模式，从而给业务带来影响的。那么是否存在着某些业务功能更容易受到运营模式的影响，而另一些业务功能，则不容易受到运营模式的影响呢？

如果是这样的情况，就意味着这两种不同的功能存在不同的弹性边界。那么按照弹性优先的原则，我们就需要将这两类需求进行分离，从而在最宏观层面上，贯彻弹性优先的原则。

那真实情况是怎么样呢？让我们以极客时间专栏为例，罗列一下与之相关的功能点，看看这些功能点之间存在什么差异：

编辑发布新的专栏；

编辑为专栏定价；

读者订阅专栏；

读者阅读专栏；

读者转发专栏拉新返现；

编辑发布专栏内新的课程章节；

读者在课程下评论；

作者回复读者评论；

作者根据专栏销量获得提成。

现在让我们考虑一下，在运营模式改变的情况下，哪些功能点会发生改变。

比如说，极客时间专栏提供了企业版，也就是对企业收取特定的费用（每年或者一次性），这样企业内的员工就能无限或者按照规定的量来自由阅读专栏。

可以发现，与发布、阅读、评论相关的功能点，无论是公开版本，还是企业版本，都没有什么区别。而定价、返现、提成等功能，则会发生较大的改变。也就是说，这些功能点更容易受运营模式改变的影响。

如果按照变化隔离的原则，从架构上将这两类功能分割开的话，我们就有了一个不同的看待极客时间系统架构的角度。我们将它分成两部分：一个是与运营无关的内容管理系统（Content Management System，CMS）；一个是极客时间的运营系统。

那么，企业版不过就是在内容管理系统上，提供了另一套运营系统而已。如下图所示：

由此我们就发现了一个常用的架构模式，可以把系统分解为两部分：与运营无关的部分；提供运营能力的部分。

比如，对于类似 Google、Bing 这类搜索引擎而言，与运营无关的部分是搜索引擎，而与运营有关的则是广告的投放和竞价。对于类似优酷、爱奇艺这类视频网站而言，与运营无关的是流媒体部分，而与运营有关的则是 VIP 账户、广告等。

下面让我们来看看分开的这两部分中的模型。在与运营无关的部分中，模型似乎不怎么需要业务方的输入。比如说：

极客时间专栏中的内容管理系统，核心的概念是专栏、课程和评论，和通用的内容管理系统中的栏目（Category）、文章（Article）、评论（Comment）似乎没有什么区别。

类似地，对于搜索引擎而言，与运营无关的部分中，关键概念是分词、词频、索引、爬虫等，似乎更不需要业务方的输入。

而在与运营相关的部分，模型则需要业务方的输入和验证。比如说：

对于极客时间专栏而言，这部分牵扯到如何定价、如何分成等诸多业务规则。在没有业务方输入的前提下，几乎不可能构造出任何的模型，甚至都无从开始。

类似地，对于搜索引擎而言，广告如何与关键词关联、如何计算投放量、如何衡量广告效果等等，就需要业务方制定相应的规则与逻辑。

那么我们就会发现，在进行建模时，针对与运营无关的部分和与运营有关的部分，我们需要处理的核心复杂度并不相同。而且，“两关联一循环”对于这两部分的功用也不尽相同。事实上，从 Eric Evans 提出领域驱动设计开始，行业并没有明确区分这两种不同的领域逻辑。所以我们一直都在用“领域”这个词，指代这两种不同的领域逻辑。很多误会与歧义也是从这里就产生的。

我个人从 2010 年开始，就分别使用“领域”（Domain）和“业务”（Business）来指代这两类不同的逻辑。即：“领域”表示与运营无关的问题域，“业务”表示与运营相关的问题域。

之所以将与运营无关的逻辑称作“领域逻辑”，原因有二。

第一，这两部分分别有不同的建模方法。既然领域与业务有不同的关注点，那么针对不同的关注点，自然有不同的最佳实践和最佳建模方法。分别对待，会让我们更容易得到恰当的模型。

第二，在我们区分了领域逻辑与业务逻辑之后，再回过头去看 Eric Evans 的领域驱动设计，会发现他在书中给出的例子，大部分都是与领域系统相关的。所以他没起错书名，他讲的的确是“领域驱动设计”。因此，如此命名可以减少概念上的冲突。

究竟是领域建模还是业务建模？

知识消化的“两关联一循环”，对于领域逻辑和业务逻辑都是有效的吗？还是只对某一种逻辑更适用呢？从我的经验来看，通过“两关联一循环”对业务逻辑建模的效果，要远远好于对领域逻辑的建模的效果。

业务逻辑与运营相关。运营本身也没有绝对的好坏之分，只有合适与否。大量的业务逻辑其实是源自运营实践经验的总结，其背后也不一定存在什么严谨的逻辑。所以所谓的“专家”可能也只是经验丰富而已。

我经常举这样一个例子。比如你和你朋友都是卖烤白薯的，你们分别在东直门地铁站左右站立，配方、产品、文案无一不同。唯一的差别是，你每天进货，而你的朋友一次进一周的货。那么在运营中，你会有很多与采购有关的操作，而你的朋友则更多关注库存的管理。当你们都成为红薯专家的时候，如果互换位置（听起来有没有职业经理人空降的感觉），其实都玩不转。因为你不知道怎么管库存，而你的朋友可能无法处理频繁的采购操作。

业务逻辑是和某种运营方式绑定的，我们把业务逻辑的这个特性称作运营特定（Business Operation Specific）。知识消化对于这类逻辑的总结与提炼，具有很好的效果，这个我们在旧约部分已经讲过了，就不再复述了。

相对地，你可能会发现，如果把卖烤红薯变成卖烤土豆，那么采购或是库存的业务逻辑其实没什么改变。这种特性我们叫做领域中立（Domain Neutral）。

也就是说，某种业务运营的模式（也就是赚钱的办法）其实可能和对应的领域功能没什么关系。比如极客时间专栏的订阅分成模式，如果把内容换成网剧，其实也是成立的。还有 Google 的广告竞价模式，把搜索引擎换成门户类网站，也是成立的。

因而业务逻辑一般具有运营特性和领域中立性。与之相对的，就是领域逻辑具有运营中立性和领域特定性。那么这里就有一个问题：对于领域逻辑，知识消化法还有效吗？

事实上，对于能通过面向对象模型表示核心复杂度的领域逻辑，知识消化法是能够发挥作用的。因为我们讲到现在，隐含的前提就是通过对象模型建模问题。但是，并不是所有的领域逻辑都适合使用对象模型表达。

比如推荐引擎，通过特征建模（Feature Modeling）并使用相似度算法，可能是更好的方法。当然，也可以是决策树（Decision Tree），甚至是神经网络（Neural Network）。这时候，知识消化并不能帮助我们得到这些模型。

那你可能会问，为什么 Eric Evans 可以从“领域逻辑”出发，提炼出知识消化法呢？

这主要是因为，他并没有区分领域和业务，所以他想解决的问题是对“业务逻辑”的提取。而书中例子多是“领域逻辑”，只不过因为“领域逻辑”更好举例而已。

所以知识消化实际是一种“业务建模”方法，而不是“领域建模”方法。这也是为什么，我们这门课叫《如何落地业务建模》。

总结来说，将领域与业务分开，可以让我们更好地理解什么是业务系统：支撑业务运营，利用领域系统赚钱 / 省钱的系统，叫做业务系统。所以我们业务建模的目的，就是寻找到恰当的业务模型，更好地支撑业务系统的构建与演进。

小结

这节课我们主要介绍了云给业务运营模式带来了什么样的改变，以及这种改变会如何影响我们获取业务模型。然后，我们将逻辑分成了领域逻辑与业务逻辑，分别讨论了它们的异同，以及我们究竟是在针对什么进行建模。

总结起来说，业务逻辑是源自业务运营的逻辑，是领域中立且运营特定的，其复杂度来自于业务流程，关注的是如何通过某个领域逻辑实现获利。因而它的关注点，就集中在盈利和成本结构上。或者说，业务逻辑对外体现为利润或现金，对内体现为成本和绩效承诺。

而领域逻辑源自问题域自身的逻辑，是运行中立而领域特定的，其复杂度来自于问题本身，关注点通常是算法、计划、统计、优化等等。

所以关注业务逻辑的组件与关注领域逻辑的组件，通常具有不同的弹性边界。以极客时间专栏为例，在缓存策略恰当的前提下，如果读者猛增，那么对内容管理部分的弹性需求，会远远小于对业务运营部分的弹性需求。而对于搜索引擎业务来说，当搜索用户猛增时，对广告投放这部分的弹性需求，要远远小于对索引部分的弹性需求。

因而从弹性优先的角度出发，我们应该将关注业务逻辑的组件与关注领域逻辑的组件分离。这样构建出来的模型，更能适应云时代的架构约束，也能让我们分别使用不同的建模方法，获得对应的领域模型与业务模型。

思考题

对于业务系统来说，除了可以使用事件建模法之外，还有没有其他的方式，可以让我们更好地从“盈利”的角度来展开业务维度？此外，在我们常见的领域系统中，有哪些可以使用非对象模型以更好地描述问题？

欢迎在留言区写下你的想法和思考，我会和你交流。我们下节课继续 8X Flow 的学习！

15｜8X Flow（中）：如何通过模型发现业务系统的变化点？

你好，我是徐昊。今天我们继续 8X Flow 的学习。

上节课我们讲到可以将逻辑分为领域逻辑和业务逻辑，而且可以通过不同的方式，分别对领域逻辑和业务逻辑建模。我们也知道了，领域与运营无关，源自某个特定问题域，而业务逻辑与运营相关，大量的业务逻辑源自运营中实践经验的总结。

这个时候我们就需要考虑一个问题：领域逻辑真的能复用吗？我们来分析一下。

领域逻辑真的能复用吗？

由此我们会发现，业务逻辑不如领域逻辑稳定。毕竟业务关注的问题是如何盈利，而不是怎么准确地描述某个问题域。这似乎和我们长久以来的想法相违背：我们希望通过模型精确建模某个问题域，从而实现对该问题域的复用。

然而在商业社会里，复用某个领域，就是围绕这个领域构建能够盈利的运营模式。也就是说，复用某个领域，除了复用领域功能之外，还要构造运营实体。

就像直到今天不会还有人天真地以为，只要复制了淘宝的功能就能再造一个阿里吧！软件总是简单的，但是构造背后的运营实体，以及确立成本合理的运营模式，才是复用的真正重点和难点。换言之，业务知识的重建与复用，才是领域复用的关键。

从这个角度来说，领域驱动设计就是天真的谎言。因为它所畅想的美好的复用方式，大多源自技术领域的领域系统，并且不假思索地将这种复用模式推广到业务系统，而忽略了业务系统的运营特定性与领域中立性，这就给我们植入了一个虚假的愿景：通过领域驱动设计，我们可以构造某个领域的通用产品，正如数据库一样，然后就可以忽略业务逻辑，在不同企业间形成复用。

按照这个思路，领域驱动设计所希冀的最大成就，就是实现类似于 MRP（Manufacturing Resource Planning）或者 ERP（Enterprise Resource Planning）这类领域产品。然而，事实告诉我们，所有 ERP 系统最终都会走上两条路。

保持产品不变，通过管理咨询公司介入，重组企业流程，使得它们的运营模式可以与产品匹配。

大量定制产品特性，将受众公司的业务逻辑嵌入到领域产品中。

所以就算把领域做成了产品，也还是不能离开业务逻辑直接复用。要么改变客户的业务以适应产品，要么再把业务逻辑加回到产品中来。所以领域驱动设计其实是个非常纠结且过时的理念。领域驱动中的大部分不协调和奇怪之处，都不是另有深意，而只是单纯地想错了。

再顺便说一句，知识消化的“两关联一循环”其实是尝试去解决这些没有从领域逻辑中分离开的那部分业务逻辑的。毕竟单纯只是建模领域逻辑的话，找到一个简洁的隐喻，将系统核心问题描述清楚，比其他一切手段都有用，也都重要。

要是实在找不到，通过知识消化磨出一个对象模型，也凑合能用。这也是为什么我说用知识消化对领域逻辑建模，没有对业务逻辑建模来得有效（也可以用）的原因。

那么对于业务系统，也就是与盈利相关的系统，我们更应该关注的是业务逻辑（将其中的领域逻辑做好隔离就行）。那么对于业务逻辑，我们有什么好办法去系统化地理解吗？

答案是有！

现实生活中，在我们现行法律下，所有企业活动都必须在合同法（现在是民法典）的框架之下进行，而且需要满足第三方审计要求。所以就有这么一群人，他们可以在完全不了解某公司领域的前提下，能快速且充分地了解该公司的业务情况，并指出其中可能存在的问题。这些人就是财务审计、合同审计和职业经理人。

那么也正是这么一群人，每天都在完成我们眼中的艰深的工作：虽然对具体情况了解不多，但能快速且细致地理解业务逻辑。于是，我们只要学习他们是怎么做的，就可以将他们的方法用于理解业务逻辑，并进行业务建模。

他们是怎么做的呢？也就是：以凭证追溯从财务角度理解企业经营，以及从合同履约理解企业业务。

事实上，构成审计核心逻辑的凭证追溯，也构成了四色建模的核心逻辑。而合同履约则是 8X Flow 的核心逻辑。

从合同履约理解业务

所有业务活动都需要在合同（具有法律效力的口头约定）的框架下进行，这是法律法规的要求。

比如说，我们网上购物之所以能拿到货品，是因为我们和网店之间存在采购合同（也就是订单），其中规定了为顾客提供货品是商家的义务，拿到货品则是我们的权利。同样的，商家之所以能收到钱，也是因为采购合同的存在，付款是我们的义务，收款则是商家的权利。

以上是权责正常得以行使的情况。但是除此之外，还有大量的业务逻辑是由违约场景构成的。比如还是网购的例子，如果我们没有付款，那么在一定时效内，订单可能会关闭。如果是预定订单，那么商家就有权利不退还预付款等等。

违约场景同样也是合同上下文中规定的权责项。就是说，虽然一方违约，需要承担对应的责任，但是这个责任也需要预先约定，由合同覆盖。

不过如果违约方对于违约责任的承担继续出现了违约呢？比如欠款不还，触发罚息，罚息继续不还，触发强制要求本金归还。或者如果再继续不还呢？那只有去法院打官司了。这时候法院会根据合同中约定的权责履约情况进行裁定，也就是终极履约。

于是我们发现，在所有的业务逻辑中，权责履约是最小的业务交互，合同是最小的业务上下文。那么我们就能使用权责和合同上下文对业务进行建模了。

对于权责履约，我们可以使用履约请求（表示一个时间段）和履约确认（时间点）这样的结构来表示。并将相应的凭证与履约请求关联，以表示发起履约的依据。同样，履约确认也会与相应的凭证关联，以表示履约的证明。

履约请求是由权利方发起的，并要求义务方在规定时限内完成履约。比如我们在网上购物，完成下单之后去支付。这个过程从合同角度看，是权利方卖家要求义务方买家，在 15 分钟内完成对订单支付的履约。如果买家在规定时间内完成支付，就是履约成功。否则就是履约失败，比如没有支付或者因为网络故障等情况，未完成支付。

凭证在权责履约的范围内，可以按照四色建模的方式寻找。也就是说，针对每一个权责履约，实际是在业务上定义了一小段时间线，从履约请求开始，到履约确认为止。这是利用了事件建模中的多时间线法。而我们可以在这条时间线上追溯履约中的关键凭证。

除了履约时间线上的凭证之外，履约请求还可以引用合同上下文中的其他凭证。特别是，履约请求和履约确认本身也是凭证，可以供其他请求与确认引用。比如，卖家未在约定时间内发货，那么违约取消合同的时候，就可以引用之前的支付履约请求或者支付履约确认，证明卖家没有发货。

明确了履约的结构之后，合同可以作为上下文对象，用以涵盖对应的所有履约项。而且由于合同只存在于双方之间（多方合同是多份双方合同），因而任何合同上下文中都存在两个角色对象。比如采购订单合同中的买家和卖家，快递合同中的邮寄方和承运方等。

在整个合同上下文中，权责也是围绕着两个角色展开的。所以，我们可以将合同上下文看做两个角色间业务交互的证据的聚合。这是一种业务上存在的聚合关系，是一种比对象聚合更具有业务含义的包含关系。

合同的参与方以及凭证中的标的物，可能来自领域系统。比如说，我们网购的采购合同中的支付凭证，牵扯到的标的物就是商品。那么我们可以将商品看做领域系统产品目录中的概念，然后通过凭证引用领域系统中的概念，让领域系统中的概念参与到业务逻辑中来。

到这里，8X Flow 的核心概念就讲完了，接下来我们看看该怎么通过 8X Flow 进行建模。

使用 8X Flow 建模

如下图所示为 8X Flow 的元模型（Meta Model）图，表示了上面我们所讲的核心概念，以及它们之间的关联：

使用 8X Flow 建模的流程大致是这样的：

寻找合同上下文，明确合同的参与方；

寻找合同中的主要履约项，按四色建模寻找凭证；

对于主要履约项，寻找违约情况，设立新履约项，按四色建模寻找凭证；

重复 3，直到不得不打官司为止；

将参与方和标的物划分入领域边界。

下面就通过例子来看一下这个过程，还是我们的老朋友，极客时间专栏。首先来看一下读者侧，合同是非常明确的，即专栏订阅合同。参与方就是读者和极客时间。

然后，我们来看看主要履约项：

支付订阅费用，权利方是极客时间，义务方是读者。

访问付费内容，权利方是读者，义务方是极客时间。

那么我们可以建立一个业务模型，里面包含合同和这两个主要履约项：

接下来我们就需要寻找违约情况了，对应上面讲的两个权责，那么分别存在如下两种违约情况：

未在规定时间内完成支付，那么合同自动作废，读者并不承担额外责任。权利方是极客时间，义务方是读者。

如果专栏出现断更，没能按说明提供内容，专栏下架，极客时间退钱，并且在下次同专栏上架时，不再向原读者收取任何费用。权利方是读者，义务方是极客时间。

对于第一项违约，合同作废是自动行为，不需要义务方确认，所以不需要有独立的权责项，也没有后续继续违约的操作了。

对于第二项违约，如果极客时间拒不退钱，那么读者也只能走法律手段了。这就已经触碰到需要走法律程序的边界了，那我们就不需要继续寻找违约项了。

所以我们可以继续添加模型到上面的模型图中，并将标的物划入不同的上下文，以表示领域边界，如下图所示。这里我也说明一下，由于图片大小关系，我省略了一些凭证，还简化了权责方与请求和确认的关联。不过这并不影响对模型的整体理解，你可以着重看一下权责履约项：

至此，我们就通过 8X Flow 完成了对极客时间专栏的建模。不过如果我们仔细观察得到的模型，会发现几个有意思的现象。

首先，通过 Request-Confirmation 表示的履约结构是一种异步结构：在规定时限内，未得到确认之前，履约处在未知状态。这种异步并不是技术上的刻意选择，而是业务的真实状态。

让我们回想一下真实世界中的业务操作，其实都是这样的异步状态：权利方主张，义务方履约。而我们在软件世界中习惯的是同步状态，是在很短时间内完成的主张 - 履约。但无论这个完成的时间有多短，在概念上，主张、履约仍是异步结构。

所以严格意义来说，我们过往通过同步方式建模都是错的，都是从实现方式去理解问题，而不是从问题本身出发。

其次，如果履约出现错误，也就是不能履约的情况，那么对于这种异常状态的修正，就需要触发新的履约过程。

想想看，在现实世界中的业务，从来不存在可以自动修正的情况。因为任何履约失败，都存在破坏合同条款，最终引起法律纠纷的可能。所以不可能在合同双方不知情，也没有提前协商的情况下，对未完成的履约项进行修正（而且严格意义来讲，这么做也是违法的）。

因而，从业务角度出发，履约中的异常会触发新的履约项，从而在合同的上下文中维持业务的一致性。

也就是说，如果我们完全从业务出发，不受对象模型搅合的话，就会自然地得到异步为主的模型以表示业务逻辑。那么接下来，就让我们看看合同上下文。

凭证角色化建立合同间的关联

如果再仔细看前面的模型，你可能会发现，我们将支付部分归属于专栏订阅合同了。这表明什么呢？表明这是一个线下的现金交易模型。也就是读者和极客时间面对面完成了现金交易，然后极客时间开具发票。

因为在专栏订阅的合同上下文中，参与的双方是读者和极客时间，既没有微信，也没有其他的参与方。因而说明，这是个现金交易模式。不过，这显然不适合现在的数字化时代。那么如果我们需要支持移动支付，模型会有什么改变呢？

首当其冲的改变就是引入了第三个角色：移动支付供应商。我们知道合同只能在两者之间签署，那么我们势必需要引入另一个合同上下文，去表示移动支付供应商和读者之间的关系。也就是说，在读者和移动支付供应商供应商之间，存在一个合同，用以支撑支付行为。

这个合同呢，其实你也不陌生，就是你开通微信或者支付宝支付服务时，签署的用户协议（对，就是你看也没看就点了“同意”的那篇“废话”）。在这个用户协议中，规定了如何付款与收款。那么我们同样可以通过 8X Flow 对这个用户协议进行建模，过程与上面大同小异，不再赘述，直接来看结果：

通过这个模型我们很明显就能发现，业务模式改变了，从面对面的现金交易，变成了借由第三方支付服务完成的交易。

因为在专栏订阅合同上下文的履约确认中，我们关联了另一个合同，也就是移动支付协议上下文中产生的凭证，而这个凭证是由合同上下文中的义务方移动支付供应商提供的。

那么这种跨合同上下文的凭证引用，实际上就表示了不同合同履约项中权利方与义务方间的协作。专栏付款确认引用了移动支付确认作为凭证，也就是说，专栏订阅合同上下文中，专栏付款履约项的权利方极客时间，和移动支付协议上下文中，移动支付履约项的义务方移动支付供应商，共同协作，为用户（移动支付协议合同上下文）完成了支付，以帮助读者（专栏订阅合同上下文）完成了订阅。

说句题外话，你是不是觉得不同的合同上下文和服务边界很像呢？有点儿眼熟对吧？这其实就是服务边界在业务上的体现。我们费尽心力，希望从聚合和其他技术能力上寻找的服务边界，天然就存在于业务中。而且具有明显的结构特征！只要睁眼看一看，怎么还能找不到服务边界呢！

好了，言归正传。我们通过跨合同上下文间的凭证引用，建模了不同服务之间的协作。但是对其他上下文中凭证的引用，实际上表示了不同上下文间的依赖关系。

也就是说，在上面的例子里，专栏订阅合同是依赖于移动支付协议的。那么如果我们引入另一种支付模式，比如说预充值抵扣，那么我们可能就会引入另外一个合同上下文：预付费合同。而专栏订阅合同也会依赖于它。如下所示：

这里明显会出现一个坏味道，随着新支付方式和手段的引入，专栏订阅合同上下文会不断地去依赖新的合同上下文。

且不说由于依赖关系引起的变化传播，单从业务上讲，专栏订阅合同是极客时间的核心业务逻辑，也是核心差异化所在。而不同的支付合同，更像是为了支撑这个核心业务逻辑而存在的。那么对于这种依赖关系，我们就需要小心了，因为让核心逻辑依赖于支撑逻辑，总是一种坏味道。

业务系统中的变化点

不过解决的办法也很简单，那就是反转依赖，我们可以让履约确认角色化，让其他合同中的凭证来扮演这个履约确认。这其实就是我们在第 9 节讲的能力供应商模式，它是面向对象技术中反转依赖的一个小应用。如下图所示：

通过将专栏付款确认角色化，我们就在专栏订阅合同中引入了一个变化点。就意味着这个履约项，可由多种不同凭证来确定，也就是存在跟其他不同合同上下文交互的可能性。而这种对业务变化的判断，并不是源自技术方案，而是从业务本身的结构出发，寻找可能存在的变化点。

于是我又要说句题外话了，我们不停地通过各种建模手段，希望可以预判业务改变的方向，尽早做出隔离，从而更好地响应业务的改变。同样的，业务逻辑变化的可能性天然也就存在于业务中，而且具有明显的结构特征！只要睁眼看一看，怎么能找不到业务变化点呢！

小结

你肯定还记得我们在第 13 节讲云计算给建模带来的挑战时，总共讲了四条：

确立一种模型结构以反映弹性边界；

在弹性边界切分业务上下文时，维护业务一致性；

从异步模型的视角，解读业务逻辑；

在异步调用产生中间态异常时，维护业务一致性。

我们来看看怎么应对这四个挑战。前两条与弹性边界有关。我们知道合同上下文表示了服务边界，其中合同的乙方就是服务提供者，甲方就是消费者。那么是不是合同上下文就是弹性边界呢？

显然不是！因为在合同中，不同的履约项明显具有不同的弹性诉求。就以专栏订阅合同为例，付费内容访问明显比专栏支付与断更补偿具有更多的容量诉求。

所以合同上下文并不是弹性边界，履约上下文才是弹性边界。此外，由于我们分离了领域逻辑和业务逻辑，那么领域逻辑也是弹性边界。

在领域逻辑中，数据一致性为主导；而在业务逻辑中，是合同上下文中对业务逻辑的聚合为主导。合同上下文中的业务一致性，也就是履约与违约构成的一致性，因而业务逻辑的弹性改变并不会影响领域逻辑。

所以我们说，通过切分领域与业务边界，分别以领域逻辑边界和合同上下文中的履约上下文作为弹性边界，就能解决在弹性边界切分业务上下文时的一致性维护问题。

后两条与异步模式有关。这里有意思的是，如果我们完全从业务角度出发，遵循主张 - 履约的方式建模（比如用 8X Flow 建模），这根本不是问题：因为业务本身就应该通过异步解读；异常状态作为违约，触发其他履约项或诉诸法律。

也就是说，我们之所以会认为后两条是问题，主要是因为我们采用了技术的出发点，从解决方案去理解问题，才有了如此结果，这是我们应该反思的。

至此，我们不仅通过 8X Flow 获得了一种纯粹的从业务角度出发的建模方法，而且，通过 8X Flow 得到的模型，可以帮助我们解决云时代给业务建模带来的四个挑战。

最后讲一个小题外话。表达想法、提出问题，问题就等于解决了一半。非常期待你能把自己的想法或问题、总结或笔记分享出来，我在留言区等着你。

思考题

对于其他合同上下文的引用，能不能将其转化为领域系统（比如支付子系统），并与业务部分整合？我们现在的建模是从合同确立开始的，那么合同确立之前要怎么建模？

16｜8X Flow（下）：多于一个例子

你好，我是徐昊。今天我们继续 8X Flow 的学习。

上节课我们讲解了如何通过 Request-Confirmation 结构去建模履约项，并通过合同上下文聚合履约过程中产生的凭证。之后，我们又通过将履约确认泛化为角色，让凭证可以跨越不同的合同上下文完成履约。那么对于合同签订之后的业务逻辑，就都可以如此建模了。

不过到目前为止，你会发现，我们对业务逻辑的建模都是以合同签订为起点，以合同履约完成为终点。在合同上下文中聚合的业务逻辑，可以通过履约进行结构化地理解和建模。那么对于合同签订之前的业务逻辑，该怎么建模呢？

这正是我们接下来要讨论的问题。同时，这节课我们还会通过一个例子，将这三节的所学串到一起，来让你对 8X Flow 建模有更多的体会。

以事件建模法使用 8X Flow

由于合同尚未签订，所以也就不存在履约项，于是我们无法通过履约关系去建模业务。这时我们要怎么办呢？答案是：不要自己臆想，要从生活中学习。

在现实的业务中，绝大部分的合同签订，都遵从这样一个过程：请求邀请投标（Request For Proposal）、投标（Proposal）、合同签约。

如果算上合同之后的履约，那么我们可以将整个业务划分为四个不同的阶段：邀请投标、投标、合同签约、履约。

其中合同签约是最重要的时间点，合同是最重要的业务产出。如下图所示：

让我们用一个最简单的场景理解一下业务从邀请投标到履约的全生命周期。假设你现在去菜市场买菜，看到菜摊上的朝鲜蓟（Artichoke）妖艳诡异。

朝鲜蓟，一种高营养的保健蔬菜，具有良好的减肥功效，在法国菜中较为常见

于是就问摊主这个东西怎么卖。这就是你作为买菜的甲方，对卖菜的乙方提出的邀请投标。

摊主回答你说，一个 20 块，这就是投标。

如果你选择不接受这个投标，也可以提出另一个邀请投标，比如说，两个三十卖不卖。

乙方的顾问可能会说，掌柜的，瞧本，再卖就赔了。于是乙方回答你，卖不了。

最后你难以抵挡朝鲜蓟的诱惑，说行，20 就 20 吧，给我拿一个。

于是口头合同成立。你掏钱付款，完成支付履约；摊主把朝鲜蓟递给你，完成交割履约。至此，业务执行完毕。

这时只要我们使用四色建模法，按照凭证的追溯，就能完成对签订合同之前的业务逻辑的建模，没什么难的。

另外，按照这个生命周期，我们可以以事件建模法使用 8X Flow 建模。也就是说，你可以将 8X Flow 的建模过程，改造为共创工作坊、引导式工作坊，或者是头脑风暴。而我们只需要按照凭证所处的阶段，将它们分别贴到对应的位置即可。

比如上面买菜的例子，我们可以通过头脑风暴得到如下的结果：

接下来，我们只需要在履约凭证中引入不同的履约请求，形成履约项。再根据凭证，寻找对应的参与方与标的物。然后再寻找违约补偿，就可以自然地进入 8X Flow 的建模流程了。

这也是我们讲，事件建模法是一种元方法。大部分建模方法都可以通过某种形式的事件建模，更多地与业务方形成交互与共创共建的氛围。如果所结合的建模法是一种强分析法的话，那么就能获得一种既有良好交互氛围，又能得到高质量模型的好方法。

回到上面这张图。如果再配合上投标邀请和投标，我们就可以在全生命周期上对 8X Flow 的元模型有一个更全面的视图。如下图所示，合同之前的上下文（也就是投标上下文）和合同上下文是完全分离的：

通过这个视图我们至少可以明白三点。第一，合同之前的上下文和合同上下文应该具有不同的弹性边界。

从业务上讲，只要不签合同（或是口头协议），那么双方就不具有任何法律约束，合同之前的上下文与合同上下文完全无关。合同之前的上下文中的关键信息，最终会汇总到合同上，一旦合同签订，那么在此之前的所有协商并不具备法律效力。因而从业务上讲，两者是完全无关的，自然可以分离。

从实际业务上讲，二者的弹性诉求通常也差距巨大。以网上购物为例，浏览啊、查找啊、比价啊、砍价啊、拼单啊、拼团啊，都是合同之前的业务。而下单之后的采购合同签订了，然后才会进入到合同履约的环节，弹性诉求自然不同。

第二，合同前的上下文是系统另一个重要变化点。

因为合同上下文与合同之前的上下文没有什么关系（仅仅在审计时，满足可以追溯即可），那么可以说，在合同履约中，我们并不关心合同是如何生成的。

在实际业务中，最终能够签订合同的途径有很多种，而我们不需要把它们强制归纳为一种模式。我们需要做的，是将合同之前的上下文看作渠道上下文，并承认它是系统中的变化点，这样就会从架构角度为系统带来极大的简化。这也是构造中台时，一个非常有用的技巧。接下来的第 17-19 讲里，我们也会讲到。

第三，虽然投标邀请和投标并不是履约项，但是它们也具有 Request-Confirmation 的结构，所以实际上也是异步的业务行为。这也再一次证明了我们上节课所讲的，异步在业务的交互中无处不在。

如果系统中不包含合同呢？

讲了这么多 8X Flow 的内容，我想你一定会有这么一个问题：如果我做的系统并不包含对外的合同，那么要怎么办？

有这几种情况。第一，你做的系统的目标是管理内部绩效。比如说客户关系管理系统（CRM，Customer Relationship Management），目标管理系统（Objective Management），销售管理系统（Sales Management），等等。

虽然这些系统并不牵扯对外的权责履约，但是我们仍然可以使用权责履约对这类系统进行建模。因为从业务上讲，这类系统是存在履约项的，而履约项，其实就是干系人的工作产出、KPI、OKR 等等。

让我们以 CRM 电话销售为例，通常在一年或者一个季度的开始，管理者会与电话销售就绩效目标达成一致。

比如，每月需要联系客户多少次，其中有几次是电话、有几次是邮件等等。然后电话销售就需要在每次联系客户的时候，记录凭证，以便在周会或月会上汇报。

那么从合同上下文和履约的角度来看，管理者和电话销售就绩效目标达成一致，实际上是一种口头的绩效合同，或者叫绩效协议。那么周会、月会，实际上就是进度履约的检查和确认。

所以我们仍然可以用履约框架对其进行建模，而且只需要将元模型稍作修改即可：

可以看到，在这种情况下，我们仍然是以合同履约的形式来分解的业务逻辑。毕竟说到底，我们日常用人仍然在《劳动法》的框架之下。而《劳动法》规定，如果用人方要开除某个员工的话，需要通过举证来证明员工不符合用工标准，并且是持续的。因而我们把绩效约定和进度检查作为日常的管理框架，也就不足为奇了。说到底，都是攒证据，以保证管理活动不违法！

第二种情况，你做的是领域系统，并不在合同上下文之内。

我们仍然以上面这个 CRM 电话销售为例。假设你所做的是为电话销售提供客户信息（标的物），那么你做的系统就处在合同上下文之外了，当然不会具有合同上下文了。这时候你就需要按领域系统建模。

第三种情况，你做的是工具。仍然是 CRM 电话销售的例子。你做的系统是帮助电话销售，直接从电脑上控制座机电话拨号。同样的，不是业务系统，不需要业务建模，可能需要领域建模，也可能不需要（如果就是简单集成的话，就是胶水代码）。

第四种情况，以上都不是，那么最有可能是你做的系统并没有按照合同进行分析。其中可能含有合同，但是你没意识到。这个时候，你可以尝试对系统重新进行建模，没准儿就会发现你的合同上下文放在什么地方了。

或者，最可悲的，你做的系统完全不重要，跟业务没有任何关系。比如在任何的内外合同 / 协议上下文中，都不留下任何痕迹，也不会被人当作工具使用。这个时候，你需要反思的不仅仅是这个系统了，可能还有你的职业生涯。

关于 8X Flow 方法的内容就这么多，接下来让我们看个例子（里面包含很多个合同，所以可以看成是多于一个例子，是为点题），从而让你有更多的体会。

以合同和履约建模极客时间专栏

在我们《如何落地业务建模》这门课中，贯穿始终的一直都是极客时间专栏这个例子。那么自然，我们还是要用 8X Flow 来彻底地分析一下它。

之所以把极客时间专栏作为例子，一方面是因为我们都在使用极客时间学习，对它的功能比较了解，我也就不需要写很多废话来介绍它的上下文了。另一方面则是因为，在极客时间专栏的背后，还有与作者的分成，以及编辑团队与作者催稿等不为人知的内情。

所以哪怕极客时间专栏看起来是一个付费的内容管理系统（Content Management System，CMS），但它背后还是有足够多的业务逻辑值得我们深究。那么今天我们就继续以它为例，分析一下背后的业务逻辑。

我先简要概述一下专栏是如何与作者签约的，以及专栏是如何上线的。大致流程如下：

确定选题：首先是编辑与专栏作者沟通，就课程涉及的内容达成意向。

安排试写：编辑会要求作者去试写一下课程中的内容，对语言风格和写作习惯进行打磨。

打磨大纲：在编辑认可作者写作能力之后，需要作者确认目录以及课程中文章数目。

打磨样稿、选题评审：作者会开始试写前面部分的文章，当文章累计到达一定数量之后，编辑会进行立项讨论，也就是是否可以开课。

如果确认可以开课，那么就要签署作者协议了。

（1）支付协议内容中涉及的预付款，也就是按照课程中文章数量，提前预付一定金额的稿费。

（2） 约定的课程价格，以及作者分成比例。

（3）约定作者收款周期，以及收款方式。

（4）极客时间会提前扣除预付的稿费金额，直到有所盈余，然后再继续转给作者。

（5）作者在课程进行中应保持持续交稿，不能断更。

（6）如果出现断更的情况，作者需要赔付违约金若干。

编辑在收到足够数量的存稿之后，就可以准备课程上线。

在课程上线之后，编辑仍然需要按照约定好的节奏，督促作者写稿并按时完稿。

每个编辑可能要督促多名作者，主编会按照每名编辑的工作，安排必要的检查与核对。

通过对上述业务逻辑的描述，我们发现，其中至少存在两个合同上下文：极客时间和作者之间的创作协议；极客时间和编辑之间的绩效约定。

那么让我们分别来看一下这两个合同的履约项，首先是极客时间和作者之间的创作协议：

支付预付款，权利方是作者，义务方是极客时间。

收入分成，权利方是作者，义务方是极客时间。

交稿，权利方是极客时间，义务方是作者。

违约金赔付，权利方是极客时间，义务方是作者。

这时我们需要询问，如果极客时间没有按期支付预付款、没有按期支付收入分成、违约金迟迟不肯赔付，以及如果作者不能交稿等等。我们会发现，违约金赔付其实已经是交稿的违约履约项了，而预付款和收入分成并没有对应的违约履约。如果是在现实中，我们就需要去和业务方确认是不是如此的情况。

在这里的例子中，我们可以假设，如果出现这样的情况，就直接打官司。那么我们可以对这个合同上下文进行建模：

看完了极客时间和作者之间的协议，然后我们再来看极客时间和编辑之间的绩效约定。

事实上，我们并不知道极客时间内部究竟是如何组织编辑们的工作的，但是我们可以根据业务的常识进行判断。通常在不考虑违约的情况下，内部绩效约定只有两个主要权责履约项：目标设定和进度检查。

如果管理流程对于目标的确定性要求较高，那么目标设定可以看做邀请投标 - 投标，也就是绩效协议确立前的讨价还价。比如以年度为单位进行目标设定，通常会留有一个季度的时间，作为目标计划与磋商的阶段。

而如果是更为灵活的工作内容和产出，则可以将目标设定看做正常的权责履约项。这时可以将目标设定看做合同变更条款，也就是在合同执行过程中，若要修改合同内容，会进行怎么样的操作。

按照这个思路，我们可以列出极客时间和编辑之间绩效协议的主要履约项：

目标设定，权利方未知，义务方未知；

进度检查（周度），权利方是极客时间，义务方是编辑。

可以看到，对于目标设定，我们没有指定权利方和义务方。这主要是因为在不同的目标设定方法下，权利方和义务方是不同的。

比如在一个强管控的运营模式下，一般是主编会为编辑设定任务，然后编辑同意。这时候，权利方是极客时间，义务方是编辑。

而如果在一个强调主动性的运营模式下，编辑会为自己设定目标，然后由主编判断目标是否合理。那么这时候，权利方是编辑，义务方是极客时间。

我们自然觉得主动性的运营模式更好、更人性化，但是也要清楚，业务建模所依据的并不是方案的好坏，而是业务的真相。所以这时，我们需要做的事情就是跟业务方确认现实是何种情况，并通过模型忠实地反应出来。这就够了。

再说一句题外话。在这个例子里，当编辑和极客时间分别处在义务方时，他们各自采取的行动是不同的。编辑通常只有同意权，而极客时间不仅有同意权，还有不同意的权利。也就是说，在强管控的情况下，主编给编辑指派了任务，编辑是不能说“我不同意”的。

但是反过来，当编辑为自我设定目标时，主编是能说“这不够好”。所以同意权和不同意权是两个权利（世界上有一个人只有同意权，那就是英女王，也就是什么都只能同意，而且还不能辞职！所以，别看你大多数时间只有同意权，但是你可以辞职啊）。

言归正传，经过我侧面调查，极客时间还是很鼓励自主性的，因而目标设定的权利方是编辑，义务方是极客时间。那么我们可以对这个合同上下文建模：

在上节课里，我们大致描述了读者部分的内容，因为这部分功能你也比较熟悉了，我就不再做介绍了。

那么如果我们将这三个合同上下文放在一起，就可以看到凭证是如何在不同的合同上下文间，完成了业务的串联与整合。如下图所示：

在这个全视图中，我们可以看到：

编辑是如何帮助作者从选题、打磨内容，到签订合同，最后再到催稿上线的；

读者是如何通过订阅专栏，为作者带来分成的；

创作合同是如何保证作者一定会按时更新文章，以保证读者能阅读到课程内容的；

以及三方是如何围绕领域系统 CMS（也就是由专栏、文章组成的领域上下文）展开协同的。

至此，我们完成了极客时间专栏这个例子的终极形态，一套相对完整的业务模型。它为我们展示了，极客时间是通过哪些运营活动将一个 CMS 转化为可盈利的产品的（恭喜极客时间最近一轮融资）。这是我们如果只关注领域逻辑的话，永远也看不到的内容。

小结

通过这三节课，我们完整学习了 8X Flow 建模法。我们首先区分了什么是业务逻辑，什么是领域逻辑。从宏观上将两者分离，以保证业务系统与领域系统具有不同的知识边界和弹性边界。

然后，我们从合同履约入手，通过履约项和合同上下文建立业务模型。

今天，我们又介绍了合同之前的上下文，也就是渠道上下文。并解释了为何公司内的绩效管理也可以通过协议与履约的方式进行建模。

纵观 8X Flow 的建模方法，它的出发点是完全以业务为核心，构造可以支持业务的业务系统。并通过对业务模式的建模，将不同的领域逻辑复用到业务模式中。

归根结底一句话，不要把技术当大聪明。也就是不要从技术解决方案上去定义业务问题，要回到业务本身，去理解业务问题。

所以，架构也要以应对业务的变化点为根本出发点，将合同上下文、履约上下文和领域上下文作为系统天然的边界。从业务上下文中寻找变化点（角色化的履约确认、不同的渠道上下文等），并通过软件架构降低支持这些变化的成本。

总之，我们不要老是抱怨业务逻辑不易理解、变来变去的。事实上，业务逻辑是最简单，也是最理性的逻辑：多赚钱少花钱，规避法律风险，提供合规审计。要知道这套逻辑，业务方不光要跟你讲，也会跟投资人、股东讲，还会跟审计、法务讲，所以绝对是经过了千锤百炼，可以放心使用。

思考题

我们讲了系统中的变化点，那么通过这些变化点，我们可以怎样来建模中台系统？

很期待你能把自己对这节课的总结，对思考题的想法，或者任何疑问，分享在留言区，我在这里等你。我们下节课再见！

17｜中台建模（上）：到底什么是中台？

你好，我是徐昊。今天我们开始学习如何使用 8X Flow 建模中台系统。

在过去的几年里，有一种被称作中台的软件平台，在互联网公司中流行起来。而随着互联巨头们不停地将他们的成功与中台关联起来，越来越多的企业开始投资到中台的建设中来。

我曾见到过通过中台走向成功的企业，也同样遇到过在中台中反复挣扎的客户。那么到底什么是中台？在建模中台系统时，到底难在什么地方呢？这正是我们今天要讨论的内容。

中台到底服务了谁？

中台之所以是一个让人感到困惑的概念，主要是因为中台所服务的“前台”，并不是我们通常意义上理解的“前台”。一般当我们谈论起前台的时候，所描述的是一个软件系统。但是在大多数描述中台的上下文中，“前台”更多地指代一个具有自主性的团队，而不是承载这个团队业务的软件。

前台通常具有完备的研发与运营能力，而不同的前台则会关注不同的市场或业务。可能是在容量巨大的中国市场中，沿不同城市线级划分出的独立运营的片区；也可能是在同一城市内，不同领域下的不同产品。

比如，以某某出行为例，延不同城市划分的可能是北京的专车、上海的专车、苏州的专车这样的团队。而根据不同领域划分出来的则可能是北京的专车、北京的出租车、北京的顺风车等等。

显而易见，北京的专车和上海的专车是由不同的团队负责运营的。那么北京的专车团队在日常运营中所需要的软件系统，和上海专车团队在日常运营中所需要的软件系统会完全一样吗？可能主体的逻辑是相同的，但某些细节上可能存在差异，比如因地域原因需要集成不同的系统等等。

同样，北京的专车和北京的出租车也是由不同的团队负责运营的。那么北京的专车团队在日常运营中所需要的软件系统，和北京的出租车团队在日常运营中所需要的软件系统会完全一样吗？很可能主体的逻辑也是相同，毕竟都是出行请求和运载能力的撮合与匹配。但是某些重要组件上可能存在差异，比如对于运力的管理部分。

所以当我们明白前台指代的是这样不同的团队时，就会发现，这些团队对于软件有着高度相似但又不同的需求。他们要求高效重用，以满足业务快速扩张的需要（比如进入新的城市，或者扩展新的品类），但同时又需要高度自主自治（比如北京和苏州不同的业务环境，需要有足够的灵活性和自主性），以满足各自的需求。那么中台，就是能够满足前台对于复用和自主诉求的软件系统。

如果回到中台最开始的倡导者——阿里巴巴的“大中台小前台”战略，以及这个战略背后那令人心驰神往的 Supercell 之旅，我们或许能够更好地看明白中台是什么：一个用以支撑多个小而灵活的前台团队的软件平台。

小，首先指人员构成，无需沉重的研发能力；其次指试错成本，因小而低。

灵活，第一指对市场的响应速度；第二指流程敏捷，可以快速失败。

说句题外话，中台的首倡者，那位亿万异姓者们共同的父亲，并不是一个技术人员。怹（tān）不太可能想到用一种软件平台架构去服务不同的前台软件。更有可能的，怹看到的是一种服务于不同业务团队的业务运营模式。怹认为应对未来商业社会的一种可能的更具竞争力的模式，是通过小而灵活的前台团队，频繁而低成本地试错。而支撑这种团队结构的软件平台，就是中台。

以此为出发点，我们要去理解某一公司的中台，就需要从业务环境、组织结构、人力构成和技术架构这几个方面，去理解“高响应”“小而灵活”等概念在组织内的具体定义是什么。

而由于每个组织所处的外部竞争环境、内部组织环境、技术积累不同，每家中台的起点和目标也很可能完全不同。所以脱离这些环境去讨论中台，也就变得毫无意义起来。

所幸的是，所有这一切最终都要落脚回一个软件平台。因为再美好的构想如果无法通过软件平台去支撑，也就无法在大范围内去应用。因而我们可以从软件平台的特点出发，去寻找适用于中台的业务模式和团队结构。

效能与创新的平衡

不同于应用，软件平台是一类特殊的软件。它很少被最终用户直接使用，通常会被集成在应用之内，或是被多个应用共享。

比如操作系统就是一种最常见的软件平台。无论是台式机的 macOS、Windows，还是移动端的 iOS、Android，用户最终都不会直接去使用平台本身，而是通过不同的应用来利用平台提供的能力。

正是由于软件平台在构造之初就独立于各种“应用”，而在实际发挥作用的时候，又需要与应用整合。因此，软件平台从一开始就要平衡“效能的下限”和“创新的上限”，亦或者从更技术的角度上，我们可以称作“平台能力”（Platform Capability）和“应用自主度”（Application Automony）。

一方面，平台能力是平台提供的功能和机制。应用可直接整合某个功能，或提供某个机制去实现特定的能力。比如在手机平台上，应用可以直接打开摄像头去完成一系列的功能。也可以通过消息机制，完成不同种类的推送。这都是对平台能力的应用。

显然，平台能力越丰富，应用可以利用的也就越多，去完成某类功能的成本也就越低，因而平台能力通常被看作效能的下限。

但从另一方面来讲，平台能力又会限制应用去实现某种功能的方式。仍然回到刚才的例子里，如果应用希望实现完全不一样的通知方式，那么可能就是不被允许的，或许需要极其高昂的成本去绕过平台能力。可以说，应用利用平台能力获得的效能，是通过放弃一部分自主性来获取的，而低自主性，就会影响创新的可能。所以，应用自主度就被看作创新的上限。

说到底，软件平台需要根据平台用户对效能和创新的需求，来平衡平台能力和应用自主度。比如像操作系统、技术中间件，就是典型的高自主度平台，平台无法框定平台用户所需要的功能与场景，因而都尽可能地释放应用自主度作为平台的主要设计思路。

而数据库、搜索引擎则是高效能软件平台的代表，应用为了获得平台能力，甚至要使用特定的语言、协议与平台交互。不过高效能平台走到极致，就会出现可定制 SaaS 这种低代码环境。用户会为了获得大量的平台能力，放弃自主性。

触发了阿里巴巴中台思考的游戏引擎就是一种软件平台。游戏的核心玩法不同，因而平台需要相当的自主度。不过考虑到网络游戏的大场景，很多特定的模式又可以重复。可以说，它既不是一个纯粹的技术平台，也不完全是复用特定业务的平台。这种特性，也被后来的互联网公司制作的中台保留了。

互联网的中台

通过公开的资料与文档，我们颇能管窥阿里中台的面貌。阿里中台是基于一组共享服务完成构建的，这组共享服务包含类商品、品类、用户等模块。

在形成中台之前，这组共享服务已经被淘宝、天猫和 1688 共用。那么无论我们如何称呼这组共享服务——SOA 或者是微服务——它们实际上代表了阿里的基础业务能力。而重新组合这些基础能力，就可以支撑三个不同前台的业务的需求。

共享服务的形式给予了前台相当的自主度，而基于这组共享服务构造的中台，则为前台提供了更高的效能。

简单思考一下淘宝、天猫和 1688，这三个前台在业务上有很大的差异：个人卖家卖给个人消费者、商户卖给个人消费者、商户卖给其他商户。但其中也存在有类似的场景：订单履约、客户关系管理等等。很明显，业务的差异，使得这些场景中的具体流程并不完全相同，可是模式却十分类似。

那么我们可以说，模式的复用能够带来更高的效能，同时也能避免流程复用对前台自主度的影响，因而阿里中台通过将后台能力的复用转化为对场景中模式的复用，提高了平台效能。

类似的，另一个电商巨头京东也早早开始了中台的建设。在中台的设计上，同样是将核心业务模式中的业务能力从前台分离，并将这部分基础能力下沉到中台，并且提出了业务能力“积木化”的概念。那么通过对核心业务模式的抽象分离与下沉，京东的全域交易中台慢慢浮现出来。通过核心业务模式的复用，有效地支撑了线上线下、实物虚拟、2B2C2M 等各种形态的无界零售场景。

如果说阿里巴巴和京东都是典型的交易场景下对核心交易业务模式的复用的话，那么滴滴就是一个典型的出行场景下对出行业务模式的提取和复用的例子。

滴滴通过构建出行中台，将不同业务类型的出行业务，例如出租车、专车、快车等背后通用的出行业务模式，与具体业务进行了分离，并通过插件化、配置化等方式，实现了核心业务模式与前台具体业务的适配。

可以看到，通过构建出行中台，一个基于出行业务模式的新的业务模式诞生的速度被大大加快。从出租车业务到专车的业务上线，前后经历了 2 年的时间。但是从快车到顺风车的上线，只用了 1 个月的时间。业务模式的复用在业务快速扩展过程中展现出了极大的威力。

在上面的几个例子中，你可以看到大量 SaaS（Software as a Serivce）的影子，无论是淘宝、天猫和 1688，还是滴滴的出租车、专车、快车，是不是都可以看做某个 SaaS 平台的不同租户呢？从这个角度看，中台和 Saas 模式又有什么区别呢？

中台与 SaaS 的区别有这么两点。第一，SaaS 通常是从前端到后台的全部系统。

比如淘宝自己就是一个 SaaS 系统，我们在淘宝上开店的时候，享受的是从用户渠道、产品目录到库存管理等全部的组件和模块。而且我们还可以有自己的域名，让用户可以直接访问我们的店铺。所以从某种程度上来说，我们可以让我们的店铺看起来和平台没什么关系，作为完全独立的应用存在。

但是对于中台来说并不是这样的。我们还是以滴滴出行为例，当不同的前台团队从这个 SaaS 中开通自己的租户时，并不会出现新的出行 App，而是会在统一的主 App 中增加对应的功能，比如根据城市不同，将对应的前台团队（北京出租、北京专车等等）的服务汇总到 App 中。

那么当新的前台团队出现时，我们只是在后端增加了系统与服务，以支持新团队的运营需求，并不会新增一个完全不同的渠道与入口。如下图所示：

但如果将中台看做 SaaS 的话，那么这个 SaaS 只有后端部分。所以严格意义来说，中台的模式更接近“后端即服务”（BaaS，Backend as a Service），而不是 SaaS。

这也很好地解释了中台的“中”是怎么回事。它既没有扩展前端，也不仅仅是简单套用后台系统，而是在已经存在的前端和后台之间，构成一套软件，以支撑前台团队的运营模式。那么支持这种操作的平台，自然应该叫中台。

第二，中台与 SaaS 的定制化程度不同。还是拿滴滴出行来说，虽然从模式上看，无论是出租车、网约车还是专车，它们的模式都是需求跟资源的匹配。也就是乘客提出出行需求，而平台呢，根据一定的规则，从可用的资源中寻找最佳资源匹配给乘客。但是具体到运营上，差别还是非常大的。

比如派单还是抢单？积分还是考评？在不同的产品形态中，由于对资源的控制力不同，运营方式与方法也就千差万别了。就算我们硬要说中台是 SaaS 的话，那么它可能更接近一个产生 SaaS 的 SaaS，而不是直接服务于租户的 SaaS。

所以如果从这个角度理解中台的话，那么其实不是前台团队直接使用了中台，而是使用了中台的产物：针对某种业态后台系统的 SaaS。如下图所示：

可以看到，虽然中台与 SaaS 模式存在差异，但是我们仍能感受到它们在模式上的相似：也就是对于某种能力的复制。

我们在第 14 讲提到过，SaaS 模式是典型的复制运营模式（Replication），那么中台到底在复制什么呢？这歌时候，我们就需要看一下中台里，最适合的效能与自主性的平衡点到底在哪里。

宏流程——场景下的业务模式

如前所述，阿里、京东和滴滴中台都将效能与自主性的平衡点放置在“特定场景中的业务模式”，在保障了效能的基础上，给予前台更大的灵活性。而这种灵活性与效能的平衡，就造就了我们梦寐以求的互联网创新速度的奇迹。于是，我们称这种“特定场景中的业务模式”为宏流程。

宏流程兼具业务与技术上的含义。从业务上讲，顾名思义，宏流程是一种“宏观的流程”，它体现的不是某一个前台业务具体的业务流程，而是通过把多个类似业务模式的前台抽象过后，所提炼出的共有的底层“核心业务模式”。例如阿里巴巴、京东交易中台承载的交易业务模型，以及滴滴出行中台承载的出行业务模型。

从技术上讲，宏流程不能被前台直接使用，往往需要通过基于具体的前台业务进行配置点配置与扩展点定义之后，才能实例化，成为某个前台具体的业务流程。并且这个实例化的过程非常快速，甚至可以由前台业务人员直接通过配置台独立完成。这是宏流程技术性的一面，它是前台业务流程的模板——宏的另一个广为人知的含义。而宏流程实例化的过程，也就是我们前面讲的 SaaS 化 SaaS 的过程。

宏流程促进了从业务层面上模式的复用，也给创新带来了不同的思考。那就是如何复用已经存在并成功的业务模式，是一种更有效的创新方法。虽然这种创新方式有个不好听的名字，叫“copycat 创新”，也被戏称为“Uber for XXX”，即：将广为人知的 Uber 模式应用到不同的细分市场上去，使之成为一种新的服务。但如果被复用的模式本身就存在于企业内，那么就另当别论了。

说到底，不停地将企业成功的核心模式灌注到新的市场，让企业在扩展业务版图的同时，也能不断强化核心竞争力。这是绝大多数企业长久以来期盼的发展创新的模式。注意，不是重复，而是在不同的竞争领域复现。这是宏流程带来的思考方式的转变，也是承载宏流程的软件平台——中台让我们着迷的地方。

小结

从某种意义上来说，中台已经走过了一个技术大词（buzzword）的全生命周期：从几年前的甚嚣尘上，一众企业寄希望于通过构建中台，追赶互联网研发模式。到最近拆中台成风，好像中台马上就要被收进历史的垃圾桶了。

然而也是历史告诉我们，当我们被技术大词狂热鼓舞的时候，心里得清楚，真正希望了解这个技术的人并不多，要么应激怕掉队，要么投机想上位。只有浪潮退去，我们才能真正静下心来看看它是什么，以及怎么做。

我个人认为中台是一种源自中国市场的、特有的、基于云平台的平台架构模式，并且它的出发点完全不是技术基建，而是寻找更好的组织结构和技术架构，以支持业务快速增长和发展。

所以哪怕一时受挫，但其中闪闪发光的思考，也会不断拓展我们的认知和视野。所以哪怕今天拆中台成风，但我们还是需要学习怎么才能构造真正的中台。它肯定还是要回来的，毕竟还是很有价值的技术！

思考题

根据你所学到的建模方法，你会怎样建模宏流程？

很期待你能把自己的思考和想法分享在留言区，我会和你交流。我们下节课再见！

18｜中台建模（下）：如何寻找可复用的业务模式？

你好，我是徐昊。今天我们继续学习如何使用 8X Flow 建模中台系统。

在上节课中，我们围绕中台的诸多不同思潮，分析了中台与平台的差异，中台模式与 SaaS 模式的区别。此外，还介绍了平台能力与应用自主度的概念。通过这两个概念，可以帮助我们理解中台在平台能力与应用自主度中取舍的平衡点，就在于“特定场景中的业务模式”，也就是宏流程。

那么作为一门建模课，我们接下来的问题自然就会变成：如何建模宏流程？如何根据得到的宏流程形成完整的中台策略？这也正是我们今天要讨论的问题。

业务模式建模难在哪儿？

通过上节课的学习我们已经知道，宏流程是一种宏观的抽象的流程，需要通过配置与实例化才能变成前台团队需要的具体业务流程。所以宏流程不是流程，宏流程是业务流程的模版。或者说，宏流程是一种可以在不同业务场景下复现的业务模式。

对于业务模式的建模难点主要在于寻找合理的抽象程度。因为业务模式中既有泛化的概念，也有具体的实体。如果泛化概念不够，那么业务模式就会退化为具体的业务功能了；但如果泛化概念太多，则容易引起过度抽象，丧失业务模式的价值。

仍然以出行模式为例，我们可以说出行模式的核心是一种撮合模式，也就是由需求方发起请求，然后从资源池中寻找最为匹配的资源与之对应。但是这种模式又不是泛泛地撮合，而是跟需求方发起需求的时间与位置息息相关的。如下图所示：

在这个业务模式里面既有泛化的概念，比如资源。也有具体的实体，比如位置。当然，我们还可以在这个基础上进一步抽象，比如把位置变成资源筛选条件。那么不光是出行了，股票、婚恋、期货都可以被装入到这个业务模式中。但这就有过度抽象的倾向了。

较高的抽象层次固然可以帮助我们理解问题，但却无法支撑具体的决策。要知道，出行模式之所以成立，恰恰在于它并不是一个泛泛的撮合模式。而在于在出行场景下，它能够对具体业务予以决策上的支持。

比如当乘客发出用车请求时，A 和 B 两辆车都处在资源池中，这个时候，我们就需要给出 A 和 B 当中哪一个更为匹配的决策。再比如说，如果匹配到了 A，但是 A 未在规定时间内到达乘客所在地点，要怎么处理；或者到达了，乘客不见了，要怎么办？

业务模式不仅需要用来帮助我们理解业务是怎么回事，而且需要在业务场景下，帮助我们做出业务决策。因而我们可以讲，业务模式实际上是一组决策模式的集合。业务模式需要一种恰如其分的抽象：适度地泛化可以在不同的场景下复用，但又要避免在具体的场景下丧失它对于业务决策的指导意义。

对于业务模式，最好的办法是从已经存在的业务模型中去提取，而不是直接建模。已经存在的业务模型，可以帮助我们在业务场景下决策，这也是我们业务建模的出发点。那么我们只需要对已经存在的模型进行泛化，就可以从业务模型中提取到业务模式了。

说句题外话，在软件开发里需要恰当抽象的场景中，抽取、重构这种后置行为几乎都是更好的选择。原因就在于并不是我们不会抽象，而是很容易过度抽象。

在抽象过程中，经常会出现一种接近终极真理的幻觉。只要我们愿意，我们永远可以将繁复的细节抽象成极简的模型。比如一切都是对象，一切都是阴阳互化，一切都是空，等等。

然而事实是，这些高度抽象的模型会丧失对细节的指导意义。所以我们始终要戴着脚镣跳舞，时刻提醒自己：模型还具有指导意义吗？开始抽象容易，但停下来难。或者更格言式的表达：谁都可以抽象模型，但只有智者知道在哪里停下来。而通向智者之路，就是去掌握抽取、重构这些技巧。

从合同上下文中提取宏流程

8X Flow 中的合同上下文，非常适合用来提取宏流程。原因有二：

合同履约本身就隐含了双方业务往来的流程；

合同上下文中的角色是变化点，而引入变化点，就是对业务流程的泛化。

首先来看一下 8X Flow 合同上下文的履约中隐含的业务流程。以出行的合同为例，根据我们日常经验，其中权责履约项目包含：取消、接人、送达、支付。与这个合同对应的投标邀请和投标，就是出行请求和承运投标。对应的合同上下文如下图所示：

在这个合同上下文中，有两个主要的流程。第一个是乘客发出出行请求，通过“出行说媒的”这个撮合引擎的拟人化角色，匹配到对应的承运人，最后形成出行合同的过程。

从业务功能角度来说，这就是叫车的功能流程；从合同的角度来说，是签订出行合同的流程。

第二个就是出行合同签订之后的履约流程。承运人到指定地点接人，然后送达目的地，最后乘客支付费用。

从业务功能来讲，这就是乘车的功能流程。而从合同的角度来看，这是合同的履约流程。

重要的业务流程，其背后都有对应的合同流程，这并不是偶然的。合同流程是因，业务流程是果。我们知道，所有业务活动都需要在合同（具有法律效力的口头约定）的框架下进行，这是法律法规的要求。合同签订的过程是合同双方对合同内容讨价还价的过程，合同内容实际上是合同双方为完成履约所需要进行的业务活动的框架。

企业需要根据合同流程设计自己的业务流程，以完成对应的合同流程。因而从合同出发寻找到的合同流程，实际上是业务流程的目标与模板。而业务流程，是对合同流程的实现与映射。

由于合同流程是业务流程的模板，使得合同流程本身就具有一定的宏流程特点。比如，在出行合同上下文中，规定了履约项支付车费。但是由于这只是在合同流程中的履约项，那么映射到业务流程中，就可以有不同的表现形式了。

像先叫车到达目的地，之后再支付所有费用，或者是在出发之前通过算法形成一口价，提前完成费用的支付与抵扣，都是对于这个合同权益项的履约。那么在合同流程上，这两者没有什么区别。但在业务流程上，就可能存在差异。

所以要建模宏流程，合同流程就是非常好的候选和出发点。从合同流程出发，我们寻找到一个合理的流程框架，可以在不同的业务上下文中展开或映射成不同的业务流程。

那么接下来我们就需要对合同流程进行必要的泛化，也就是通过角色和角色的扮演，将变化点引入到合同上下文中来。

对于已经了解了 8X Flow 的你来说，这一部分应该不会陌生。比如我们可以将出行合同上下文中的支付确认变成角色对象，也就是接受从不同的合同上下文产生的凭证，作为履约证据。如下图所示：

然后合同上下文中的变化点与扩展点，并不仅仅是履约的确认凭证。任何角色对象，都可以看做是业务上的变化点与扩展点。但是扮演关系不一定来自另外的合同上下文，还可能来自于领域系统。比如，在这个例子中拟人化的撮合算法出行拉媒的和承运人。

我们都知道，在中台化的出行模式中，不同业务场景下的承运人，实际上就是被不同的前台团队管理的不同运力资源。那么对于网约车来说，承运人就是网约车司机。对于出租车，就是出租车司机。对于共享单车，就是附近可用的共享单车。

同样，对于拟人化的撮合算法出行拉媒的，在不同的业务场景下，就表示不同的算法，或者同一算法的不同配置策略。因而我们可以将运力和算法建模为领域系统，然后通过领域系统来扮演这两个角色。如下图所示：

通过这张图就可以发现，在不同的业务场景下，我们对合同上下文中的变化点做了不同的配置与扩展：

在网约车的业务场景下，就是出行合同上下文配合网约车领域系统，支持了网约车的业务；

在出租车的业务场景下，就是出行合同上下文配合出租车领域系统，支持了出租车的业务。

于是我们通过配置，将同样的合同流程，具体展开成了不同的业务流程，同时也就达到了业务层面上模式复用的目的。

进行到这一步，模型就展现出了中台的雏形。当然，如果在这里停下来，也是可以接受的。毕竟我们已经找到了可重用的业务模式，以及重用这个业务模式的方式。但如果继续观察的话，我们就会发现在不同的业务场景下，履约确认可能也会有所不同。

比如送达确认，对于网约车而言，行程需要网约车司机发起行程结束的请求，然后再与行程目的地进行比较，以确认行程到达。对于共享单车而言，需要乘客发起还车请求，然后确认锁车，即可表示行程到达。

于是我们可以将履约确认项进一步角色化，然后再去追溯在不同的业务场景中，还有哪些凭证与合同上下文，可以实现确认履约。

因为后续的做法与 8X Flow 中提取角色的做法（具体参见第 15 讲）大同小异，我们就不再赘述了。如下示意图，表示我们假设的不同上下文与出行合同上下文的组合：

于是我们就从 8X Flow 的合同上下文中提取了宏流程，也就是可以在不同业务场景下复用的业务模式。不光如此，对于业务模式在不同的业务场景下，能进行什么样的扩展与订制，我们也通过扩展点进行了建模。最终我们得到的就是中台业务模型。

领域系统？业务系统？

在结束今天的课程之前，再来看看刚才建模过程中我们对领域系统的使用。我们简单地将不同业务场景下的实体与算法扔入一个领域上下文，于是得到了网约车、共享单车等领域系统。

那么真实的情况是这样的吗？可能并不是。比如对于网约车的管理，背后肯定有网约车司机与出行公司之间的合同，运营网约车的前台团队内部肯定也有自己的绩效约定。那么我们为什么不去深挖这些合同呢？从这些合同中进一步寻找凭证来完成角色的扮演呢？

让我们再想一想领域系统与业务系统的差异（具体参见第 14 讲）。针对业务系统，我们使用合同上下文建模，最终得到是业务模式或运营模式。而针对领域系统，我们使用对象建模，最终得到的是一组功能。

所以当我们希望重用业务模式的时候，会将其中的业务部分与领域部分分离，进行业务建模。而如果我们暂时不关心业务模式的复用，那么就可以从功能的角度，将它看成领域系统。

在刚才的例子中，我们关心且希望复用的，是出行合同上下文中所蕴含的撮合模式，以及这个模式背后的履约流程。在这种情况下，运力部分的业务模式和内部的运营模式就不是我们的关注点了，因而可以将它们简化为一个领域系统来看待。

注意，我们永远可以从领域系统中分析并分离背后的运营模式，问题是有没有这个必要。

小结

这节课我们主要介绍了如何从 8X Flow 的合同上下文中提取宏流程，并通过提取出的宏流程构建中台的业务模型。

总结起来，大致流程是这样的：

通过 8X Flow 进行业务建模；

确定需要在不同业务场景中复用的业务模式；

寻找到业务模式所对应的 8X Flow 合同上下文；

通过将履约项角色化引入变化点；

在不同的业务场景下扩展替换变化点，并将它们分别放置于不同上下文中；

在不同上下文中继续 8X Flow 建模，直到可以解释清楚不同的业务上下文是如何完成对宏流程的扩展。

在得到中台的业务模型之后，我们就可以开始着手于中台的实现了。事实上，中台除了在业务建模具有挑战之外，在技术实现上也颇具难度，因而我们需要大量使用微服务和 SaaS 化架构。这也正是我们接下来两节课要讲的内容。

思考题

在进行合同上下文泛化的时候，我们是对履约确认项进行的角色化处理，那么为什么不能对履约请求项进行角色化处理呢？请注意，我明确告诉你不能这么做，所以请思考不能这么做的理由。

很期待你能把自己的思考和想法分享在留言区，我会和你交流。我们下节课再见！

19｜如何将模型实现为微服务？

你好，我是徐昊。今天我们来聊聊如何将 8X Flow 模型映射为微服务。

经过 14-16 节的学习，我们已经可以使用合同上下文、履约上下文和领域上下文对业务进行建模了。那么当我们希望在云平台上将业务模型实现为软件系统时，就需要将合同上下文、履约上下文和领域上下文作为系统天然的边界，并将它们放置到不同的弹性边界中，从而在满足业务扩展与需求变化的前提下，尽可能地降低成本。

有了弹性边界，我们自然很容易地想到可以将不同弹性边界内的内容，实现成微服务架构风格（Microservices）。那么今天我们就来讲一讲，如何将 8X Flow 的模型实现为微服务架构风格。

微服务还是伪微服务？

在开始讨论这个问题之前，我们首先要重申一下什么是微服务。关于微服务，现在有诸多迷思，以及很多看起来像是微服务，但实则南辕北辙的伪微服务风格。

在 James Lewis 和 Martin Fowler 的名作《微服务》中，将微服务定义为一种架构风格，并总结了它的九种特质：

通过服务实现组件化；

服务按照业务能力划分组织；

服务以产品而不是项目研发；

逻辑集中在服务中，编排简单；

每个服务自主决策（技术栈、语言等等）；

每个服务自主管理数据（不强制使用统一数据源）；

基础设施自动化；

将服务失败当作常态纳入设计考量；

演进式设计（不求一步到位）。

具体的内容可以参考 James 和 Martin 的文章，其中有三个特别关键的点，我需要专门提出来说一说。正是对于这三点理解的偏差，从而形成了三种典型的伪微服务风格。

第一点是服务按照业务能力划分组织。这个架构特性说的是，微服务中的服务应该以业务能力（Business Capability）为粒度。其实这也间接回答了“微服务到底多微合适”这个问题：既不是单纯的技术能力（比如查询、获取系统时间），也不是完整的应用（Application），而是用以支撑构建应用的业务能力。

所以微服务的“微”是个相对概念，而不是绝对概念。也就是说，与单体应用（Monolithic Application）相比，微服务是微小的。这就好比说，对比银河系，太阳连尘埃都不如。但是太阳就很小吗？并不是吧。

说句题外话。在我看来，软件行业中存在一个神秘的“恰当粒度”（Appropriate Size）。比如很多年以前，当我们使用用例（Use Case）捕捉用户需求的时候，出现过这样的争论：到底用什么样的粒度去捕捉用例最合适？而转换到用户故事（User Story）之后，它直接把“粒度恰当”（Size Appropriated）当作特性之一了。技术上也是一样，但凡出现分布式架构，每一个分布式组件多大，永远是讨论的焦点。SOA（Service Oriented Architecture）基本上就是被这个争论给搅黄的。现在热门话题不又是“微服务到底应该多微”吗？

对于用例的恰当粒度，Alistair Cockburn 在《编写有效用例》（Writing Effective Use Case）一书中给出了最佳实践。他将业务和技术比喻为天空与大海。天空中有云彩（宏观目标）、风筝（目标），大海里有鱼（功能点）和扇贝（实现细节）。而恰当的粒度恰好在天空与大海的交汇处：用户目标。它既有业务上的含义，也有技术上的支撑。

到了用户故事就更明显了，它必须包含用户价值与实现价值的行为，也就是说，用户故事必须在业务和实现上都具有指导意义才行。而对于 SOA，最佳实践是能力建模（Capability Modeling），以业务能力为粒度进行 SOA 的服务构建。从某种意义上说，微服务这个词被发明出来，本意就是为了将使用能力建模的 SOA（当然还有利用 RESTful API 而不是复杂的 WS-*）与其他 SOA 风格分离。

你会发现，通常所说的“恰当粒度”是在业务与实现两个维度上平衡的结果。并不会存在“只从单一维度入手，越怎么样就越好”这么简单粗暴的结论。所以微服务并不是越小越好，当小到不能表示业务能力，就不再是微服务了。

如果不顾及服务是否按照业务能力划分组织，就是一种典型的伪微服务模式，我称之为分布式服务（Distributed Services）。要注意，分布式服务并不是反模式，它有其特有的用处，只不过它并不是微服务而已。也可以这么理解，微服务算是分布式服务的一种特例。与其他分布式服务相比，微服务特有的优势，大半都源自按照业务能力划分组织服务。

对于微服务，我要说的第二点是，以产品而不是项目研发。产品和项目的差异体现在团队结构和生命周期上。

从团队结构上来讲，产品团队包含产品研发和产品运营。在产品生命周期内，产品团队负责产品的一切。而项目团队的职能通常会更简单一些，不包含运营和运维部分。快速说一句题外话，DevOps 是需要产品团队才能实现真正的研发自运维。

从生命周期上讲，产品的生命周期分为初始、稳定、支持和结束生命这么几个阶段。那么产品的不同版本，可能处在不同的生命周期中。比如我们熟知的 Windows 操作系统，在 Windows 8 发布之后，Windows XP 依旧有官方的延期支持。

所以产品团队也可能需要在同一时间内，支持多个处在不同生命周期的产品版本。比如 1.0 是支持阶段，1.5 是稳定阶段，2.0 是初始阶段，等等。而项目通常假设只有唯一的产物，随着项目生命周期的进展，项目化服务一直在改变。

那么产品化服务的生命周期，实际上相当于承诺在产品生命周期内，服务是不变的。也就是说，只要 1.0 不结束生命，那么我们就可以一直使用它。哪怕你发布了 1.5、2.0、3.0，只要 1.0 能满足我的需要，并且还在生命周期内，那么作为消费者，我可以无视你的后续版本。那么对于产品化服务的耦合，在产品的生命周期内可以看作是稳定的，并且可由消费方选择是否升级。

但项目化服务通常做不到这一点。项目化服务通常都只有唯一的产品，当服务出现新版本后，之前的版本就默认消失了，对应的消费者也都会受到波及。这就带来了服务间紧密的耦合和修改的传播。

因而微服务需要服务间不仅在接口上松耦合，还要在生命周期上松耦合。也就是微服务可以自主发布，其他服务不应受到影响。产品化是实现这一点的根本途径。如果服务缺乏产品化生命周期，那就是另一个典型的伪微服务模式，我称之为微工作组（Microstreams）。这个模式会产生一组在生命周期上紧密耦合的服务，从而完全丧失微服务的意义。而且随着服务数量变多，这种生命周期的耦合还会带来难以承受的沟通成本。

说句题外话，判断是不是微工作组，有两个特别简单的办法：

你可以去看看每个服务团队之间的会议有多少。真正的微服务团队，很少与其他团队沟通（都在契约里）；

你可以去听听团队里会有多少次用到“集成”或者“整合（integration）”这个词。用得越多，就越是微工作组。

对于微服务，我要说的最后一点是，逻辑集中在服务中，编排简单。逻辑越在服务中集中，所需要的编排就越简单，通常通过 RESTful API 或者轻量的消息机制即可完成。而如果服务中的逻辑简单，那么就会有大量的逻辑泄露到编排逻辑中去，此时就需要使用复杂的编排工具辅助我们的工作，比如企业服务总线（Enterprise Service Bus，ESB）。

选择编排复杂的逻辑，听起来很有道理：既然我们希望在不同场景下复用服务，那么总有一些需要改变的订制代码，我们需要将它们与服务本身分离。分离之后，就能通过编排引擎，帮助我们在不同的场景下重用这些服务了。

然而你会发现，按照这个逻辑走下去，服务往往会变成对于数据的 CRUD，然后大量的逻辑存在于编排引擎中，这也是典型的伪微服务模式，我称之为傻服务（Dumb Service）。当服务没有封装业务能力，而架构师又对复用充满雄心的时候，经常会出现傻服务。这不光服务傻，人也不太机灵。

除了上面三种典型的伪微服务之外，最近还流行起了一种伪微服务架构风格，被称作分布式单体（Distributed Monolith）。正如微服务本身一样，我们很难给出分布式单体的确切定义，但是可以描述它的特质。

分布式单体也是通过一组分布式服务构建的应用，但是却具有单体应用的特质：修改其中某个服务，会引发修改很多其他服务；无法有效地水平扩展。

微服务的核心思路就在于将业务能力封装成独立的松耦合的服务。通过这样一组服务，构建企业内的能力生态系统。除了能满足当前应用的需要之外，也为未来可能的新应用提供了坚实的基础。

而分布式单体虽然是分布的，但在逻辑上仍然是一个单体应用。这一组分布式服务的首要目的是高效地服务于所构建的应用。这种观念上的差别（虽然可能不是主观故意的），也造成了我们对技术的不同评价和偏好。比如“用 RPC 还是 RESTful API 来设计服务的 API”，就能分辨出你是不是真的在做微服务。

使用 RESTful API 描述微服务 API

通过前面的讲解，我们可以发现微服务架构风格与分布式服务、分布式单体架构的关键区别在于三点：

服务封装业务能力，以服务构成企业内生态；

以产品的角度管理版本和集成策略；

服务富含逻辑，编排简单。

其实理解了这三点关键差异，很多关于微服务的问题自然也就迎刃而解了。什么样的粒度是合适的？服务之间存在耦合怎么办？我需要多复杂的编排工具？前台的逻辑传递到服务要怎么处理？这些问题只要变成：微服务会怎么做？分布式服务会怎么做？分布式单体会怎么做？然后互加比较，自然就知道应该选择什么方案了。

接下来的问题更偏技术性一些，对于每个服务我们要如何设计它的 API？虽然有一些新的思潮涌现，但我个人认为，以目前而言，分布式超媒体仍然是微服务 API 的最佳方式。也就是说，RESTful API 仍然是微服务 API 的最佳选择。主要有这么三点原因。

首先，我们需要将微服务构成的企业内生态看做一个整体。套用我前同事 Ian Robinson 的名言 _Be the web , not behind the web_（成为互联网，而不是躲在互联网之后）。也就是说，我们应该将企业能力构造成一个 web，而不是仅仅将 web 作为对外暴露服务的接口。

那么怎么才能描述这构成企业内生态的一组服务呢？自然是分布式超媒体。也就是通过超链接表示服务与服务之间的关系，指导客户端完成它对其他服务的调用。比如我们可以通过这样的超媒体描述，来表示完成支付的不同方式：

<article>
    <title>订单详情</title>
    <p>....</p>
    <link href="http://payment.com/12358921" rel="online-payment"/>
    <link href="http://enterprise.com/12358921" rel="enterprise-payment"/>
client"/>
</article>

在这段超媒体描述中，我们指明了可以用于完成订单支付的两种方式，一种是在线直接支付，一种是使用企业支付。

这里我们会发现一个有意思的情况，就是如果以分布式超媒体方式去描述服务间的关联，那么其实我们不太在意它到底是单体应用还是微服务结构。所以一旦把眼光放到，通过服务构成企业内生态，并使用分布式超媒体去描述这个类似于 web 的生态系统时，会发现微服务不过是实现上的细枝末节。

当我们明确了自己真正要解决的问题，那么就需要让服务可以融入到分布式超媒体中。所以 RESTful API 仍是目前将服务融入分布式超媒体的最佳方式。

第二，URI 在表示服务编排的时候，仍然可以保持模型之间的关系。除了通过超链接，将关联的服务与资源联系在一起之外，URI 也可以用于表示服务之间的编排。这是因为符合 RESTful 风格的资源 URI 可以看做是函数调用。比如，如下的 URI 和代码是等价的：

GET /users/1/subscriptions

(subscriptions (users 1)) //LISP 函数风格

getSubscriptions(getUsers(1)) // Java

我们可以将这个 URI 看做是对 Users 和 Subscriptions 这两个服务的编排。那么按照对照函数式编程，不同的求值策略（比如惰性求值）也就变成了对于服务不同的编排策略。如下图所示：

对于同样的 URI，在不同的场景下，最合适的编排策略可能并不相同。但是 URI 帮我们封装了这些细节。

也就是说，我们通过/users/{uid}/subscriptions引用了 Users 和 Subscriptions 服务的编排。无论编排方式怎么改变，只要 URI 仍是/users/{uid}/subscriptions，那么对于消费方来说，就没有影响。而妙就妙在/users/{uid}/subscriptions是从模型而来的，因而是稳定的。这一点是其他 RPC 风格微服务 API 所不具备的。

说句题外话，URI 可以看作求值，是我在我司技术委员会上做的一个闪电演讲的主题。当时听众里有 Martin Fowler、Neal Ford、Scott Shaw、Rebecca Parsons 等人，他们都是函数式编程的狂热信徒，于是我就讲了函数化 URI、Continuation 与服务编排等话题。Rebecca 和 Martin 说这个话题在我所有的闪电演讲里可以排进前三（截止到 2017 年）。有兴趣的同学可自行推导，反正讲之前我只想了 20 分钟。

第三，RESTful API 也可以表示异步。这是一个对于 RESTful API 常见的误解，就是 RESTful API 都是同步的。那么如果需要异步调用服务，就无法使用 RESTful API 了。

然而在 HTTP 的状态码中，有一个专门为异步设计的 202，表示请求已接受，然后我们只需要在 header 中指明对应的 URI，或者设计成特定的返回结构，就能支撑异步结构了。

比如，你想创建一个新的用户，那么 API 可以返回 202 表示请求已接受，然后返回对应的创建任务，供你查询创建进度：

POST /users

HTTP Status 202

{
  "task": {
    "href" : "/user-tasks/12345"
  }
}

因为之前讲过的原因，我一般会使用分布式超媒体来描述整个服务生态，而将异步机制放置在分布式超媒体之后。也就是说，哪怕服务间完全以消息机制通讯，在 API 层面上，也仍然是以资源为主体的 RESTful API。如下图所示：

综合来看，如果 RESTful API 设计得合理，那么微服务只是在实现层面给予了更好的弹性调配空间；而如果 API 设计得不合理，那么就算把微服务玩出花也没什么用。我的建议一直都是：比起微服务，我们更应该关注如何通过分布式超媒体表示由服务构成的生态，更应该关注如何 Be the web。

将 8X Flow 模型实现为微服务

当我们捋顺了思路，那么将 8X Flow 得到的模型转化为微服务，就是水到渠成的事情了。让我们再看一下第 16 讲中得到的模型：

首先，先不管微服务不微服务，以不同的合同上下文和领域上下文为 URI 的根，将其中模型映射为 RESTful API（方法与第 10 讲、第 11 讲相同），并构造分布式超媒体表示这一组服务构成的生态系统。

接下来就要拆分不同的服务了。通过 8X Flow 建模，业务能力有三种表现模式，分别是：合同上下文表示的业务模式，履约上下文表示的业务能力，以及合同前上下文表示的合同签订流程。

理论上讲，每个不同履约项都可能具有不同的弹性边界，而以履约项为单位拆分成细小的服务，仍然可以表示业务能力。然而我会建议你，从合同上下文开始，先将不同的合同上下文作为服务边界，然后再看看是否需要将履约项拆分成独立的服务。

由于合同上下文与履约项通常会处在相同的 URI 根上，我们可以按照通过 URI 编排服务的方式，对它们进行编排。示意图如下：

需要再次强调，你并不需要把每一个履约项拆分成独立的服务，这样不一定有什么好处。只有在履约项真的需要独立的弹性边界时，才需要这么做。至于其他两种业务上下文，比照合同上下文处理即可。

另外，对于每个领域上下文，我建议做成一个服务就行了。毕竟在面向对象设计中，弹性边界不是很容易切分。但是只要使用 RESTful API，无论服务如何拆分，接口都是稳定的。

小结

今天我们先讨论了什么是微服务，什么是伪微服务。然后讨论了如何使用 RESTful API 通过分布式超媒体来描述由微服务构成的企业内生态。最后描述了怎么将 8X Flow 的模型转化为微服务。

你可能会发现，这篇有点文不对题，通篇 5000 多字，只有最后 500 字讲了怎么从 8X Flow 映射到微服务。

首先，在我看来，行业内做伪微服务的人多，而做真微服务的人少。很多问题不值得去解决，因为没有将问题定义清楚。而一旦明白什么是真微服务，大多问题都变得不言自明。

其次，如果对比微服务和如何使用 RESTful API 构建企业内生态，显然后者重要得多。微服务可能仅仅是我们通向后者的一种方式而已，我们需要更关注后者。

一旦明确了，我们要实现的是真的微服务，并且我们的目的是为了使用 RESTful API，并通过分布式超媒体来描述企业内生态。那么“正确的微服务”的做法也就变得简单直接了。所以我在这门课快要结束的时候，给你一个例子，看看怎么样通过定义问题，来简化解决方案。

思考题

请你思考一下，除了组织结构之外，产品化的服务会给我们带来哪些挑战？

很期待你能把自己的思考和想法分享在留言区，我会和你交流。我们下节课再见！

20｜云时代的下一站：SaaS化与魔球建模法

你好，我是徐昊。今天我们来聊聊 SaaS（Software as a Service）化。

按理说，SaaS 化与业务建模无关，本不应该出现在这门课程中。但是，SaaS 化却与云平台、微服务有着千丝万缕的联系。而且，我是通过建立一个模型解决了 SaaS 化服务设计的问题，所以勉强算是沾边吧。

那么今天就来讲一讲为什么我们需要在现在关注 SaaS 化，以及如何通过服务设计，对已经存在的产品进行 SaaS 化。

微服务真的能帮助创新吗？

很多企业上微服务，都是听信了这样的愿景：微服务可以促进企业内创新，未来只需要编排存在的服务，就能满足一堆创新的需求。微服务的确可以实现这个愿景，但是有个实现的前提：微服务需要以开放服务的形式，被其他应用或服务调用。

所谓开放服务，即：这个服务不是为某个特定应用或服务构建的，而是面向生态中所有可能的客户端构建的。

微服务并不是服务化的应用后台，它封装了企业的业务能力。任何需要用到这个业务能力的应用，都会对这个服务进行调用。因而微服务与应用后台在概念上就存在巨大的差异。应用后台仅仅服务于应用前台，只要满足前台的需求就可以了。而微服务不仅需要服务于当前已知的应用，还要服务于未来可能需要用到对应业务能力的应用。

除此之外，我需要特别强调，开放还指的是，并不是专门为生产环境中的应用或服务构建的，而是面向所有环境内的应用或服务构建的。也就是说，这个微服务是否能够允许，围绕它展开试验和创新，同时还不影响生产环境下的正常使用。

说句题外话，我见过很多过度拟合的微服务（Overfitting Microservices）。过度拟合这个概念是我从机器学习那里借来的，表示那些没有封装业务能力，过度考虑某些应用场景的服务。这也算是伪微服务的一种形态。

再说一句题外话，我同事 Zhamak Dehghani（Data Mesh 概念的初创者）曾经在我们某次酒吧扯淡中，描述过一种微服务的机缘凑巧（Serendipity in Microservices）。意思是在微服务完全成为开放式服务生态之后，你无法预知会有什么样的创新出现。因而在机缘巧合之下，会有很多意外之喜。我完全同意这样美好的愿望，而我唯一的顾虑在于，serendipity 本义一定指的是好事，但在开放式服务生态中，却没法保证发生的一定是好事。

言归正传。总之，我们需要将微服务构造成开放式服务，那么有两种策略可选：完全开放式服务和 SaaS 化的开放服务。

我们先来看完全开放式服务。也就是对于某个版本的服务，在企业内生态中存在一个唯一的、开放的、可供任何人访问的实例。这时候，这个服务就像是微博、搜索引擎等互联网开放服务一样。只要具有对应的访问权限（通常是注册用户），那么就可以毫无顾忌地对这个服务进行访问。

对于只读性服务，这种策略问题不大。比如提供企业内所有员工公开信息的服务，再比如提供所有产品信息的产品目录服务等。在这种情况下，只需要保证足够的弹性，以应对试验与创新带来的流量即可。

而对于会留下业务痕迹的业务系统，这种完全开放式服务的策略可能就不是那么适用了。来自于试验和创新的流量会在生产系统中留下痕迹，也可能会影响生产环境的正常使用。那么这时候，采用 SaaS 化的开放服务可能就是更好的选择。

这时候被 SaaS 化的，就是这个服务的沙箱环境（Sandbox Environment）。也就是说，在任何非生产环境的请求，都会在沙箱环境中获得独立的、与生产环境隔离的专有环境。如图所示：

如果存在需要借助这个服务进行某种实验或创新的场合，那么都可以通过 SaaS 化的沙箱获得自己专用的服务实例，完成所需的实验。同样，如果存在另一个服务需要与这个服务交互，那么上线前最终的验证与测试，也可以在沙箱环境中完成。

这里你可能会问，对于测试的情况，难道不能通过对契约打桩（Contract Stub）来实现吗？当然可以。只是通过 SaaS 化的沙箱，可以提供更接近生产环境的环境。

需要注意的是，有一种特殊的 SaaS 化形式，我称之为冷 SaaS。也就是服务团队不提供运行沙箱的基础设施硬件，而仅仅提供相应的镜像和脚本。这时候就需要沙箱环境的团队，可以在自己的基础设施上构造沙箱环境。因为云时代的基础设计与镜像等价（Infrastructure as Code，IaC 的一种形式），那么我们仍然可以在概念上，将这种方式看作是 SaaS。

因而，当我们完成了微服务改造之后，想要发挥微服务承诺的美好未来，总是要通过 SaaS 化将服务开放化的。

服务还是选项？

云计算的普及也是推动我们需要关注 SaaS 的一个重要因素。请考虑这样一个例子，针对某个应用或服务，我们希望可以对其进行云化处理，也就是使其具有水平扩展的能力。那么在假设这个应用或服务已经符合云化架构的诸多特点之后，最简单的做法是，将其放置于一个弹性边界之内，并通过弹性负载均衡处理相应的流量。当流量增加时，我们就在弹性边界内增加实例，以应对多出来的流量。

正如我们前面课程讲到的，这是一个典型的云化策略。但是这种策略有个问题，它会假设所有的用户都有一样的服务质量需求。这就相当于什么呢？这就相当于，当你买咖啡的时候，只有超大杯；当你发快递的时候，只有昂贵的次日达；当你选手机卡套餐的时候，只有 699 包月这一档。

当然你会问，超量满足客户的需求不好吗？如果你真能超量收钱的话，就当我没说。但绝大多数情况下，是为了服务好每一个用户而付出了超量的成本。所以这种云化策略就是一种昂贵的云化策略。

以可用性为例，我们都知道从 99.9999% 提升到 99.99999% 要付出巨大的成本代价。反之，从 99.99999% 降低到 99.99% 就能获得巨大的成本缩减。那么所有用户都需要一样的可用性吗？

可以再考虑一下前面微服务的例子，生产环境和沙箱环境对于可用性的要求明显不同。而且就算在生产环境中，不同消费者对于可用性的诉求其实也不尽相同。那么，作为功利主义架构师（Utilitarianism Architect），我自然要问，既然实际上消费者有不同的可用性诉求，那么什么样的云化策略，可以在实质上不影响服务质量的前提下，降低成本呢？

答案是 SaaS 化，但是并不是围绕着服务或应用，而是围绕着选项（Offering）。

之所以产生了昂贵的云化策略，原因就在于我们忽略了不同用户对于服务质量（可用性只是其中之一）的差异化需求，而仅仅考虑了服务或应用能力上的差异。那么，我们就需要将服务质量重新纳入考量。于是我们可以这么定义：

选项（Offering）= 能力（Capabilities）+ 服务质量（Service-Level Agreement/Non Functonal Requirement，SLA/NFR）

我们随便举个例子，比如快递，能力都是一样的，把包裹从 A 地运到 B 地。那么根据不同的服务质量，我们就可以定义出不同的选项：同城当天可达的是当日达；第二天保证送到的是次日达；三到五天的是标快；七到十天的是特惠；等等。

从运营的角度上讲，不同的选项实际代表了不同的运营成本。比如特惠可能会使用便宜的运力，像大卡车；而次日达则可能会使用昂贵的运力，像全货机。那么对应到云环境下，不同的服务质量实际上表示了不同的组网环境。

就以可用性为例，当我们选择不同的云平台选项时，基础可用性本身就存在差异，从多个 9 到不到 90% 都有可能。那么我们可以在不同的组网上，实现我们所需的可用性需求。然后当有流量进入时，我们可以按照不同流量的种类（比如开通的服务），在不同的组网上启动镜像即可。如下图所示：

在图中的例子里，对于选项 A，我们可能要求 5 个 9 的可用性，那么我们在 6 个 9 的云平台选项基础上，采用专用服务集群策略（dedicated services）实现 5 个 9 的可用性；而对于选项 B，可能只需要 1 个 9，那么我们可以在 2 个 9 的云平台选项基础上，采用共享服务策略（shared services）实现 1 个 9 的需求。

但无论是 5 个 9 还是 1 个 9，它们都是根据相同的镜像来获得服务实例的。因而在不同的组网之上，选项 A 和选项 B 的能力仍然是相同的，差异只存在于服务质量。

我们就可以根据消费者的不同诉求分派流量。那么选择选项 A 的消费者，可能是生产环境中的关键消费者，对于我们的服务存在关键路径依赖；而选择选项 B 的消费者，可能只是在边角功能中用到了我们的服务，而且并不在意我们的服务质量。或是实验性项目，或是测试沙箱等等。通过分流选项 B 的流量，我们节约了大量的运营成本。我们相当于围绕着不同的选项构建了 SaaS 化的云化策略。

那么你可能会问，在不同的选项中，能力可以不同吗？答案是可以的（当然这对于软件交付的策略、变化点的识别、架构的策略都提出了更高的要求。但由于不是我们这门课的主题，这里就不展开了）。于是我们可以得到一个通用的 SaaS 化的部署模式：

你自然可以脑补出在 K8S、Rancher 上的各种具体实现，我就不多说了。我的很多客户总是对实现 SaaS 化的技术细节很感兴趣，所以我每次都免费送他们这个图。因为这个图完全不值钱。毕竟决定 SaaS 化成败的关键，在于如何正确地设计选项，否则我们又怎么会知道在不同的组网中该放什么镜像，要去实现哪些服务质量呢？

需要几个选项？

那么我们要怎么设计选项呢？又需要几个选项呢？答案很简单，对于任意 SaaS 化的应用或者服务，都需要三类选项以应对三类客户：

高价值客户：这类客户除了看重服务质量外，更看重响应速度。类比到市面上可以见到的 SaaS 服务，也就是 GitHub 企业版、Salesforce 企业版这样的。对于这类客户，快速满足他们的个性化诉求，才是赢得青睐的关键。因而，通常 SaaS 产品，不光会采用单独独立的组网环境，还会提供专门的服务团队，以满足用户对于响应速度的要求。

现金奶牛：这类客户应该覆盖大部分的用户群体，比如 GitHub 免费用户，或是 Salesforce 标准版这样的用户。对于这类客户，目标就是降低成运营成本。比如通过自动化脚本实现低成本运维。再比如，通过自助式服务来解决服务开通等问题。

尴尬客户：这类客户价值不高，成本又减不下来。食之无味弃之可惜，庙小妖风大池浅王八多，都是这类客户的真实写照。解决办法通常是将其提升为高价值客户，或者简化为现金奶牛。但也需要注意的是，这些客户是 SaaS 化应用持续改进的重要源头。

这就是我发明的魔球 SaaS 选项建模法（Moneyball SaaS Offering Modeling）。从名字你也能看出来，灵感来自篮球中的魔球理论：三分价值高，上篮把握性大，中距离很尴尬。所以，要尽可能地将出手转化为上篮或者三分，避免中距离。同样的方法，可以帮助我们建模 SaaS 选项：将客户尽可能地转化为现金奶牛或者高价值客户，避免尴尬客户，从而得到一个简单直接的选项设计。

划分了客户群体类别之后，就可以进行选项设计了。这个时候，就需要考虑客户在能力和服务质量上的差异化诉求了。你可以在具体场景中自由发挥。

最后再顺便说一点。魔球建模中的三类客户，也代表了一种将存量系统 SaaS 化的变革策略。我们可以将现有系统和现有的服务质量，建模成遗留选项（Legacy Offering），然后将它放置在尴尬客户的 Tier 2。那么现在我们 100% 的用户都是尴尬客户了。

这个策略虽然不太妙，但是说明我们潜力无穷啊！于是我们可以建立相应的变革策略，从中分化出高价值客户和现金奶牛，并用新的选项去覆盖他们，这里可以采用产品设计和产品演进的思路推进变革。

鉴于产品设计和产品演进也不是我们这门课的核心内容，这里就不具体展开了。需要了解这方面信息的，可以找我司 CX（Customer Experiences）的产品专家们。

小结

这节课我们从微服务和云化策略入手，展望了它们后续的发展。发现殊途同归，最后都需要 SaaS 化能力。所以我说，无论是微服务还是云时代，下一站都会是 SaaS 化。当 SaaS 化成为我们默认的运营和架构风格，我们就进入了云时代的下一章。

在下一章中，不仅仅是业务建模，产品化设计、产品化运维、服务消费体验、生态体验都将会进入我们的视野。到那时，技术的景致（landscape）将会大有不同。我希望你能以一个开放的心态，迎接下一章。特别是，希望你能从现在开始，补充产品与体验设计相关的知识和技能。

让我们到那时再相见！See you on the other side！

思考题

最后一节课了，来畅想一下吧。如果进入下一章，你会怎样？

到这里，我们专栏的正文内容就全部结束了，感谢你一直以来的陪伴！还有一篇“结束语”马上会和你见面，我们下一讲再见！

结束语｜吃喝玩乐如何让我成为更好的程序员？

你好，我是徐昊。首先恭喜你完成了《如何落地业务建模》这门课程的学习。让我利用这最后的机会，再跟你说点题外话。

按道理写到这里，我应该从我丰富的技术经验中提取些也可适用于人生的大道理。比如建模人生什么的，才显得我是一个富有深刻洞见的技术达人。但恰恰相反，技术从没有反哺过我的人生，反而是我的人生给养了我技术上的洞见。所以今天我要来讲一讲，吃喝玩乐是如何让我成为一名更好的程序员的。

我爱喝葡萄酒，起因是看漫画。我特别喜欢有一定真实背景的番，《将太的寿司》《王牌酒保》等等。里面乱七八糟的知识点，我会在现实中进行尝试。于是看漫画也就有了一种类似于诗的意境。就好像到西湖淋了雨，回到北京就会想起：“春衫犹是，小蛮针线，曾湿西湖雨”。漫画也可以提供这种可思可玩的背景，与古典诗歌无异。

那么葡萄酒番，自然就是《神之水滴》了，里面超级中二的通感能力让我浮想联翩，于是准备自己也试一试。买到漫画里同款的酒之后，遇到的第一个障碍，就是里面对于味道与香气的描述，与我平时所熟悉的风味大异其趣。比如黑色浆果里的黑醋栗，鬼知道这是个什么玩意儿。还有什么赤霞珠（Cabernet Sauvignon）透过鼻腔会有薄荷的香气，怎么可能？完全感受不到！

于是我又通过多方途径，买来这些水果蔬菜香草，想训练一下我的舌头。然后就有了第二个障碍，就算我面前同时放着黑醋栗和一杯赤霞珠，但我怎么也无法发现在赤霞珠中透出的黑醋栗香气。然后我就陷入了深深的怀疑，难道真像漫画里说的，化学调味剂吃多了，舌头坏掉了不成！

好在我及时发现了酒侍们（Sommelier）训练味觉的工具：酒鼻子。一套 60 种（或 50 几种）不同香气的参考。那么通过嗅闻训练，各种典型香气也就熟悉了。然后让我重拾自信的是，对照着酒鼻子里面的黑醋栗去闻真实的黑醋栗的味道，发现二者差距极大。所以我之前没辨识出来，纯属正常。

那么为什么酒里的黑醋栗味道和真实的黑醋栗没什么关系，我们还要说从酒中闻到了黑醋栗呢？其实不在于黑醋栗的味道真正是什么，而在于喝酒的人对这种味道达成了共识，同意选择使用黑醋栗去描述这种味道。一言以蔽之，共识胜于真相。

也就是说，并不在于它到底是什么，而在于我们都同意使用这么个词去表示一种我们有共识的味道。

酒鼻子里另一个明显的例子，是菠萝还是其他什么味道，我记不清楚了。这个味道，对于亚洲人来说就是香蕉味，对于欧洲人来说就是菠萝味。因为这个味道同时存在于这两种水果中。但亚洲人更熟悉香蕉，而欧洲人更熟悉菠萝。不过没关系，在喝酒的时候，只要我们有共识，这个就叫菠萝味。通过共识的词汇构成沟通的桥梁最重要，词语是什么并不重要。

所以这在技术上给予我什么启示呢？在业务建模中形成统一语言的时候，最重要的产出物是共识。统一语言只是共识的载体。没有共识，统一语言完全不重要；而有了共识，统一语言是什么不重要（当然要和模型关联）。

顺便说一句，我虽然爱喝，然则鉴赏天赋确实一般，属于靠大量饮酒刺激神经网络训练出来的舌头。还是很羡慕那种，什么都没练过，只是闻一下或是喝一口，靠生理反应就能知道好坏的天赋。

然后我还喜欢刷抖音，我总是被抖音种草，买过很多奇怪的东西。比如，训练狗和人说话的按钮，去祸害别人家的狗。虽然总体上当居多，但还是乐此不疲。比如，之前东北某地出现一名新冠患者，流调显示他曾多次外出购买鸡架。于是抖音上大家就很兴奋，不知道到底是何种小吃，能让人如此欲罢不能。

随后就出现了好几个类似的视频，都是去买肯德基新出的炸鸡架。但是由于肯德基的炸鸡架只能叫外卖，于是他们只好坐在肯德基门店的门口，通过叫外卖来买鸡架，然后送到肯德基门店门口。于是荒谬的一幕出现了，同样一家店，外卖小哥进去就能拿到鸡架，而一般的顾客却不能。于是，大家都来了热情，晒肯德基的鸡架一时成风。

图为编辑同学跟风购买的肯德基炸鸡架

我是个俗人也爱凑热闹，于是被抖音成功种草。谁知道我家附近肯德基的鸡架格外抢手，连续几天都没抢到。于是我穷则思变闻鸡起舞，早上设了个 6 点半的闹铃，起床第一件事就是外卖下单买肯德基鸡架。吃到之后，味道也是平常，完全不值得早起。而且嚼着鸡架的时候，浑身激灵。倒不是有了灵感，主要是因为咸的。

我痛定思痛，发现其实也不算上当。因为它在技术上（广泛意义来说）给了我很大启示。那就是肯德基推出烤鸡架产品的整个过程，是非常典型的节俭创新（frugal innovation）的过程。

首先是存在一个与肯德基主流产品有交集的新品品类——炸鸡架。那么肯德基需要验证的假设是，是否值得为鸡架开发一款产品。通过传统的研发渠道，成本太高。就算成功，不仅需要更换店面信息、修改线上点单系统，还需要保证所有门店都能提供同品质的产品。对于快餐连锁店而言，一家上新等于家家上新（当然也可以局部地区，或某些示范店作为尝试）。

于是肯德基选择了一种节俭创新的方式，仅仅通过外卖渠道提供炸鸡架。通过外卖渠道提供试验产品，对于已经存在的线下线上信息系统毫无影响。通过外卖渠道，可以很容易地控制提供商品的门店。通过外卖渠道，可以使用更少的门店覆盖更大的范围。最后，如果产品不受欢迎，那么只需要取消这个外卖品类即可。由于肯德基从来没在线下门店中售卖过这款商品，就好像这个品类从来不曾出现过一样。

这是一个非常有意思的案例，它利用别人的数字化渠道，从而降低了自身创新的成本。这是数字化时代节俭创新的典范案例。而我是通过刷抖音被种草然后了解到的（想进一步了解的，可观看《八叉说：精益创业之肯德基炸鸡架》）。

所以吃喝玩乐并不耽误事，因为学习是一种状态（being），而不是一种行为（doing）。在学习的状态中，吃喝玩乐都可以让我成为更好的程序员。与诸君共勉，就此别过，他日江湖再见！

最后再播放一条重要消息。我为你准备了一份调查问卷，题目不多，希望你可以花一到两分钟填一下。很期待能听到你的反馈，说说你对这门课程的想法和建议。