专栏首页ThoughtWorks讲真,别再拿着聚合寻找限界上下文了

讲真,别再拿着聚合寻找限界上下文了

聚合分组法采用“相关性”来划分限界上下文,其问题在于缺少一个主题,而子域恰好可以用来提供这个主题。本文的“愿景”-“核心域”-“周边子域”方法,不是唯一分解问题域的方法,任何可以将领域分解成高内聚低耦合的子域的方法都是可行的方法。

聚合分组法和它的问题

在事件风暴工作坊中,常用的划分限界上下文的方法是:

对前一步(事件风暴)产生的聚合进行分组,通过业务的内聚性和关联度划分边界,结合限界上下文的定义进行判断,并给出上下文名称。

[服务化设计阶段路径方案]

我将其称之为“聚合分组法”。然而面对一堆聚合,要得出一套合理的分组是非常困难的:

  1. “相关性”全凭经验 相关性是一个过于抽象的规则,非常依赖经验。 举个例子。在一个活动运营系统中,有“注册奖励活动”、“注册奖励规则”、“任务奖励活动”、“任务奖励规则”等概念。是把所有的“活动”分为一组,所有“规则”分为一组,还是把“注册”相关的分为一组,把“任务”相关的分为一组?这是个让人头疼的问题。也许你会说需要业务人员的输入,但是业务人员很可能只会告诉你这些概念之间都有关系。
  2. 不健康的聚合上下文 聚合分组法很容易导向一种按照聚合划分的架构。服务围绕聚合建设,而非针对某个业务价值,也就无法提供正确的业务价值。围绕聚合建设的服务,看上去可以复用,但是会造成服务间的紧耦合,容易成为最糟糕的分布式单体架构: 当架构是分布式单体时,往往需要同时修改多个服务,同时部署多个服务、服务之间调用非常频繁。 [You’re Not Actually Building Microservices] 聚合分组法也无法很好的识别“重复的概念”问题([领域驱动设计]14.1,指某一个概念,应该被设计成多个模型,因为它们有不同的规则,甚至有不同的数据)。使用聚合分组法往往导致把带着这样的聚合简单的放到某个限界上下文中。
  3. 隐藏的划分方案 还很可能是这种情况:在使用聚合分组法时,架构师已经有一个隐藏在心里的模糊的划分方案,在划分限界上下文时都是往该方案上靠。但是由于这个划分方案只是模糊存在于架构师的脑中,并没有拿出来讨论,很可能经不起推敲,最终无法言说,沦为“by experience”。

如何划分限界上下文

如何划分限界上下文?在回答这个问题前,让我们先看看限界上下文到底是什么。

在[领域驱动设计]第14章提出了著名的限界上下文。限界上下文是为了分解大型模型:

然而在几乎所有这种规模的组织中,整个业务模型太大且过于复杂以至于难以管理,甚至很难把它作为一个整体来理解。我们必须把系统分解为较小的组成部分,无论在概念还是在实现上。

有时,企业系统会集成各种不同来源的子系统,或者包含诸多属于完全不同领域的应用程序。要把这些不同部分中隐含的模型统一起来是不可能的。通过为每个模型显式地定义一个限界上下文,然后在必要的情况下定义它与其他上下文的关系,建模人员就可以避免模型变得混乱。

[领域驱动设计]第四部分

限界上下文告诉我们,同一个概念,不必总是对应于一个单一模型,也可以对应于多个模型。用限界上下文明确模型要解决的问题,可以保持每个模型的清晰。限界上下文是领域模型的边界,也就是领域知识的边界。和上下文主题紧密相关的模型内聚在上下文内,而其他模型被会分到其他限界上下文中。限界上下文内的领域知识是高内聚低耦合的。

限界上下文的主题是什么呢?我认为是子域。每个限界上下文专注于解决某个特定的子域的问题。每个子域都对应一个明确的问题,提供独立的价值,所以每个子域都相对独立。子域及其对应的限界上下文中的模型会因为其要解决的问题变化而变化,不会因为其他子域的变化而变化,即低耦合;当一个子域发生变化时,只需要修改其对应限界上下文中的模型,不需要变动其他子域的模型,即高内聚。

Evans也谈论了限界上下文和子域的关系:

One confusion that Evans sometimes notices in teams is differentiating between bounded contexts and subdomains. In an ideal world they coincide, but in reality they are often misaligned.

Evans有时会在团队中发现的一个困惑,就是如何区分限界上下文和子域。在理想的世界中它们是重合的,但在现实世界中它们常常是错位的。

[Defining Bounded Contexts — Eric Evans at DDD Europe]

当我们设计一个新系统或者设计遗留系统的目标架构时,我们往往会按照理想的方式进行设计。而在理想情况下,子域和限界上下文是重合的。

[领域驱动设计精粹]中也讲述了一个通过寻找核心域相关的概念来识别限界上下文的方法。


如何分解子域

根据子域来识别限界上下文,那么子域如何得到呢?我们通过分解问题域的方式,将整个问题域分解成若干个更小、更简单、更容易解决的问题子域。

我们需要某种方法,将领域分解成逻辑上相互独立且没有交叉的子域。在这里的方法是通过产品愿景,识别核心域,进而识别核心域周边的子域。

识别核心域

由于核心域是最明显、最容易识别出来的子域,所以我们先从核心域开始。

每一个子域甚至每一个领域模型都是为了产品愿景而存在的。我们分解子域的第一步,就是从产品愿景中获取核心域。产品愿景包含“相对抽象的产品价值”,以及“实现该价值的主要功能”。其中,主要功能就是我们寻找核心域的依据。想象一下,如果要做MVP的话,我们会挑选最能够提供其核心价值的功能来开发,以验证产品价值。MVP往往就是核心域。

以上述活动运营系统为例,其产品愿景是通过各种吸引用户的优惠活动,以帮助客户通过活动提升用户量和知名度。其核心域是给客户提供吸引用户的多样的灵活的活动,包括活动形式、活动规则和多种奖励。

识别核心域周边的子域

核心域识别出来了,接下来就是识别核心域周边的子域。核心域往往不会独立存在,会有其他子域同核心域一起才能达成业务目标。这里需要回答的问题是:

  • 有哪些子域是用来支撑核心域的? 这些子域是帮助核心域更好的工作。例如提供审批流程以配置核心域,提供各种辅助功能更好的为核心域提供内容。
  • 有哪些子域是核心域衍生出来的? 核心域经常会产生一些数据,这些数据也有其价值。比如产生各种报表,活动奖励的发放记录。
  • 有哪些子域是用来支撑或衍生自这些新识别出的子域的? 用来支撑核心域的子域、以及核心域衍生的子域,也有各自的支撑子域和衍生子域。

(活动运营系统)

识别出来的每个子域只对应一个问题,子域之间是相互独立的,没有交叉,不是包含关系。所以子域加起来就是整个领域。

也可以通过角色、时间等因素分解子域。解决不同角色的问题可能分属不同子域,比如用户参与活动、运营人员配置活动分属不同子域,两个子域的变化原因不同;不同时间使用的功能可能属于不到子域,比如先有运营人员配置活动,再有用户参与活动,配置活动和参与活动分属不同子域。

如果按照聚合分组划分限界上下文,很可能出现“活动上下文”,同时活动模型,即承担运营人员配置的职责,又承担用户参与规则校验的职责,这会导致职责过多,违背了单一职责。另外活动规则校验的模块需要支持高并发,需要使用和配置模块不同的技术架构。如果这些相似的概念和不同的技术实现属于不同的上下文,就可以保持各自模型的完整,技术上也可以做到独立演进。

子域的粒度

理论上子域仍然可以被分解。例如活动子域可以分解为活动参与规则子域、奖励子域等。那么子域粒度多大是合适的呢?

我们希望每个子域可以解决某个特定的问题,让这个问题的解决方案都内聚在子域对应的限界上下文内,所以如果问题的再分解没有的边界并不清晰,建议先不分解。随意的拆分会导致成为“分布式单体”。


识别限界上下文

一个限界上下文封装了一个相对独立子领域的领域模型和服务。

子域subdomain和限界上下文某种意义上是互相印证的

[DDD战术篇:领域模型的应用]

这个时候我们通过事件风暴得到的领域模型就可以出场了。领域模型和子域都是从业务知识里分析得到的,将两者匹配起来可以再次验证我们对于业务的理解、子域的分解和领域模型是否合理。

为每个子域创建一个解决其问题的限界上下文,然后为每个领域模型找到其归属的限界上下文。每个领域事件都是为了解决某个问题,它和它相关的领域模型就应该放在这个问题子域对应的限界上下文里。

比如“活动已上线“这个事件,由运营人员在配置时触发,会导致用户可以开始参与活动。那么这个事件及其对应的“活动”概念应该被分为两个模型,分别归属于活动配置子域对应的“活动配置上下文”和活动子域对应的“活动上下文”。

为领域模型寻找归属完成后,我们会发现这么几个情况。

  • 同一个概念可能会出现在多个限界上下文中。发生这种情况很正常,说明这多个子域都需要这个概念,而且很可能不同子域的领域模型不完全相同。 比如刚才说到“活动”既存在于“活动上下文”中,又在“活动配置上下文”中。这里我们就很好的识别出了“重复的概念”问题。
  • 也有一些概念重复在多个限界上下文中,这些概念和该上下文的主题并没有紧密的关系。这些模型可以单独出一个限界上下文,用以同时支撑多个限界上下文,以减轻限界上下文的负担。
  • 有时候某个模型找不到合适的限界上下文,说明很可能是遗漏了一个子域,那就需要回到“分解子域”步骤,重新审视产品愿景。

聚合分组法采用“相关性”来划分限界上下文,其问题在于缺少一个主题,而子域恰好可以用来提供这个主题。本文的“愿景”-“核心域”-“周边子域”方法,不是唯一分解问题域的方法,任何可以将领域分解成高内聚低耦合的子域的方法都是可行的方法。

本文分享自微信公众号 - ThoughtWorks洞见(TW-Insights),作者:祁兮

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-06-30

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

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

    在 DDD 中,通用语言是以限界上下文为边界的。如果一个产品或者项目有多个限界上下文,我们就需要为每个限界上下文定义通用语言。

    ThoughtWorks
  • 实体零售突围 | TW商业洞见

    科技即商业 TECHNOLOGY IS BUSINESS 互联网对于实体零售的冲击是前所未有的,一个新的购物范式由此产生,并大行其道,最终形成全新的商业生态,包...

    ThoughtWorks
  • 服务拆分与架构演进|洞见

    本文首发于InfoQ: http://www.infoq.com/cn/articles/service-split-and-architecture-evol...

    ThoughtWorks
  • DDD精粹:运用子域进行战略设计

    用户1682855
  • 普华永道:2017年全球金融科技调查报告

    据普华永道表示,零售银行、投资及财富管理和资金转移支付将是未来五年被金融科技(FinTech)颠覆程度最高的领域,电商平台、大型科技公司和传统金融机构是这场变革...

    钱塘数据
  • 一个优秀的架构师应该具备什么?需要掌握哪些技术?

    时光退回到七八年以前,那个时候“架构师“还是一个很“高大上“的title。可是在今天的互联网圈,随便一个工作了三、五年的开发人员,都可以称之为架构师。

    美的让人心动
  • 面向对象的演进过程

    我们知道 程序 = 数据结构 + 算法,其中数据结构包括数组、栈、队列、链表、树以及图等,而算法是包含顺序、循环、分支三种逻辑结构的代码,为了使算法能够到处复用...

    木可大大
  • 面向对象的演进过程

    我们知道 程序 = 数据结构 + 算法,其中数据结构包括数组、栈、队列、链表、树以及图等,而算法是包含顺序、循环、分支三种逻辑结构的代码,为了使算法能够到处复用...

    木可大大
  • 用Matplotlib制作动画

    动画是呈现各种现象的有趣方式。在描述像过去几年的股票价格、过去十年的气候变化、季节性和趋势等时间序列数据时,与静态图相比,动画更能说明问题。因为,从动画中,我们...

    昱良
  • 花样试用微软语音服务晓晓

    受微软美女员工 Grace Peng 邀请(也可能是套路???),参加微软神经语音(没错,就是神经)晓晓的试用,首先是看到了群里面的消息,然后就是发送申请,等待...

    梁规晓

扫码关注云+社区

领取腾讯云代金券