
2.6 总结与拓展
本章节主要阐述了DDD中的重要概念:有界上下文和子域。它们应该是领域建模开始的第一步,也是最难的步骤,需要创新思维,因此发挥群策群力的作用,通过头脑风暴会议发挥每个人的领域特长,类似盲人摸象,有摸到鼻子的,摸到耳朵的,摸到尾巴的,和摸到身体的,虽然每个人的认识都是片面的,但是可以说属于每个有界上下文,可以在自己的有界上下文中对大象鼻子、耳朵和尾巴等进行建模。
Eric Evans在2019年9月的Explore DDD主题演讲中,邀请大家积极参与改进DDD语言。他承认,DDD中使用的一些基本术语行话,如有界上下文会引起误解,他欢迎所有人不同意他的意见,只有通过一个活跃的社区,在富有争议的辩论中,才能真正实现DDD成为一个真实、生动的思想体系的目标。
有界上下文是一种基于一致语言模型划分软件的模式,在有界上下文中,通过业务专家和软件人员之间的对话创建共享的统一语言,这是专注于一种简洁描述领域内情况的语言。创建几个有界上下文,而不是统一大全的语言规范,这样每个上下文都有自己特定的语言和模型。
DDD是有关语言的建模技术,需要对语言有一定敏感性,有一定文科背景再结合逻辑推理能力则非常适合。DDD需要对语言进行主谓宾分类,然后捕捉其中的谓语动词或行为,将这些发生的动作抽象为命令模型,将发生过的事实抽象为领域事件模型,用领域事件替代状态分析,例如音乐播放器有三种状态:停止、播放、暂停,这是从状态名词角度分析的,而从领域事件角度分析则有:已停止、已播放和已暂停。当然中文中也常说“已停止状态”,将事件和状态混合在一起,其实两者可以互相替代,只是动词和名词的区别。
在通常界面设计中也会出现“状态、命令和事件”的情况:界面上有一个按钮显示“停止”,按一下出现“播放”,这表示两个状态,当按一下时,其实是发出“播放”命令,当听到音乐以后,其实系统处于已播放状态,因此按钮显示“播放”状态,这时系统内部会将已播放这个领域事件发送给其他感兴趣的上下文。当然有时会疑惑,按钮显示“停止”时,是让发出“停止”命令,还是表示现在是“停止”状态呢?这是中文的尴尬之处,也可能反映了一个侧面现象:可能在中文语言环境中长大的人对动词或名词区别的敏感性不够高,那么会带来根据动词建模的难度。
领域事件的捕获不但有助于区别有界上下文,抓住核心子域,集中精兵强将突破复杂的问题空间,而且有助于划分团队,有几个有界上下文就划分几个团队,在这个基础上再应用微服务架构去实现,一切显得水到渠成,这也称为一种“社会技术架构”。
在有界的上下文中,当团队成员开发软件时,一个团队可以获得其模型的所有权并增加其自主权,他们可以单独测试,因为团队可以清楚地了解他们的客户是谁,并且可以接收他们自己的反馈指标。
Nicole Forsgren博士等人的研究表明,持续交付绩效和成功组织扩展的最强预测因素是松耦合的团队,这些团队通过松耦合的软件架构实现。如果想要提速软件交付,那么就根据有界上下文划分微服务、组织团队,这就使得有界上下文成为一个非常重要的模式。这也从侧面验证了康威定理:技术架构是组织架构的反映。
虽然使用有界上下文在DDD中划分模型可以提高自主权,但团队并非拥有完全的自主权,系统和团队之间总会有耦合。只有通过设计清晰的边界,并通过决定事物在边界之间移动的位置和方式(领域事件的作用),才能创造自主权。
定义明确边界的第一步是获得问题空间中的领域知识,如果团队缺乏对领域的了解或对领域有不同的理解,那么为企业设计有界上下文是很困难的。如果缺乏这种知识,IT团队将无法构建合适的业务软件,最终客户将无法获得他们真正需要的东西。
作为为这些问题开发解决方案的软件工程师,必须充分了解问题空间以构建正确的解决方案,必须亲自了解问题的知识。更重要的是,必须持续地了解问题空间。虽然问题空间通常是稳定的,但是当获得新的见解时它会发生变化(需求变化了)。
这些见解可以来自业务方面或来自软件工程师,这是一项集体努力,这一事实构成了DDD中的最大问题。在大多数企业中,软件团队被视为工厂。只要客户为团队提供功能性设计,他们将提供相应的软件解决方案。事实并非如此,因为正如Alberto Brandolini所认为的那样:“不是领域专家知识进入了生产中的软件,而是开发人员自己的见解。”
这些见解散落在各种文档之中,它们最大的问题是可能过时,跟不上需求的变化,没有人或无法及时更新文档,此时需要面对面的协作建模,使用事件风暴进行可视化会议。
学习很复杂,没有一种统一的学习方法,然而,大脑科学展示了大脑如何更有效地学习。Sharon Bowman在她的在线文章《大脑科学训练中的六张牌》中阐述了以下人类学习特征:
运动胜过坐着。
说话胜过听取。
图像胜过言语。
写作胜过阅读。
更短胜过更长。
不同胜过相同。
当然,这是适合外国人的一套方式,不一定适合中国人,但是在召开事件风暴会议时,需要尽情表达自己的观点,阐述自己的主观认识。世界是在语言等符号中表达出来的,真实的客观世界如果不使用语言、形式逻辑等符号去表示,就无从认识,只能用一个词表达:那个客观世界,所以,维根斯坦说:语言的边界就是思维的边界,当无法用语言表达时,就到达了认识世界的最大边界了。
每个人都说话是不是很混乱?求同存异,将相同的语言模型放在同一个有界上下文中,差异就使用不同的有界上下文来表示。
事件风暴发明者Alberto Brandolini说:事件风暴让处于信息孤岛中的人们发现彼此的有限性,打开了潘多拉盒子。可靠明确的需求其实只是一种幻觉,实际中是找不到明确的需求的,这也是一种危险的幻觉。大多数需求可能是自相矛盾的,具有组织中孤岛现象形成的不同观点和需求,更不用说可能隐藏了个人目标了。描述同一个概念的术语在语言表达上会有不同的表示,Eric Evans认为应该将关注焦点转移到人们的沟通语言上,而事件风暴会议是进行语言沟通最好的方式之一。
事件风暴目前在欧洲非常流行,DDD思想很受欧洲人的追捧,可能和欧洲人深邃的哲学传统文化有关。事件风暴虽然不是DDD原作者Evans提出的,但是整个DDD社区共同努力的结果。
事件风暴有利于划分有界上下文,而有界上下文对于微服务架构实现是至关重要的,DDD的二次复兴也是由于微服务的兴起而引起的,无论微服务还是微服务反对的单体monolith(巨大)架构,通常都会有两种状态,如图2-21所示。

图2-21 微服务架构的四种状态
无论微服务架构,还是传统的单体架构,关键是在这些架构内部实现清晰的层次划分和单向依赖关系,而不是如泥球一样混为一团,依赖复杂得如蜘蛛网缠绕般,如果不解决这种混乱的泥球现象,即使强行使用微服务架构,也会造成一个分布式的混乱泥球,加重系统实施和运维的负担,哪怕采取DevOps“谁开发谁运维”的哲学宗旨,如果没有一种好的业务划分方法,“谁开发谁运维”就会变成“谁开发谁遭罪”的情况。有界上下文的划分可以帮助传统单体架构实现很好地模块化,也可以帮助微服务架构实现真正的微服务划分,减轻和降低分布式系统开发与运维的难度。
微服务是依据子域或有界上下文而划分的,然后设立专门的微服务团队负责该有界上下文,例如亚马逊电商系统中的购物车功能是由一个专门的微服务团队负责的。但是认为“微服务就是有界上下文”是一种过分简化。微服务系统中存在四种有界上下文类型。
1)服务内部(的上下文)。
2)服务的API(的上下文)。
3)服务集群(的上下文)。
4)服务之间的交互(的上下文)。
“一个微服务是一个有界上下文”通常针对第一种类型,有界上下文的边界等同于一个微服务的内部业务边界,但是一个微服务不只是调用业务逻辑(注意是调用,而不是包含,微服务只是一个协调者,不是业务决策者),还要调用基础设施的一些服务。一个微服务可能类似应用层的应用服务,需要综合调用领域服务和基础设施服务,在它们之上做些协调职责,最终为不同客户端提供服务,因此,微服务还有服务API方面的技术细节,包括使用REST还是gRPC作为API接口。微服务之间的通信涉及一些不同有界上下文的命令与事件转换;微服务的集群或分布式事务流程的实现等都可能造成与核心子域不同的子域划分。
在实际项目中,需要不断解决因为有界上下文、子域和组织的需要共同对齐而引起的混乱。子域属于领域的问题空间边界,组织或人会针对问题空间提出解决方案,有界上下文属于解决方案空间内,问题、组织或人和解决方案这三个空间的概念如何映射对应?这已经不只是技术或方法论问题,而是管理问题。例如一个微服务涉及几个不同上下文,包括核心子域对应的上下文、服务API或服务集群交互的上下文,是一个上下文对应一个小组,还是一个微服务对应一个小组呢?
又例如大型公司以重组而闻名,导致业务流程和职责的变化。当这些重组发生时,软件不会以与重组前相同的方式进行更改,因此功能的管理方式变得不清楚。以前一个小组的职责现在可能需要两个小组协作才能实现,这时候必须动态地保持有界上下文、子域和组织的对应对齐,这些对组织的管理都提出了新课题。
DDD实现需要综合敏捷和瀑布法等优点。敏捷是一种适合中小型项目的好方法,但是确实不适合大型项目,而DDD实现又不同于传统针对大项目的瀑布法DDD在大项目开始的设计阶段只是召开几次事件风暴会议,将有界上下文或关键领域事件发现并建模,然后通过有界上下文、子域和团队对齐的方法,将大项目划分为一个个小项目,在小项目团队或微服务团队内部再进行DDD聚合等详细战术设计,这样就不至于整个项目团队在开始阶段一直处于等待建模设计完成的状态,也能像敏捷那样及时反馈,及时出成果,不断探索,整个团队都是处于分工建模设计、编码、测试和交付的不断循环阶段。
最重要的是,由于重视了设计,会相应减轻测试的工作量。传统敏捷实现过程中,大量负担压在了测试环节,而测试人员的素质和报酬是有限的,他们其实无法承担架构级别的设计职责,可能也只是盲人摸象般地测试,很难实现全方位无死角的全面测试,这可能是TDD(测试驱动开发)的痛点吧。
同时,传统敏捷方式也将压力传导到重构阶段,当大型项目准备进入重构阶段时,往往发现难以重构,不如重写,因为重构的工作量太大了,那么是不是可以在平时的冲刺过程中实现重构呢?由于过于注重功能实现,无法对业务深入理解,实际上就无法重构,例如服务中散布了大量业务逻辑,它们与SQL语句一起协调,共同完成业务功能。为什么程序员不将业务逻辑放入DDD领域模型中呢?因为没有发现领域模型。领域模型不像服务那样具有强制性,本身好像是可有可无的,如果程序员没有DDD知识,他就可能无法发现DDD模型。在重构阶段也是如此,程序员如果没有对领域知识的进一步理解,没有面向对象分析设计的方法论,他是无法发现哪里可以重构的,只是感觉新功能难以修改,代码如同糨糊一样混乱搅拌在一起,他无法获得解决这种混乱的方法,而解决这种混乱的方法需要两种技能:分析划分的方法论和对领域知识的深入了解。这两种技能缺一不可,只知道划分的方法论,就不知道从何下手,而只掌握领域知识,如各种领域专家或产品经理,将不知道如何分析划分,只是一股脑儿地全部告诉技术人员,或者像量子计算机那样混沌地时不时吐露出一些分析结果,这些零碎分析结果在逻辑上是否自洽,这些都没有经过严谨的论证和模拟。
这些问题都可以通过事件风暴会议等DDD建模方法解决。DDD设计划分为战略设计和战术设计,正是有综合敏捷和瀑布两种方法的考量。在战略设计阶段,实际是一直使用瀑布法,通过头脑风暴会议,组织内部孤岛的信息得到汇总碰撞和逻辑验证,有界上下文、领域事件作为分析划分的结果,团队依此划分实现分工,各个小组进入DDD战术设计阶段;聚合、实体和值对象在各个小组内实现详细设计,小组之间的衔接,也就是有界上下文或子域之间的衔接是一种上下文映射,可以共享核心(不推荐),也可以采取发布/订阅模式(能利用领域事件),或者同步的开放API方式(简单,但无法利用战略设计阶段的领域事件)。
下一章开始进入DDD的战术设计,将分别阐述聚合、实体、值对象等DDD领域模型概念。