Chap13 理解限界上下文

Eric Evans —— 细胞之所以存在,细胞膜限定了什么在细胞内,什么在细胞外,并确定了什么物质可以通过细胞膜。
上下文表现了业务流程的场景片段:上下文(Context)是动态的业务流程被边界(Bounded)静态切分的产物。
image.png
活动是分散的,目标也不相同,但在同一个上下文中,这些活动为同一个目标提供服务
关键点:

  • 知识:业务相关性。
  • 角色:参与到这个 Context 中的对象是什么角色,角色间如何协作。
  • 边界:限界上下文按照不同关注点进行分离,边界根据耦合关系强弱来确定(弱 -> 明确划定)。

限界上下文:根据业务相关性、耦合关系的强弱、分离的关注点对活动进行分类,找到不同类型间的边界。
上下文是业务目标,限界避免业务目标不单一而带来的混乱与概念不一致

限界上下文的价值

限界上下文着眼点 —— 对边界的控制
边界划定角度:

  • 领域逻辑:领域模型的业务边界 —— 模型完整性和一致性 —— 业务复杂度。
  • 团队合作:开发团队的工作边界 —— 有序合作、沟通 —— 管理复杂度。
  • 技术实现:系统架构的应用边界 —— 上下文间的集成方式 —— 技术复杂度。

限界上下文对不同边界的控制力:

  • 业务边界对领域模型的控制。
  • 工作边界对团队协作的控制。
  • 应用边界对技术风险的控制。

Bounded Context 不在于如何划分边界,而在于如何控制边界
Alberto Brandolini —— EventStorming —— “bounded context are a mean of safety“ —— being in control and no surprise。
系统架构和组织结构都是可控的。不够浪漫,但免于压力 。
Surprise leads to stress and stress leads to no learning, just hear work.
出乎意料的惊讶会导致压力,而压力就会使得团队疲于加班,缺少学习。 —— Alberto

自治单元

将限界上下文看做“自治单元”:最小完备、稳定空间、自我履行、独立进化。

最小完备

完备:职责完整,针对自己的信息不需要请求别的自治单元(免除不必要依赖)。
最小完备:限制范围,免于被添加不必要的职责。
根据业务价值的完整性进行设计。

自我履行

对外部请求做出符合自身利益的明智判断,是否应该履行该职责,由上线文拥有的信息来决定。
为避免风险,你要履行的职责一定是你掌握的知识范畴之内。

稳定空间

减少外界对内部影响。
自治单元拥有空间内的掌控权,保持空间私密性,开放空间接口应对外部的请求。
找到限界上下文间最薄弱的依赖处,分解上下文。
符合开发封闭原则(OCP) —— 对修改封闭,对扩展开放 —— 自治单元的封闭空间和开发空间。
封闭空间:隐藏细节;开放空间:开放抽象接口 => 稳定空间:封装变化。

独立进化

减少对外部的影响。
保证对外公开接口的稳定性 —— 良好的接口设计,符合标准规范,版本上考虑兼容与演化。

总结

最小完备是基础,赋予限界上下文足够的信息,保证自我履行。稳定空间与独立进化一个对内一个对外,应对变化,并通过最小完备和自我履行保障限界上下文受到变化的影响最小。
四要素体现高内聚低耦合,根据业务关注点和技术关注点,将强相关性的内容放到同一个限界上下文中,同时降低限界上下文之间的耦合。

限界上下文的控制力

控制边界。
限界上下文是连接问题域和解决方案的重要桥梁:

  • 统一语言的边界,领域模型的边界 => 界定问题域(Problem Space);
  • 系统架构,应用边界和技术边界 => 确定系统和各个上下文解决方案。

    分离业务边界

    让每一个限界上下文拥有自己的领域模型
    image.png
    限界上下文作为边界,当前所在的上下文作为概念语境,既保证了限界上下文之间的松散耦合,又能够维持限界上下文各自领域模型的一致性。

    明确工作边界

    亚马逊 —— 2PTs:Two-Pizza Teams —— 团队成员人数控制在 7~10 人左右 => 有效沟通。
    image.png
    将人与人之间的沟通视为一个“联结(link)”,则联结的数量遵守如下公式,其中 n 为团队的人数:
    联结的数量直接决定了沟通的成本,以 6 人团队来计算,联结的数量为 15。如果在原有六人团队的规模上翻倍,则联结数陡增至 66。对于传统项目管理而言,一个 50 人的团队其实是一个小型团队,根据该公式计算得出的联结数竟然达到了惊人的 1225。如下图所示,我们可以看到随着团队规模的扩大,联结数的增长以远超线性增长的速度发展,因而沟通的成本也将随之发生颠覆性的改变:
    image.png
    沟通成本增加 => 团队适应性下降。
    最佳的单节点(你可以想象成是通信网络中可以唯一定位的人或群体)联结数是一个比较小的值,它不太容易受网络规模的影响。即使网络变大,节点数量增加,每个节点所拥有的联结数量也一定保持着相对稳定的状态。 —— Jim Highsmith
    要做到人数增加不影响到联结数,就是要找到这个节点网络中的最佳沟通数量,也即前面提到的 2PTs 原则。
    然而团队规模并非解决问题。的唯一办法,如果在划分团队权责时出现问题,则团队成员的数量不过是一种组织行为的表象罢了。
    如果结合领域驱动设计的需求,则我们应该考虑在保持团队规模足够小的前提下,按照软件的特性(Feature)而非组件(Component)来组织软件开发团队,这就是所谓“特性团队”与“组件团队”之分。
    组件团队:专业技能与功能重用,应对技术复杂度的团队成员去解决那些公共型的基础型的问题。。
    特性团队:业务知识,客户价值,端对端的开发垂直细分领域的跨职能团队
    一个典型的由多个特性团队组成的大型开发团队如下图所示:
    image.png
    特性团队专注的领域特性,是与领域驱动设计中限界上下文对应的领域是相对应的。
    当我们确定了限界上下文时,其实也就等同于确定了特性团队的工作边界,确定了限界上下文之间的关系,也就意味着确定了特性团队之间的合作模式;反之亦然。
    之所以如此,则是因为康威定律(Conway’s Law)为我们提供了理论支持。

    康威定律

    定律:“任何组织在设计一套系统(广义概念上的系统)时,所交付的设计方案在结构上都与该组织的沟通结构保持一致”。
    在康威定律中起到关键杠杆作用的是沟通成本

职责分配的错位 —— 限界上下文与团队的阻抗不匹配。
将团队与限界上下文重合,降低沟通成本,打造高效的领域特性团队,专注于属于自己的限界上下文开发。

封装应用边界

技术复杂度 —— 技术实现 —— 做出对系统质量属性的响应和承诺。

高并发

将外卖订单业务从整个系统中剥离出来,作为一个单独的限界上下文对其进行设计,就可以从物理架构上保证它的独立性,在资源分配上做到高优先级地扩展,在针对领域进行设计时,尽可能地引入异步化与并行化,来提高服务的响应能力。

功能重用

账户管理并非系统的核心领域,但与账户相关的业务逻辑却相对复杂。从功能重用的角度考虑,我们应该将账户管理作为一个单独的限界上下文,以满足不同核心领域对这一功能的重用,避免了重复开发和重复代码。

实时性

为了保证这种在高并发情况下的实时性,我们就需要专门针对价格领域提供特定的技术方案,例如,通过读写分离、引入 Redis 缓存、异步数据同步等设计方法。此时,价格领域将作为一个独立的限界上下文,形成自己与众不同的架构方案,同时,为价格限界上下文提供专门的资源,并在服务设计上保证无状态,从而满足快速扩容的架构约束。

第三方服务

在技术实现上,一方面我们希望为支付服务的客户端提供完全统一的支付接口,以保证调用上的便利性与一致性,另一方面我们希望能解除第三方支付服务与电商系统内部模块之间的耦合,避免引起“供应商锁定(Vender Lock)”,也能更好地应对第三方支付服务的变化。因此,我们需要将这种集成划分为一个单独的限界上下文。

遗留系统

通过这个遗留系统限界上下文的边界保护,就可以避免我们在开发过程中陷入遗留系统庞大代码库的泥沼。由于新增需求与原有系统在业务上存在交叉功能,因而可能失去了部分代码的重用机会,却能让我们甩开遗留系统的束缚,放开双手运用领域驱动设计的思想建立自己的领域模型与架构。只有在需要调用遗留系统的时候,作为调用者站在遗留系统限界上下文之外,去思考我们需要的服务,然后酌情地考虑模型对象之间的转换以及服务接口的提取。

总结

划分应用边界 —— 形成了对技术实现的隔离 —— 避免技术方案互相干扰。

识别限界上下文

德雷福斯模型的 5 个阶段:新手、高级新手、胜任者、精通者和专家。
专家 —— 专家根据直觉工作(Experts work from intuition)。
经验的累积过程需要方法,否则所谓数年经验不过是相同的经验重复多次罢了,没有价值。
Andy Hunt 认为需要给新手提供某种形式的规则去参照,之后,高级新手会逐渐形成一些总体原则,然后通过系统思考和自我纠正,建立或者遵循一套体系方法,就能从高级新手慢慢成长为胜任者、精通者。
因此,从新手到专家是一个量变引起质变的过程,在没有能够养成直觉的经验之前,我们需要有一套方法。
image.png
总体原则 —— 系统思考和自我纠正 —— 体系方法。

一个可行方法

一个可行的识别限界上下文的过程方法:
通过从业务边界到工作边界再到应用边界这三个层次抽丝剥茧,分别以不同的视角、不同的角色协作来运用对应的设计原则。
帮助分析需求、识别风险、确定架构方案。
image.png

从业务边界识别限界上下文

开发团队与领域专家经过充分地沟通与交流,梳理出主要的业务流程,这些业务流程体现了各种参与者在这个过程中通过业务活动共同协作,最终完成具有业务价值的领域功能。
业务流程结合了参与角色(Who)、业务活动(What)和业务价值(Why)。
业务流程的基础上,我们就可以抽象出不同的业务场景,这些业务场景又由多个业务活动组成。
可以利用前面提到的领域场景分析方法剖析场景,以帮助我们识别业务活动,例如采用用例对场景进行分析,此时,一个业务活动实则就是一个用例。
例:
在针对一款文学阅读产品进行需求分析时,我们得到的业务流程为:

  • 登录读者根据作品名或者作者名查询自己感兴趣的作品。在找到自己希望阅读的作品后,开始阅读。若阅读的作品为长篇,可以按照章节阅读,倘若作品为收费作品,则读者需要支付相应的费用,支付成功后可以阅读购买后的作品。在阅读时,倘若读者看到自己喜欢的句子或段落,可以作标记,也可以撰写读书笔记,还可以将自己喜欢的内容分享给别的朋友。读者可以对该作品和作者发表评论,关注自己喜欢的作品和作者。
  • 注册用户可以申请成为驻站作者。审核通过的作者可以在创作平台上发布自己的作品,发布作品时,可以根据需要设置作品的章节。作者可以在发布作品之前预览作品,无论作品是否已经发布,都可以对作品的内容进行修改。作者可以设置自己的作品为收费或免费作品,并自行确定阅读作品所需的费用。如果是新作品发布,系统会发送消息通知该作者的关注者;若连载作品有新章节发布,系统会发送消息通知该作品的关注者。
  • 驻站作者可以为自己的作品建立作品读者群,读者可以申请加入该群,加入群的读者与作者可以在线实时聊天,也可以发送离线信息,或者将自己希望分享的内容发布到读者群中。注册用户之间可以发起一对一的私聊,也可以直接给注册用户发送私信。

通过对以上业务流程进行分析,结合在各个流程环节中需要的知识以及参与角色的不同,可以划分如下业务场景:

  • 阅读作品
  • 创作作品
  • 支付
  • 社交
  • 消息通知
  • 注册与登录

可以看到,业务流程是一个由多个用户角色参与的动态过程,而业务场景则是这些用户角色执行业务活动的静态上下文。从业务流程中抽象出来的业务场景可能是交叉重叠的,例如在读者阅读作品流程与作者创作流程中,都牵涉到支付场景的相关业务。
(业务流程:动态过程;业务场景:静态上下文)
利用领域场景分析的用例分析方法剖析这些场景,业务活动的描述应该精准地表达领域概念,且通过尽可能简洁的方式进行描述,通常格式为动宾形式。
以阅读作品场景为例,可以包括如下业务活动:

  • 查询作品
  • 收藏作品
  • 关注作者
  • 浏览作品目录
  • 阅读作品
  • 标记作品内容
  • 撰写读书笔记
  • 评价作品
  • 评价作者
  • 分享选中的作品内容
  • 分享作品链接
  • 购买作品

识别业务边界:语义相关性、功能相关性。

语义相关性

语义相关性主要来自于描述业务活动的宾语
例如,前述业务活动中的查询作品、收藏作品、分享作品、阅读作品都具有“作品”的语义,基于这一特征,我们可以考虑将这些业务活动归为同一类。
识别语义相关性的前提是准确地使用统一语言描述业务活动。在描述时,应尽量避免使用“管理(manage)”或“维护(maintain)”等过于抽象的词语。抽象的词语容易让我们忽视隐藏的领域语言,缺少对领域的精确表达。
例如,在文学阅读产品中,我们不能宽泛地写出“管理作品”、“管理作者”、“维护支付信息”等业务活动,而应该挖掘业务含义,只有如此才能得到诸如收藏作品、撰写作品、发布作品、设置作品收费模式、查询支付流水、对账等符合领域知识的描述。
当然,这里也有一个业务活动层次的问题。在进行业务分析时,若我们发现只能使用“管理”或“维护”之类的抽象字眼来表述该用户活动时,则说明我们选定的用户活动层次过高,应该继续细化。细化后的业务活动既能更好地表达领域知识,又能让我们更好地按照语义相关性去寻找业务的边界,可谓一举两得。
在进行语义相关性判断时,还需要注意业务活动之间可能存在不同的语义相关性。例如,在文学阅读产品中,查询作品、阅读作品与撰写作品具有“作品”的语义相关,而评价作品与评价作者又具有“评价”的语义相关,究竟应该以哪个语义为准呢?没有标准!我们只能按照相关性的耦合程度进行判断。如果我们将评价视为一个相对独立的限界上下文,则评价作品与评价作者放入评价上下文会更好。

功能相关性

从功能角度去分析业务活动是否彼此关联和依赖。
可以通过用例之间的关系来判别功能相关性,如用例的包含与扩展关系,其中包含关系展现了功能的强相关性。所谓“功能相关性”,指的就是职责的内聚性,强相关就等于高内聚。
故而从这个角度看,功能相关性的判断标准恰好符合“高内聚、松耦合”的设计原则。
例如:

  • 设置作品收费模式并非发布作品的前置约束条件,属于用例中的扩展关系,但由于二者还存在语义相关性,因而将其放入到同一个限界上下文中也是合理的。
  • 两个相关的功能未必一定属于同一个限界上下文。购买作品与支付购买费用是功能相关的,且前者依赖于后者,但后者从领域知识的角度判断,却应该分配给支付上下文,我们非但不能将其紧耦合在一起,还应该竭尽所能降低二者之间的耦合度。

    为业务边界命名

    无论是语义相关性还是功能相关性,都是分类业务活动的一种判断标准。
    对划定的业务边界进行命名 —— 识别所有业务活动共同特征 —— 以最准确地名词来表达该特征
    例如:建立读者群、加入读者群,发布群内消息、实时聊天、发送离线消息、一对一私聊与发送私信等业务活动 —— “社交”的共同特征 —— 得到社交上下文。
    当我们距离真正理解业务还有距离的时候,不妨先“草率”地规划它,待到一切都明朗起来,再寻机重构。

    从工作边界识别限界上下文

    工作分配的基础在于“尽可能降低沟通成本”,遵循康威定律,沟通其实就是项目模块之间的依赖,这个过程同样不是一蹴而就的。康威认为:
    在大多数情况下,最先产生的设计都不是最完美的,主导的系统设计理念可能需要更改。因此,组织的灵活性对于有效的设计有着举足轻重的作用,必须找到可以鼓励设计经理保持他们的组织精简与灵活的方法。
    特性团队正是用来解决这一问题的。
    高效团队:

  • 共同的目标

  • 团队的边界

Jurgen Appelo —— 边界:

  • 团队成员应对团队的边界形成共识,这就意味着团队成员需要了解自己负责的限界上下文边界,以及该限界上下文如何与外部的资源以及其他限界上下文进行通信。
  • 团队的边界不能太封闭(拒绝外部输入),也不能太开放(失去内聚力),即所谓的“渗透性边界”,这种渗透性边界恰恰与“高内聚、松耦合”的设计原则完全契合。

针对这种“渗透性边界”,团队成员需要对自己负责开发的需求“抱有成见”,在识别限界上下文时,“任劳任怨”的好员工并不是真正的好员工。一个好的员工明确地知道团队的职责边界,他应该学会勇于承担属于团队边界内的需求开发任务,也要敢于推辞职责范围之外强加于他的需求。通过团队每个人的主观能动,就可以渐渐地形成在组织结构上的“自治单元”,进而催生出架构设计上的“自治单元”。同理,“任劳任怨”的好团队也不是真正的好团队,团队对自己的边界已经达成了共识,为什么还要违背这个共识去承接不属于自己边界内的工作呢?这并非团队之间的“恶性竞争”,也不是工作上的互相推诿;恰恰相反,这实际上是一种良好的合作,表面上维持了自己的利益,然而在一个组织下,如果每个团队都以这种方式维持自我利益,反而会形成一种“互利主义”。
这种“你给我搔背,我也替你抓抓痒”的互利主义最终会形成团队之间的良好协作。如果团队领导者与团队成员能够充分认识到这一点,就可以从团队层面思考限界上下文。此时,限界上下文就不仅仅是架构师局限于一孔之见去完成甄别,而是每个团队成员自发组织的内在驱动力。当每个人都在思考这项工作该不该我做时,变相地就是在思考职责的分配是否合理,限界上下文的划分是否合理。

从应用边界识别限界上下文

质量属性

如果把关乎质量属性的问题都视为在将来可能会发生,其实就是“风险(Risk)”。
Martin Fowler —— 架构是重要的东西,是不容易改变的决策 —— 改变可能是灾难性的。
限界上下文的边界 —— 可以将这种风险带来的影响控制在一个极小的范围,这也是前面提及的安全

重用和变化

限界上下文 —— 自治单元 —— 逻辑重用和封装变化。
Eric Evans —— 共享内核其实就是重用的体现,而开放主机服务与防腐层则是对变化的主动/被动应对。
限界上下文对变化的应对,其实是“单一职责原则”的体现,即一个限界上下文不应该存在两个引起它变化的原因
例如:

  1. 引起库存变化的原因只有库存业务单据。
  2. 企业征税政策和运费计算规则变化都会引起财务上下文的变化,将运费计算独立成限界上下文,可以独自演化。

    遗留系统

    领域驱动设计建议的通常做法是将整个遗留系统视为一个限界上下文。
    遗留系统 —— 知识的缺乏 —— 一个还在运行和使用,但已步入软件生命衰老期的缺乏足够知识的软件系统
    借鉴技术栈迁移中常常运用的“抽象分支(Branch By Abstraction)”手法。该手法会站在消费者(Consumer)一方观察遗留系统,找到需要替换的单元(组件);然后对该组件进行抽象,从而将消费者与遗留系统中的实现解耦。最后,提供一个完全新的组件实现,在保留抽象层接口不变的情况下替换掉遗留系统的旧组件,达到技术栈迁移的目的:
    上图所示的抽象层,本质就是后面我们要提到的“防腐层(Anticorruption Layer)”,通过引入这么一个间接层来隔离与遗留系统之间的耦合。这个防腐层往往是作为下游限界上下文的一部分存在。若有必要,也可以单独为其创建一个独立的限界上下文。

    设计驱动力

    结合业务边界、工作边界和应用边界,形成一种层层推进的设计驱动力,可以让我们对限界上下文的设计变得更加准确,边界的控制变得更加合理。
    对限界上下文的准确性心存疑虑,比较实际的做法是保持限界上下文一定的粗粒度
    功能的边界不好把握分寸,可以考虑将这些模棱两可的功能放在同一个限界上下文中
  • 限界上下文变得越来越庞大,以至于一个 2PTs 团队无法完成交付目标;
  • 限界上下文的功能各有不同的质量属性要求;
  • 因为重用或变化,使得我们能够更清楚地看到分解的必要性;

此时我们再对该限界上下文进行分解,就会更加有把握。这是设计的实证主义态度。