5

《解构领域驱动设计》第二章

 3 years ago
source link: http://zhangyi.xyz/chapter-2-of-ddde/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

《解构领域驱动设计》第二章

发表于

2021-09-08 分类于 Writing

阅读次数: 1 Valine: 0 本文字数: 15k 阅读时长 ≈ 13 分钟

ddde-cover.jpg

购买《解构领域驱动设计》,请扫描上图二维码在京东购买。

软件的核心是其为用户解决领域相关的问题的能力。 所有其他特性,不管有多么重要,都要服务于这个基本目的。

——Eric Evans,《领域驱动设计》


应对复杂度的挑战,或许是构建软件的过程中唯一亘古不变的主题。为了更好地应对软件复杂度,许多顶尖的软件设计人员与开发人员纷纷结合实践提出自己的真知灼见,既包括编程思想、设计原则、模式语言、过程方法和管理理论,又包括对编程利器自身的打磨。毫无疑问,通过这些真知灼见,软件领域的先行者已经改变或正在改变我们构建软件的方法、过程和目标,我们欣喜地看到了软件的构建正在向着好的方向改变。然而,整个客观世界的所有现象都存在诸如黑与白、阴与阳、亮与暗的相对性,任何技术的发展都不是单向的。随着技术日新月异向前发展,软件系统的复杂度也日益增长。中国有一句古谚:“道高一尺,魔高一丈。”又有谚语:“魔高一尺,道高一丈。”究竟是道高还是魔高,就看你是站在“道”的一方,还是“魔”的一方。

在构建软件的场景中,软件复杂度显然就是“魔”,控制软件复杂度的方法则是“道”。在软件构建领域,“道”虽非虚无缥缈的玄幻叙述,却也不是绑定在具象之上的具体手段。软件复杂度的应对之道提供了一些基本法则,这些基本法则可以说放之四海而皆准,其中一条基本法则就是:能够控制软件复杂度的,只能是设计(指广泛意义上的设计)方法。因为我们无法改变客观存在的问题空间(参见2.1.2节对问题空间和解空间的阐释),却可以改变设计的质量,让好的设计为控制复杂度创造更多的机会。如果我们将软件系统限制在业务软件系统之上,又可得到另外一条基本法则:“要想克服”(业务系统的)复杂度,就需要非常严格地使用领域逻辑设计方法。在近20年的时间内,一种有效的领域逻辑设计方法就是Eric Evans提出的领域驱动设计(domain-driven design)。

Eric Evans通过他在2003年出版的经典著作《领域驱动设计》(Domain-Driven Design: Tackling Complexity in the Heart of Software)全方位地介绍了这一设计方法,该书的副标题旗帜鲜明地指出该方法为“软件核心复杂性应对之道”。

领域驱动设计究竟是怎样应对软件复杂度的?作为一种将“领域”放在核心地位的设计方法,其名称足以说明它应对复杂度的态度。用Eric Evans自己的话来说:“领域驱动设计是一种思维方式,也是一组优先任务,它旨在加速那些必须处理复杂领域的软件项目的开发。为了实现这个目标,本书给出了一套完整的设计实践、技术和原则。”

结合我们通过理解能力和预测能力两个维度对软件系统复杂度成因的剖析,确定了影响复杂度的3个要素:规模、结构与变化。控制复杂度的着力点就在这3个要素之上!领域驱动设计对软件复杂度的应对,是引入了一套提炼为模式的设计元模型,对业务软件系统做到了对规模的控制、结构的清晰化以及对变化的响应。

要深刻体会领域驱动设计是如何控制软件复杂度的,还需要整体了解Eric Evans建立的这一套完整的软件设计方法体系,包括该方法体系提出的设计概念与设计过程。

2.1 领域驱动设计的基本概念

领域驱动设计作为一个针对大型复杂业务系统的领域建模方法体系(不仅限于面向对象的领域建模),它改变了传统软件开发工程师针对数据库建模的方式,通过面向领域的思维方式,将要解决的业务概念和业务规则等内容提炼为领域知识,然后借由不同的建模范式将这些领域知识抽象为能够反映真实世界的领域模型。

Eric Evans之所以提出这套方法体系,并非刻意地另辟蹊径,创造出与众不同的设计方法与模式,而是希望恢复业务系统设计核心关注点的本来面貌,也就是认识到领域建模和设计的重要性,然而在当时看来,这却是全新的知识提炼。正如他自己所云:“至少20年前[1],一些顶尖的软件设计人员就已经认识到领域建模和设计的重要性,但令人惊讶的是,这么长时间以来几乎没有人写出点儿什么,告诉大家应该做哪些工作或如何去做……本书为做出设计决策提供了一个框架,并且为讨论领域设计提供了一个技术词汇库。”这里提到的“技术词汇库”就是我提到的设计元模型。

2.1.1 领域驱动设计元模型

领域驱动设计元模型是以模式的形式呈现在大家眼前的,由诸多松散的模式构成,这些模式在领域驱动设计中的关系如图2-1所示。

领域驱动设计的核心是模型驱动设计,而模型驱动设计的核心又是领域模型,领域模型必须在统一语言(参见第4章)的指导下获得。为整个业务系统建立的领域模型要么属于核心子领域(参见第6章),要么属于通用子领域[2]。之所以区分子领域,一方面是为了将一个不易解决的庞大问题切割为团队可以掌控的若干小问题,达到各个击破的目的,另一方面也是为了更好地实现资产(人力资产与财力资产)的合理分配。

为了保证定义的领域模型在不同上下文表达各自的知识语境,需要引入限界上下文(参见第9章)来确定业务能力的自治边界,并考虑通过持续集成来维护模型的统一。上下文映射(参见第10章)清晰地表达了多个限界上下文之间的协作关系。根据协作方式的不同,可以将上下文映射分为如下8种模式[3]

  1. 客户方/供应方;

  2. 共享内核;

  3. 分离方式;

  4. 开放主机服务;

  5. 发布语言;

clip_image001.png

图2-1 领域驱动设计元模型

模型驱动设计可以在限界上下文的边界内部进行,它通过分层架构(layered architecture)将领域独立出来,并在统一语言的指导下,通过与领域专家的协作获得领域模型。表示领域模型的设计要素(参见第15章)包括实体(entity)、值对象(value object)、领域服务(domain service)和领域事件(domain event)。领域逻辑都应该封装在这些对象中。这一严格的设计原则可以避免领域逻辑泄露到领域层之外,导致技术实现与领域逻辑的混淆。

聚合(aggregate)(参见第15章)是一种边界,它可以封装一到多个实体与值对象,并维持该边界范围之内的业务完整性。聚合至少包含一个实体,且只有实体才能作为聚合根(aggregate root)。工厂(factory)和资源库(repository)(参见第17章)负责管理聚合的生命周期。前者负责聚合的创建,用于封装复杂或者可能变化的创建逻辑;后者负责从存放资源的位置(数据库、内存或者其他Web资源)获取、添加、删除或者修改聚合。

2.1.2 问题空间和解空间

哲学家常常会围绕真实世界和理念世界的映射关系探索人类生存的意义,即所谓“两个世界”的哲学思考。软件世界也可一分为二,分为构成描述需求问题的真实世界与获取解决方案的理念世界。整个软件构建的过程,就是从真实世界映射到理念世界的过程。

如果真实世界是复杂的,在映射为理念世界的过程中,就会不断受到复杂度的干扰。根据Allen Newell和Herbert Simon的问题空间理论:“人类是通过在问题空间(problem space)中寻找解决方案来解决问题的”,构建软件(世界)也就是从真实世界中的问题空间寻找解决方案,将其映射为理念世界的解空间(solution space)来满足问题空间的需求。因此,软件系统的构建实则是对问题空间的求解,以获得构成解空间的设计方案,如图2-2所示。

clip_image003.png

图2-2 从问题空间到解空间

为什么要在软件构建过程中引入问题空间和解空间?

实际上,随着IT技术的发展,软件系统正是在这两个方向不断发展和变化的。在问题空间,我们要解决的问题越来越棘手,空间规模越来越大,因为随着软件技术的发展,许多原本由人来处理的线下流程慢慢被自动化操作所替代,人机交互的方式发生了翻天覆地的变化,IT化的范围变得更加宽广,涉及的领域也越来越多。问题空间的难度与规模直接决定了软件系统的复杂度。

针对软件系统提出的问题,解决方案的推陈出新自然毋庸讳言,无论是技术、工具,还是设计思想与模式,都有了很大变化。解决方案不是从石头里蹦出来的,而必然是为了解决问题而生的。面对错综复杂的问题,解决方案自然也需要灵活变化。软件开发技术的发展是伴随着复用性和扩展性发展的。倘若问题存在相似性,解决方案就有复用的可能。通过抽象寻找到不同问题的共性时,相同的解决方案也可以运用到不同的问题中。同时,解决方案还需要响应问题的变化,能在变化发生时以最小的修改成本满足需求,同时保障解决方案的新鲜度。无疑,构成解空间的解决方案不仅要解决问题,还要控制软件系统的复杂度。

问题空间需要解空间来应对,解空间自然也不可脱离问题空间而单独存在。对于客户提出的需求,要分清楚什么是问题,什么是解决方案,真正的需求才可能浮现出来。在看清了问题的真相之后,我们才能有据可依地寻找真正能解决问题的解决方案。软件构建过程中的需求分析,实际就是对问题空间的定位与探索。如果在问题空间还是一团迷雾的时候就贸然开始设计,带来的灾难性结果是可想而知的。徐锋认为,“要做好软件需求工作,业务驱动需求思想是核心。传统的需求分析是站在技术视角展开的,关注的是‘方案级需求’;而业务驱动的需求思想则是站在用户视角展开的,关注的是‘问题级需求’。”

怎么区分方案级需求和问题级需求?方案级需求就好比一个病人到医院看病,不管病情就直接让医生开阿司匹林,而问题级需求则是向医生描述自己身体的症状。病情是医生要解决的问题,处方是医生提供的解决方案。

那种站在技术视角展开的需求分析,实际就是没有明确问题空间与解空间的界限。在针对问题空间求解时,必须映射于问题空间定义的问题,如此才能遵循恰如其分的设计原则,在问题空间的上下文约束下寻找合理的解决方案。

领域驱动设计为问题空间与解空间提供了不同的设计元模型。对于问题空间,强调运用统一语言来描述需求问题,利用核心子领域通用子领域支撑子领域来分解问题空间,如此就可以“揭示什么是重要的以及在何处付出努力”。除去统一语言与子领域,其余设计元模型都将运用于解空间,指导解决方案围绕着“领域”这一核心开展业务系统的战略设计与战术设计。

2.1.3 战略设计和战术设计

对于一个复杂度高的业务系统,过于辽阔的问题空间使得我们无法在深入细节的同时把握系统的全景。既然软件构建的过程就是对问题空间求解的过程,那么面对太多太大的问题,就无法奢求一步求解,需要根据问题的层次进行分解。不同层次的求解目标并不相同:为了把握系统的全景,就需要从宏观层次分析和探索问题空间,获得对等于软件架构的战略设计原则;为了深入业务的细节,则需要从微观层次开展建模活动,并在战略设计原则的指导下做出战术设计决策。这就是领域驱动设计的两个阶段:战略设计阶段和战术设计阶段。

战略设计阶段要从以下两个方面来考量。

  • 问题空间:对问题空间进行合理分解,识别出核心子领域、通用子领域和支撑子领域,并确定各个子领域的目标、边界和建模策略。

  • 解空间:对问题空间进行解决方案的架构映射,通过划分限界上下文,为统一语言提供知识语境,并在其边界内维护领域模型的统一。每个限界上下文的内部有着自己的架构,限界上下文之间的协作关系则通过上下文映射来体现和表达。

子领域的边界明确了问题空间中领域的优先级,限界上下文的边界则确保了领域建模的最大自由度。这也是战略设计在分治上起到的效用。当我们在战略层次从问题空间映射到解空间时,子领域也将映射到限界上下文,即可根据子领域的类型为限界上下文选择不同的建模方式。例如为处于核心子领域的限界上下文选择领域模型(domain model)模式,为处于支撑子领域(supporting sub domain)的限界上下文选择事务脚本(transaction script)模式,这样就可以灵活地平衡开发成本与开发质量。

战术设计阶段需要在限界上下文内部开展领域建模,前提是你为限界上下文选择了领域模型模式。在限界上下文内部,需要通过分层架构将领域独立出来,在排除技术实现的干扰下,通过与领域专家的协作在统一语言的指导下逐步获得领域模型。

战术设计阶段最重要的设计元模型是聚合模式。虽然聚合是实体和值对象的概念边界,然而在获得了清晰表达领域知识的领域模型后,我们可以将聚合视为表达领域逻辑的最小设计单元。如果领域行为是无状态的,或者需要多个聚合的协作,又或者需要访问外部资源,则应该将它分配给领域服务。至于领域事件,则主要用于表达领域对象状态的迁移,也可以通过事件来实现聚合乃至限界上下文之间的状态通知。

战略设计与战术设计并非割裂的两个阶段,而是模型驱动设计过程在不同阶段展现出来的不同视图。战略设计指导着战术设计,这就等同于设计原则指导着设计决策。Eric Evans就明确指出,“战略设计原则必须把模型的重点放在捕获系统的概念核心,也就是系统的‘远景’上。”当一个业务系统的规模变得越来越庞大时,战略设计高屋建瓴地通过限界上下文规划了整个系统的架构。只要维护好限界上下文的边界,管理好限界上下文之间的协作关系,限制在该边界内开展的战术设计所要面对的就是一个复杂度得到大幅降低的小型业务系统。

人们常以“只见树木,不见森林”来形容一个人不具备高瞻远瞩的战略眼光,然而,若是“只见森林,不见树木”,也未见得是一个褒扬的好词语,它往往可以形容一个人好高骛远,不愿意脚踏实地将战略方案彻底落地。无论战略的规划多么完美,到了战术设计的实际执行阶段,团队在开展对领域的深层次理解时,总会发现之前被遗漏的领域概念,并经过不断的沟通与协作,“碰撞”出对领域的新的理解。对领域概念的新发现与完善除了能帮助我们将领域模型突破到深层模型,还可能促进我们提出对战略设计的修改与调整,其中就包括对限界上下文边界的调整,从而使战略设计与战术设计保持统一。

从战略设计到战术设计是一个自顶向下的设计过程,体现为设计原则对设计决策的指导;将战术设计方案反馈给战略设计,则是自底向上的演化过程,体现为对领域概念的重构引起对战略架构的重构。二者形成不断演化、螺旋上升的设计循环。

2.1.4 领域模型驱动设计

领域驱动设计是一种思维方式,而模型驱动设计则是领域驱动设计的一种设计元模型。因此,模型驱动设计必须在领域驱动设计思维方式的指导下进行,那就是面向领域的模型驱动设计,或者更加准确地将其描述为领域模型驱动设计

领域模型驱动设计通过单一的领域模型同时满足分析建模、设计建模和实现建模的需要,从而将分析、设计和编码实现糅合在一个整体阶段中,避免彼此的分离造成知识传递带来的知识流失和偏差。它树立了一种关键意识,就是开发团队在针对领域逻辑进行分析、设计和编码实现时,都在进行领域建模,产生的输出无论是文档、设计图还是代码,都是组成领域模型的一部分。Eric Evans将那些参与模型驱动设计过程并进行领域建模的人员称为“亲身实践的建模者”(hands-on modeler)。

模型驱动设计主要在战术阶段进行,换言之,整个领域建模的工作是在限界上下文的边界约束下进行的,统一语言的知识语境会对领域模型产生影响,至少,建模人员不用考虑在整个系统范围下领域概念是否存在冲突,是否带来歧义。由于限界上下文拥有自己的内部架构,一旦领域模型牵涉到跨限界上下文之间的协作,就需要遵循限界上下文与上下文映射的架构约束了。

既然模型驱动设计是面向领域的,就必须明确以下两个关键原则。

  • 以领域为建模驱动力:在建模过程中,针对领域知识提炼抽象的领域模型,并不断针对领域模型进行深化与突破,直到最终以代码来表达领域模型。

  • 排除技术因素的干扰:领域建模与技术实现的关注点分离有助于保证领域模型的纯粹性,也能避免混淆领域概念和其他只与技术相关的概念。

模型驱动设计不能一蹴而就。毕竟,即使通过限界上下文降低了业务复杂度,对领域知识的理解是一个渐进的过程。在这个过程中,开发团队需要和领域专家紧密协作,共同研究领域知识。在获得领域模型之后,也要及时验证,确认领域模型有没有真实表达领域知识。一旦发现遗漏或失真的现象,就需要重构领域模型。首先建立领域模型,然后重构领域模型,进而精炼领域模型,保证领域概念被直观而真实地表达为简单清晰的领域模型。显然,在战术设计阶段,模型驱动设计也应该是一个演进的不断完善的螺旋上升的循环过程。

2.2 领域驱动设计过程

领域驱动设计过程是一条若隐若现的由许多点构成的设计轨迹,这些点就是领域驱动设计的设计元模型。如果我们从问题空间到解空间,从战略设计到战术设计寻找到对应的设计元模型,分别“点亮”它们,那么这条设计轨迹就会如图2-3那样格外清晰地呈现在我们眼前。

领域驱动设计的过程几乎贯穿了整个软件构建的生命周期,包括对业务需求的探索和分析,系统的架构和设计,以及编码实现、测试和重构。面对客户的业务需求,由领域专家与开发团队展开充分的交流,经过需求分析与知识提炼,获得清晰明确的问题空间,并从问题空间的业务需求中提炼出统一语言,然后利用子领域分解问题空间,根据价值高低确定核心子领域通用子领域支撑子领域

通过对问题空间开展战略层次的求解,获得限界上下文形成解空间的主要支撑元素。识别限界上下文的基础来自问题空间的业务需求,遵循“高内聚松耦合”的原则划分领域知识的边界,再通过上下文映射管理它们之间的关系。每个限界上下文都是一个相对独立的“自治王国”,可以根据限界上下文是否属于核心子领域来选择内部的架构。通常,需要通过分层架构将限界上下文内部的领域隔离出来,进入战术设计阶段,进行面向领域的模型驱动设计。

clip_image005.png

图2-3 领域驱动设计过程

选定一个限界上下文,在统一语言的指导下,针对该上下文内部的领域知识开展领域模型驱动设计。首先进行领域分析,提炼领域知识建立满足统一语言要求的领域分析模型,然后引入实体值对象领域服务领域事件聚合资源库工厂等设计要素开始程序设计,获得设计模型后在它的指导下进行编码实现,输出最终的领域模型。

在领域驱动设计过程中,战略设计控制和分解了战术设计的边界与粒度,战术设计则以实证角度验证领域模型的有效性、完整性和一致性,进而以迭代的方式分别完成对限界上下文与领域模型的更新与演化,各自形成设计过程的闭环。两个不同阶段的设计目标保持一致,形成一个连贯的过程,彼此之间相互指导与规范,最终保证战略架构与领域模型的同时演进。

2.3 控制软件复杂度

回到对软件复杂度的本质分析。问题空间的规模与结构制造了理解能力障碍,问题空间的变化制造了预测能力障碍,从而形成了问题空间的复杂度。问题空间的复杂度决定了“求解”的难度,领域驱动设计对软件复杂度的控制之道就是竭力改变设计的质量,也就是在解空间中引入设计元模型,对问题空间的复杂度进行有效的控制。

2.3.1 控制规模

问题空间的规模客观存在,除了在软件构建过程中通过降低客户的期望,明确目标系统的范围能够有效地限制规模,要在问题空间控制规模,我们手握的筹码确实不多,然而到了解空间,开发团队就能掌握主动权了。虽然不能改变系统的规模,却可以通过“分而治之”的方法将一个规模庞大的系统持续分解为小的软件元素,直到每个细粒度(视问题空间的问题粒度而定)的软件元素能够解决问题空间的一个问题为止。当然,这种分解并非不分原则地拆分,在分解的同时还必须保证被分解的部分能够被合并为一个整体。分而治之的过程首先是自顶向下持续分解的过程,然后又是自底向上进行整合的过程。

分而治之是一个好方法,可是,该采用什么样的设计原则、以什么样的粒度对软件系统进行分解,又该如何将分解的软件元素组合起来形成一个整体,却让人倍感棘手。领域驱动设计提出了两个重要的设计元模型:限界上下文上下文映射,它们是控制系统规模最为有效的手段,也是领域驱动设计战略设计阶段的核心模式。

下面让我们通过一个案例认识到如何通过限界上下文控制系统的规模。

国际报税系统是为跨国公司的驻外雇员提供的、方便一体化的税收信息填报平台。税务专员通过该平台收集雇员提交的报税信息,然后对这些信息进行税务评审。如果税务专员评审出信息有问题,则将其返回给雇员重新修改和填报。一旦信息确认无误,则进行税收分析和计算,并生成最终的税务报告提交给当地政府以及雇员本人。

系统主要涉及的功能包括:

  • 驻外雇员的薪酬与福利;

  • 税收计划与合规评审;

  • 对税收评审的分配管理;

  • 税收策略设计与评审;

  • 对驻外出差雇员的税收合规评审;

  • 全球的签证服务。

主要涉及的用户角色包括:

  • 驻外雇员(assignee);

  • 税务专员(admin);

  • 出差雇员的雇主(client)。

采用领域驱动设计,我们将架构的主要关注点放在了“领域”,在与客户进行充分的需求沟通和交流后,通过分析已有系统的问题空间,结合客户提出的新需求,在解空间利用限界上下文对系统进行分解,获得如下限界上下文。

  • 账户(account):管理用户的身份与配置信息。

  • 日程(calendar):管理用户的日程与旅行足迹。

  • 工作(work record):实现工作的分配与任务的跟踪。

  • 文件共享(file sharing):实现客户与系统之间的文件交换。

  • 合规(consent):管理合法的遵守法规的状态。

  • 通知(notification):管理系统与客户之间的交流。

  • 问卷调查(questionnaire):对问卷调查的数据收集。

整个系统的解空间分解为多个限界上下文,每个限界上下文提供了自身领域独立的业务能力,获得了图2-4所示的系统架构。

clip_image007.png

图2-4 引入限界上下文的国际报税系统架构

每个限界上下文都是一个独立的自治单元。根据限界上下文的边界划分团队,建立单独的代码库。团队只为所属限界上下文负责:除了需要了解限界上下文之间的协作接口,以确定上下文映射的模式,团队只需要了解边界内的领域知识,为其建立各自的领域模型。系统复杂度通过限界上下文的分解得到了明显的控制。

2.3.2 清晰结构

保持系统结构的清晰是控制结构复杂度的不二法门。关键在于,要以正确的方式认清系统内部的边界。限界上下文从业务能力的角度形成了一条清晰的边界,它与业务模块不同,在内部也拥有独立的架构(参见第9章),通过分层架构将领域分离出来,在业务逻辑与技术实现之间划定一条清晰的边界。

为何要在业务逻辑与技术实现之间划分边界呢?实际上仍然可以从软件复杂度的角度给出理由。

问题空间由真实世界的客户需求组成,需求可以简单分为业务需求质量需求

业务需求的数量决定了系统的规模,这是业务需求对软件复杂度带来的直观影响。以电商系统的促销规则为例。针对不同类型的顾客与产品,商家会提供不同的促销力度。促销的形式多种多样,包括赠送积分、红包、优惠券、礼品;促销的周期需要支持定制,既可以是特定的日期(例如“双十一”促销),也可以是节假日的固定促销模式。显然,促销需求带来了促销规则的复杂度,包括支持多种促销类型,根据促销规则进行的复杂计算。这些业务需求并非独立的,它们还会互相依赖、互相影响,例如在处理促销规则时,还需要处理好它与商品、顾客、卖家与支付乃至于物流、仓储之间的关系。这对整个系统的结构提出了更高的要求。如果不能维持清晰的结构,就可能因为业务需求的不断变化带来业务逻辑的多次修改,再加上沟通不畅、客户需求不清晰等多种局外因素,整个系统的业务逻辑代码会变得纠缠不清,系统慢慢腐烂,变得不可维护,最终形成一种Brian Foote和Joseph Yoder所说的“大泥球”系统。

我们可以将业务需求带来的复杂度称为“业务复杂度”(business complexity)。

软件系统的质量需求就是我们为系统定义的质量属性,包括安全、高性能、高并发、高可用性等,它们往往给软件的技术实现带来挑战。假设有两个经营业务完全一样的电商网站,但其中一个电商网站的并发访问量是另一个电商网站的一百倍。此时,针对下订单服务,要达到相同的服务水平,就不再是通过编写更好的业务代码所能解决的了。质量属性对技术实现的挑战还体现在它们彼此之间的影响,如系统安全性要求对访问进行控制,无论是增加防火墙,还是对传递的消息进行加密,又或者对访问请求进行认证和授权,都需要为整个系统架构添加额外的间接层。这会不可避免地对访问的低延迟产生影响,拖慢系统的整体性能。又比如为了满足系统的高并发访问,需要对业务服务进行物理分解,通过横向增加更多的机器来分散访问负载;同时,还可以将一个同步的访问请求拆分为多级步骤的异步请求,引入消息中间件对这些请求进行整合和分散处理。这种分离一方面增加了系统架构的复杂度,另一方面也因为引入了更多的资源,使得系统的高可用面临挑战,且增加了维护数据一致性的难度。

我们可以将质量需求带来的复杂度称为“技术复杂度”(technology complexity)。

技术复杂度与业务复杂度并非完全独立的,二者的共同作用会让系统的复杂度变得不可预期、难以掌控。同时,技术的变化维度与业务的变化维度并不相同,产生变化的原因也不一致。倘若未能很好地界定二者之间的关系,确定两种复杂度之间的清晰边界,一旦各自的复杂度增加,团队规模也将随之扩大,再糅以严峻的交付周期、人员流动等诸多因素,就好似将各种不稳定的易燃易爆气体混合在一个密闭容器中,随时都可能产生复杂度的组合爆炸,如图2-5所示。

clip_image009.png

要避免业务逻辑的复杂度与技术实现的复杂度混杂在一起,就需要确定业务逻辑与技术实现的边界,从而隔离各自的复杂度。这种隔离也符合关注点分离的设计原则。例如,在电商的领域逻辑中,订单业务关注的业务规则包括验证订单有效性,计算订单总额,提交和审批订单的流程等;技术关注点则从实现层面保障这些业务能够正确地完成,包括确保分布式系统之间的数据一致性,确保服务之间通信的正确性等。业务逻辑不需要关心技术如何实现。无论采用何种技术,只要业务需求不变,业务规则就不会变化。换言之,理想状态下,我们应该保证业务规则与技术实现是正交的。

领域驱动设计引入的分层架构规定了严格的分层定义,将业务逻辑封装到领域层(domain layer),支撑业务逻辑的技术实现放到基础设施层(infrastructure layer)。在领域层之上的应用层(application layer)则扮演了双重角色:一方面,作为业务逻辑的外观(facade),它暴露了能够体现业务用例的应用服务接口;另一方面,它又是业务逻辑与技术实现之间的黏合剂,实现了二者之间的协作。

图2-6展示了一个典型的领域驱动设计分层架构。领域层的内容与业务逻辑有关,基础设施层的内容与技术实现有关,二者泾渭分明,然后汇合在作为业务外观的应用层。应用层确定了业务逻辑与技术实现的边界,通过依赖注入(dependency injection)的方式将二者结合起来。

抽象的资源库接口隔离了业务逻辑与技术实现。资源库接口属于领域层,资源库实现则放在基础设施层,通过依赖注入[4]可以在运行时为业务逻辑注入具体的资源库实现。无论资源库的实现怎么调整,领域层的代码都不会受到牵连。例如:领域层的领域服务OrderService通过OrderRepository资源库添加订单,OrderService并不会知道OrderRepository的具体实现:

package com.dddexplained.ecommerce.ordercontext.domain;

@Service

public class OrderService {

@Autowired

private OrderRepository orderRepository;

public void execute(Order order) {

if (!order.isValid()) {

throw new InvalidOrderException(String.format("the order which placed by buyer with %s

is invalid.", buyerId));

}

orderRepository.add(order);

}

}

@Repository

public interface OrderRepository {

void add(Order order);

}

领域驱动设计通过限界上下文隔离了业务能力的边界,通过分层架构隔离了业务逻辑与技术实现,如此就能保证整个业务系统的架构具有清晰的结构,实现了有序设计,可以避免不同关注点的代码混杂在一处,形成可怕的“大泥球”。

2.3.3 响应变化

未来的变化是无法控制的,我们只能以积极的态度拥抱变化。变被动为主动的方式就是事先洞察变化的规律,识别变化方向,把握业务逻辑的本质,使得整个系统的核心领域逻辑能够更好地响应需求的变化。

领域驱动设计通过模型驱动设计针对限界上下文进行领域建模,形成了结合分析、设计和实现于一体的领域模型。领域模型是对业务需求的一种抽象,表达了领域概念、领域规则以及领域概念之间的关系。一个好的领域模型是对统一语言的可视化表示,可以减少需求沟通可能出现的歧义。通过提炼领域知识,并运用抽象的领域模型去表达,就可以达到对领域逻辑的化繁为简。模型是封装,实现了对业务细节的隐藏;模型是抽象,提取了领域知识的共同特征,保留了面对变化时能够良好扩展的可能性。

领域建模的一个难点是如何将看似分散的事物抽象成一个统一的领域模型。例如,我们要开发的项目管理系统需要支持多种软件项目管理流程,如瀑布、统一过程、极限编程或者Scrum,这些项目管理流程迥然不同,如果需要我们为各自提供不同的解决方案,就会使系统的模型变得非常复杂,也可能引入许多不必要的重复。通过领域建模,我们可以对项目管理领域的知识进行抽象,寻找具有共同特征的领域概念。这就需要分析各种项目管理流程的主要特征与表现,以从中提炼出领域模型。

瀑布式软件开发由需求、分析、设计、编码、测试、验收6个阶段构成,每个阶段都由不同的活动构成,这些活动可能是设计或开发任务,也可能是召开评审会。

统一过程(rational unified process,RUP)清晰地划分了4个阶段:先启阶段、细化阶段、构造阶段和交付阶段。每个阶段可以包含一到多个迭代,每个迭代有不同的工作,例如业务建模、分析设计、配置和变更管理等。

极限编程(eXtreme programming,XP)作为一种敏捷方法,采用了迭代的增量式开发,提倡为客户交付具有业务价值的可运行软件。在执行交付计划之前,极限编程要求团队对系统的架构做一次预研(architectural spike,又被译为架构穿刺)。当架构的初始方案确定后,就可以进入每次小版本的交付。每个小版本交付又被划分为多个周期相同的迭代。在迭代过程中,要求执行一些必需的活动,如编写用户故事、故事点估算、验收测试等。

Scrum同样是迭代的增量开发过程。项目在开始之初,需要在准备阶段确定系统愿景、梳理业务用例、确定产品待办项(product backlog)、制订发布计划以及组建团队。一旦确定了产品待办项以及发布计划,就进入冲刺(sprint)迭代阶段。sprint迭代过程是一个固定时长的项目过程,在这个过程中,整个团队需要召开计划会议、每日站会、评审会议和回顾会议。

显然,不同的项目管理流程具有不同的业务概念,例如瀑布式开发分为6个阶段,却没有发布和迭代的概念;RUP没有发布的概念;Scrum为迭代引入了冲刺的概念。不同的项目管理流程具有不同的业务规则,例如RUP的4个阶段可以包含多个迭代周期,每个迭代周期都需要完成对应的工作,只是不同工作在不同阶段所占的比重不同;XP需要在进入发布阶段之前进行架构预研,而在每次小版本发布之前,都需要进行验收测试和客户验收;Scrum的冲刺是一个基本固定的流程,每个迭代召开的“四会”(计划会议、评审会议、回顾会议和每日站会)都有明确的目标。

领域建模就是要从这些纷繁复杂的领域逻辑中寻找到能够表示项目管理领域的概念,对概念进行抽象,确定它们之间的关系。经过分析这些项目管理流程,我们发现它们的业务概念和规则上虽有不同之处,但都归属于软件开发领域,因此必然具备一些共同特征。

从项目管理系统的角度看,无论针对何种项目管理流程,我们的主题需求是不变的,就是要为这些管理流程制订软件开发计划(plan)。不同之处在于,计划可以由多个阶段(phase)组成,也可以由多个发布(release)组成。一些项目管理流程没有发布的概念,我们也可以认为是一个发布。那么,到底是一个发布包含多个阶段,还是一个阶段包含多个发布呢?我们发现,在XP中明显地划分了两个阶段:架构预研阶段与发布计划阶段,而发布只属于发布计划阶段。因而从概念内涵上,可以认为是阶段(phase)包含了发布(release),每个发布又包含了一到多个迭代(iteration)。至于Scrum的sprint概念,其实可以看作迭代的一种特例。每个迭代可以开展多种不同的活动(activity),这些活动可以是整个团队参与的会议,也可以是部分成员或特定角色执行的实践。对计划而言,我们还需要跟踪任务(task)。与活动不同,任务具有明确的计划起止时间、实际起止时间、工作量、优先级和承担人。

于是可提炼出图2-7所示的统一领域模型。

clip_image013.png

图2-7 项目管理系统的统一领域模型

为了让项目管理者更加方便地制订项目计划,产品经理提出了计划模板功能。当管理者选择对应的项目管理生命周期类型后,系统会自动创建满足其规则的初始计划。基于增加的这一新需求,我们更新了之前的领域模型,如图2-8所示。

clip_image015.png

图2-8 领域模型对变化的应对

在增加的领域模型中,生命周期规格(life cycle specification)是一个隐含的概念,遵循领域驱动设计提出的规格(specification)模式,封装了项目开发生命周期的约束规则。

领域模型以可视化的方式清晰地表达了业务含义。我们可以利用这个模型指导后面的程序设计与编码实现:当需求发生变化时,能够敏锐地捕捉到现有模型的不匹配之处,并对其进行更新,使得我们的设计与实现能够以较小的成本响应需求的变化。

2.4 冷静认识

控制软件复杂度是构建软件过程中永恒的旋律,必须明确:软件复杂度可以控制,但不可消除。领域驱动设计控制软件复杂度的中心主要在于“领域”,Eric Evans就认为:“很多应用程序最主要的复杂度并不在技术上,而是来自领域本身、用户的活动或业务。”这当然并不全面,随着软件的“触角”已经蔓延到人类生活的方方面面,在业务复杂度变得越来越高的同时,技术复杂度也在不断地向技术极限发起挑战,其制造的技术障碍完全不亚于业务层面带来的困难。领域驱动设计并非“银弹”,它的适用范围主要是大规模的、具有复杂业务的中大型软件系统,至于对技术复杂度的应对,它的选择是“隔离”,然后交给专门的技术团队设计合理的解决方案。

领域驱动设计控制软件复杂度的方法当然不仅限于本章给出的阐释和说明,它的设计元模型在软件构建的多个方面都在发挥着作用,其目的自然也是改进设计质量以应对软件复杂度——这是领域驱动设计的立身之本!如果你要构建的软件系统没有什么业务复杂度,领域驱动设计就发挥不了它的价值;如果构建软件的团队对于软件复杂度的控制漠不关心,只顾着追赶进度而采取“头痛医头,脚痛医脚”的态度,领域驱动设计这套方法可能也入不了他们的法眼。即便认识到了领域驱动设计的价值,怎么用好它也是一个天大的难题。我尝试破解落地难题的方法,就是重新梳理领域驱动设计的知识体系,尝试建立一个固化的、具有参考价值的领域驱动设计统一过程。


[1] 指的是《领域驱动设计》一书出版时(2003年)的20年前,也就是20世纪80年代。

[2] Eric Evans提出了核心领域与通用子领域,Vaughn Vernon在《实现领域驱动设计》一书中补充了支撑子领域。为了统一,我将“核心领域”称为“核心子领域”。

[3] Vaughn Vernon在《实现领域驱动设计》一书中补充了合作伙伴模式。

[4] 由Martin Fowler提出,以利于有效解耦,参见文章Inversion of Control Containers and the Dependency Injection pattern


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK