关键要点
- 反应式的分布式应用程序必须知道如何处理有关消息传递的不确定性,包括多次传递和无序传递。
- 领域驱动设计通过良好的建模来管理不确定性。
- 处理不确定性的规则必须在领域专家同意的业务逻辑中定义,而不是埋没在技术实现中。
- 消息去重器或重排器的实现最初看起来可能很简单,但在实际操作时会变得困难。
- 当可以使用通用语言来定义规则时,实现将变得非常简单且健壮。
我认为应该通过领域驱动设计的方式来开发软件。从 Evan 最初出版“ Domain-Driven Design: Tackling Complexity in the Heart of Software ”开始,我们经历了一趟艰难的旅程,已经走了很长的一段路。现在有一些关于领域驱动设计的技术大会,人们对领域驱动设计的兴趣程度也与日俱增,包括业务人士的参与,这是非常关键的。
如今,反应式变得越来越重要,稍后我会解释为什么它获得人们的关注。我认为真正有趣的是,DDD 在 2003 年的使用或实现方式与今天使用 DDD 的方式截然不同。如果你已经读过我的红皮书“ Implementing Domain-Driven Design ”,那么你可能已经熟悉这样的一个事实:我在这本书中提到的有界上下文是单独的过程,需要进行单独的部署。然而,在 Evan 的蓝皮书中,有界上下文在逻辑上是分开的,但有时候部署在同一部署单元中,可能是在 Web 服务器或应用程序服务器中。现如今,越来越多的人采用 DDD 是因为它与单独部署机制(例如微服务)不谋而合。
领域驱动设计的本质并没有变——在有界的上下文中建模一门通用语言。那么,什么是有界上下文?基本上,有界上下文背后的想法就是在清晰地划定模型之间的边界。围绕领域模型的边界让边界内的模型变得非常清晰,让概念、模型的元素以及团队(包括领域专家)对模型的思考方式都具备了非常明确的含义。
你会发现团队内形成了一种通用语言。例如,当有人在讨论问题时提到“产品”,他们确切地知道,在这种情况下产品代表了什么意思。而在另一个上下文中,产品可能具有不同的含义。产品可以在有界的上下文中共享身份,但一般来说,在另一个上下文中(至少)具有略微不同的含义,甚至可能具有非常不一样的含义。
我们意识到,并不存在一种实用的方法可用来建立规范的企业数据模型,模型中的每个元素都代表了企业的每个团队希望如何使用它们。这种事情是不可能发生的。因为差异总是存在的,很多时候,因为存在太多差异,导致一个团队难以使用另一个团队创建的模型。这就是为什么我们要使用带有通用语言的有界上下文。
在一个团队中,通过一种通用语言对一个实体进行非常明确的定义,一旦理解了这一点,你就会意识到,其他团队也会通过类似的方式开发其他模型。或许开发相同模型的同一个团队也可能负责开发其他模型。在某些情况下,你可能拥有多个有界上下文,因为我们无法在单个企业中或单个系统中为我们将要使用的每个概念定义含义。
鉴于我们有多个上下文和多种语言,我们必须让它们之间进行协作和集成。为此,我们使用了一种称为上下文映射的技术或工具。
图 1:上下文映射
在这张图中,有界上下文之间的线表示上下文映射,可以称其为翻译。如果其中一个有界上下文使用一种语言,与其相连的上下文使用了另一种不同的语言,那么你需要在两种语言之间做些什么才能让两个模型之间相互理解?答案是翻译。通常,我们需要在模型之间进行翻译,这样才能让每个模型保持纯净。
在创建上下文映射时,不要限制连线的含义。上下文映射可能涵盖技术集成、风格或技术,但定义上下文之间的团队关系也非常重要。是使用 RPC 还是 REST 并不重要。与谁集成比怎样集成更重要。
针对不同类型的关系,有各种上下文映射工具,包括合作伙伴关系、客户与供应商关系或随从(conformist)关系。在合作伙伴关系当中,一个团队会对另一个团队的模型有很多了解。客户与供应商关系在模型(一个是上游模型,另一个是下游模型)之间引入了一个反腐(anti-corruption)层。上游模型需要进行反腐,因为它被下游模型消费。如果下游模型需要将某些内容发给上游,它将被翻译成上游模型,以便可以进行可靠而一致的数据交换,且具有明确的含义。
到目前为止,我所描述的战略设计实际上是域驱动设计的本质,因此也是最重要的部分。
在某些情况下,我们会小心翼翼细粒度地进行通用语言建模。如果你将战略设计比作使用宽笔刷画画,那么战术设计就是使用细笔刷来填充细节。
DDD 建模的一般性指南
根据我对提及 DDD 相关大会的观察以及我与团队进行的广泛的合作,我找到了一些有助于 DDD 建模的小技巧。我希望能够提供一些指导,让你走在正确的方向上,不至于跑偏。
我们必须记住,在进行建模时,特别是在战术上,我们需要领域专家(而不只是程序员)的帮助。但我们不能占用他们太多的时间,因为在团队中扮演领域专家角色的人通常忙于与业务相关的其他事务。
另外,我们要避免贫血领域模型。当你看到某些领域建模演示当中包含自动创建 getter、setter、Equals()、GetHashCode() 的注解时,请立即远离它们。如果我们的领域模型只是关于数据,那么这些演示可能会是完美的解决方案。但我们需要问一些问题,比如数据是如何产生的?我们如何让数据进入到领域模型中?它是基于业务和领域专家的心理模型进行表达的吗?getter 和 setter 并不会明确指出模型的含义——它们只是用来移动数据。如果你正在考虑使用 DDD,那么应该意识到 getter 和 setter 其实是你的敌人,因为你真正想要建模的是那些能够表示企业完成工作所需的方式的行为。
在建模时要非常明确。例如,你使用 UUID 来表示实体或聚合的业务标识符。使用 UUID 作为业务标识符没有任何问题,但为什么不将它包装在强类型的 ID 类型中呢?另一个不使用 Java 的有界上下文可能无法理解 UUID 是什么意思。你可能需要定义 UUID ToString(),然后将字符串保存成其他类型,或者在有界上下文之间传递事件时需要对字符串进行翻译。
在使用 BigDecimal 时,为什么不考虑创建一个 Money 值对象。如果你使用过 BigDecimal,你就该知道识别舍入因子是一个常见的痛点。如果我们让 BigDecimal 渗透到我们的模型中,那么我们该如何对金额进行舍入操作?解决办法是使用 Money 类型来标准化业务所说的舍入规范。
另一个小技巧是不要担心使用怎样的持久化机制,或者使用什么类型的消息传递机制。使用符合你特定服务级别协议的内容就可以了。根据实际的吞吐量和性能做出合理的选择,不要将事情复杂化。
反应式系统
至少在我的世界里,我已经看到了反应式系统的发展趋势。不仅微服务具备反应式,构建整个系统也是反应式的。在 DDD 中,有界上下文中也会发生反应式行为。反应式并非新鲜事物,Eric Evans 在引入事件驱动时就已涉及反应式。使用领域事件意味着我们必须对过去发生的事件做出反应,并将我们的系统带入融洽状态。
如果你对系统不同层的连接进行可视化,你就会看到这种重复的模式。无论是整个互联网、企业层面的所有应用程序,或是微服务中的单个参与者或异步组件,每一层都有很多连接和相关的复杂性。我们因此很多非常有趣的问题需要解决。我想要强调的是,我们不应该用技术来解决这些问题,而应该对它们进行建模。如果我们将开发云端微服务作为构建业务的手段,那么分布式计算就是我们所从事的业务的一部分。并不是说分布式计算形成了我们的业务(在某些情况下,它确实如此),而是我们通过分布式计算来解决问题。
我需要花一点时间来解释一些概念,一些开发人员将它们作为反对异步、并行、并发或任何其他与同时发生同个动作相关的技术的论据。人们经常引用 Donald Knuth 的话说,“过早优化是万恶之源”。但这有点断章取义。他的完整的说法应该是,“我们应该忽略细微的效率优化……过早的优化是万恶之源”。换句话说,如果我们的系统存在很大的瓶颈,我们应该先解决它们。
Donald Knuth 还说了一些非常有趣的事情:“对计算机有一定了解的人应该至少知道底层硬件的含义。否则,他们写出来的程序将非常奇怪”。他的意思是说,我们需要通过我们今天编写软件的方式来利用硬件。
如果我们回到 1973 年看看处理器的制造方式,当时的晶体管数量非常少,而且时钟速度低于 1MHz。根据摩尔定律,每隔几年我们就会看到晶体管数量和处理器速度翻倍。然而,到了 2011 年,时钟速度却开始下降。在过去,时钟速度翻倍需要一年半或两年的时间,而现在需要大约十年(如果不是更长的话)。晶体管的数量继续在增加,但时钟速度却没有。今天,我们拥有更多的处理器内核。我们拥有更多的内核,而不是更快的始终速度。那么我们应该如何使用这些核心呢?
如果你一直在使用 Spring 和 Reactor 项目(使用了反应式流),那么这些基本上就是我们现在能够做到的事情。我们有一个发布器,这个发布器发布一些东西,我们称之为领域事件,也就是图 2 中的橙色方框。这些事件被传递给流的订阅者。
图 2:反应式
请注意,流中的淡紫色方框上有问号。它们实际上是指策略,这些策略位于流和订阅者之间。例如,订阅者可能对其可以处理的事件或消息的数量有限制,策略为订阅者指定这些限制。关键是,发布者、流和三个订阅者是使用单独的线程运行的。如果这是一个大型复杂的组件,在任何时候线程都不能被阻塞。因为如果线程被阻挡,那么其他部分就会等待线程。我们必须确保内部实现也必须充分利用好线程。随着我们深入不确定性建模,这将变得更加重要。
微服务是反应式的。但当我们深入内部时,发现各种组件可以同时或并行运行。当从微服务中发布事件时,它们最终会在有界上下文之外发布到某种主题上(可能使用 Kafka)。为了简单起见,我们假设只有一个主题。我们的反应式系统中的所有其他微服务都在使用发布到这个主题上的事件,并且在服务内部做一些反应式处理。
图 3:反应式系统
这就是我们想要达到的状态。当一切都在异步发生时,会发生什么?这带来了不确定性。
欢迎不确定性
在理想情况下,当我们发布一系列事件时,我们希望这些事件按顺序传递,而且只需传递一次。每个订阅者将收到事件 1,然后是事件 2,接着是事件 3,每个事件只出现一次。程序员已经被教会要小心翼翼地维持这种状态,因为这让我们对我们正在做的事情感到确定。然而,在分布式计算中,这种情况并不会发生。
微服务和反应式带来的不确定性首先是事件以怎样的顺序传递、是否会多次收到同样的事件,或者根本收不到事件。即使你在使用像 Kafka 这样的消息传递系统,而你认为这样就可以按顺序接收到事件,那么你是在自欺欺人。如果有任何消息可能出现乱序,那么你就必须做好应对所有消息都会出现乱序的准备。
我找到了一个对不确定性的简洁定义:不确定的状态。不确定性是一种状态,这意味着我们可以处理它,因为我们有办法推理系统的状态。最终,我们能够达到一种让我们感到舒适的确定状态,即使只持续一毫秒。蔓延的不确定性让事情变得不可预测、不可靠和风险重重。不确定性令人感到不舒服。
上瘾
大多数开发人员学会了所谓的开发软件的“正确方法”,让他们沉迷其中。如果你有两个需要相互通信的组件,可有将一个组件作为客户端,另一个组件作为服务器。但这并不意味着它必须是一个远程客户端。客户端将调用服务器的方法或函数。在发生调用时,客户端等待从服务器返回响应,或者可能抛出异常。不管怎样,客户端可以肯定自己会再次获得控制权。这种执行流程的确定性是我们必须处理的一种上瘾。
第二种常见的上瘾是对排好序且没有重复的事物的上瘾。当我们还是个小孩的时候,在学校里学习数数就是按顺序来的。当我们知道某些东西是按顺序排列时,我们会感觉良好。
另一个让我们上瘾的是数据库的锁。如果我的数据源中有三个节点,当我向其中一个节点写入时,我认为我已经将数据库牢牢地锁定。这意味着当获得成功响应时,数据应该已经持久地保存在所有三个节点上。但当你开始使用 Cassandra 时,它并不会锁定所有节点,那么你该如何查询尚未传播到集群中所有节点上的值呢?
所有这些都给我们带来了不舒服的感觉,我们必须学会应对它们。不过,感觉不舒服并不是不可以忍受的。
防御机制
因为我们沉迷于确定性、阻塞和同步,开发人员倾向于通过建立堡垒来应对不确定性。如果你正在基于有界上下文创建微服务,那么你会让所有东西在这个上下文中的都是阻塞、同步和非重复的。我们在这里构建了一个堡垒,所有的不确定性都存在于这个堡垒之外。
从基础设施层开始,我们创建了去重器和重排器,它们都来自企业集成模式。
去重器
如果事件 1、事件 2、事件 1 和事件 3 进入系统,当它们经过去重器时,我们确定我们只会得到事件 1、事件 2 和事件 3。这似乎不是一个难以解决的问题,但在实际实现时需要考虑到其他一些问题。我们可能会使用缓存,但不能无限制地在内存中保存事件。这意味着需要使用数据库表来存储事件 ID,并检查每个传入事件在之前是否已经出现过。
如果某个事件之前没有出现过,那么就往下传。如果之前已经出现过,那么就可以忽略它,对吧?那么为什么之前会出现过?是因为我们之前在收到它时没有其进行确认吗?如果是这样,我们是否应该再次确认已经收到它了?我们需要保留这些 ID 多长时间?一周够吗?一个月怎么样?
现在让我们来谈谈不确定性。试图通过技术来解决这个问题可能非常困难。
重排器
假设我们看到事件 3,然后是事件 1,然后是事件 4。出于某种原因,事件 2 迟到了很长一段时间。我们首先必须找到重新排序事件 1、事件 3 和事件 4 的方法。如果事件 2 尚未到达,我们是否要关闭我们的系统?或许我们可以允许事件 1 通过,但我们有一些规则规定在事件 2 到达之前不能处理事件 1。在事件 2 到达后,我们就可以安全地将所有四个事件放入应用程序中。
不管你选择的是哪一种实现,要解决这些问题都很难,因为存在不确定性。
最糟糕的是,我们还没有解决任何业务问题。我们只是在解决技术问题。但是,如果我们承认分布式计算现在已经成为业务的一部分,那么我们就可以解决这些存在于模型中的问题。
我们想说,“停止所有的东西,我现在准备好了”。但问题是根本就没有“现在”这种东西。如果你认为你的业务模型是一致的,它可能只能维持一纳秒,然后再次出现不一致。
例如 Java 中的 LocalTime.now(),一旦你调用了这个方法,它就不再是现在。当你收到返回的时间时,“现在”已经不存在了。
分布式系统的不确定性
这一切都把我们带回了分布式系统都是关于不确定性的事实。分布式系统将继续存在。如果你不喜欢它,就请离开这个领域,或许你可以去开一家餐馆。但是,你仍然需要面对分布式系统,只是不是你在开发它们。
对不确定性进行建模是非常重要的,因为这里有多个核心问题,也因为云的存在。
微服务很重要,因为这是每个人都在使用它。大多数人都向我学习领域驱动设计,因为他们认为这是实现微服务的一种好方法。我同意这一点。此外,企业希望摆脱单体系统。他们被困在单体系统的泥潭中,需要数月才能发布一个版本。
延迟也很重要。当你将网络作为分布式系统的一部分时,延迟很重要。
物联网也很重要。很多小型的便宜设备都通过网络进行交互。
DDD 和建模
我指的是好的设计、糟糕的设计和有效的设计。
你可以设计出非常好的软件,但却缺失业务需求。以 SOLID 为例,你可以按照 SOLID 原则设计出 100 个类,但却完全忽略业务需求。面向对象和 Smalltalk 的发明者 Alan Kay 说,对象真正重要的是在它们之间传送的消息。
在日语中,Ma 的意思是“……之间的空间”。在这里,当然是指对象之间的空间。对象需要设计得足够好才能能够发挥它们单一职责的作用。但这不仅仅是关于对象的内部,我们还需要关心对象的名称以及在它们之间传送的消息。这就是通用语言发挥作用的地方。如果你能够在模型中捕获到它,你的模型就会变得更有效,而不仅仅是好,因为你将可以满足业务需求。这就是领域驱动设计的意义所在。
我们想要开发反应式系统,我们接受不确定性是一种状态的事实,我们将开始解决问题。
Pat Helland 在他的论文“ Life Beyond Distributed Transactions, an Apostate's Opinion ”中写道:“在一个不能相信分布式交易的系统中,必须在业务逻辑中实现不确定性管理”。在实现分布式事务很长时间之后,才有了这种认识。不过,在亚马逊工作期间,他确定,使用分布式事务无法实现他们所需的规模。
Helland 在论文中谈到了活动。当两个伙伴实体中的一个发生状态变更,并触发一个描述状态变更的事件,另外一个实体收到这个事件,那么就可以说这两个实体之间发生了一个活动。但第二个实体什么时候会收到事件?如果收不到该怎么办?这种“假设”情况会很多,Pat Helland 说这些“假设”应该由活动来处理。
在任意两个伙伴之间,每一方都必须管理来自另一方的活动。图 4 是我对 Pat Helland 的观点做出的解释。
图 4:活动
每个合作伙伴都有一个 PartnerActivities 对象,代表从相关实体那里收到的活动。当遇到一些直接针对我们的活动时,可以将它们记录下来。然后,在未来的某个时间,我可以问,“我之前看到过这个活动吗?”这样既可以使用已经发生的活动,又可以检查之前是否已经看到过它。
这似乎很简单,但在长期存在的实体中会变得复杂。Pat Helland 也说这种复杂性会急剧增加。为了跟踪活动,长期存在的实体(可能与很多合作伙伴一起)可能会导致大量实体聚集(DDD 称之为聚合)。此外,出现这种情况的迹象并不明显——它并不会显示任何与业务本身有关的信息。
我认为,我们应该比 PartnerActivity 更进一步。我提议将其放在软件的核心——领域模型中。我们消除了基础设施层中的那些元素(去重器和重排器),并且在事件发生时让它们通过。我相信这将让系统变得不那么复杂。
在显式模型中,我们监听事件,并发送与业务操作相对应的命令。领域将聚合每个包含如何处理这些命令的业务逻辑,包括如何响应不确定性。事件和命令之间的通信由进程管理器来处理,如图 5 所示。虽然图 5 可能看起来很复杂,但实际的实现非常简单,并遵循反应式模式。
图 5:进程管理器
每个领域实体都负责根据收到的命令来跟踪状态。通过遵循良好的 DDD 实践,可以基于这些命令安全地跟踪状态,并通过事件溯源来持久化状态变更事件。
每个实体根据领域专家做出的决定负责了解如何处理潜在的不确定性。例如,如果收到重复事件,那么聚合就会知道之前已经看到过这个事件,并知道如何做出响应。图 6 显示了处理这类问题的一种方法。当收到 deniedForPricing() 命令时,我们可以检查当前进度,如果我们已经看到过 PriceDenied 事件,那么我们就不会再发出新的领域事件,而是做出响应,表明已经看到过拒绝事件。
图 6:处理重复消息
这个例子可能不会给人留下什么印象,但其实是有目的的。它表明,将不确定性视为领域的一部分意味着我们可以使用 DDD 实践让不确定性成为业务逻辑的另一个方面。而这就是 DDD 的意义所在——帮助管理软件核心的复杂性,例如不确定性。
关于作者
Vaughn Vernon是一名软件开发人员和架构师,在业务领域拥有 35 年的经验。Vaughn 是领域驱动设计领域的专家,也是反应式系统的倡导者。他是 vlingo/platform(一个开源的面向 DDD 友好的反应式工具包)的创始人、首席架构师和开发者。他参与并教授 DDD 和反应式软件开发,帮助团队和组织挖掘业务驱动和反应式系统的潜力。Vaughn 是三本书的作者:Implementing Domain-Driven Design、Reactive Messaging Patterns with the Actor Model 和 Domain-Driven Design Distilled,这三本书均由 Addison-Wesley 出版。
评论