解构DDD:软件的核心是为用户解决领域相关的问题

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

应对软件复杂度,许多顶尖的软件设计人员与开发人员纷纷结合实践提出自己的真知灼见,既包括编程思想、设计原则、模式语言、过程方法和管理理论,又包括对编程利器自身的打磨。毫无疑问,通过这些真知灼见,软件领域的先行者已经改变或正在改变我们构建软件的方法、过程和目标,我们欣喜地看到了软件的构建正在向着好的方向改变。然而,整个客观世界的所有现象都存在诸如黑与白、阴与阳、亮与暗的相对性,任何技术的发展都不是单向的。随着技术日新月异向前发展,软件系统的复杂度也日益增长。中国有一句古谚:“道高一尺,魔高一丈。”又有谚语:“魔高一尺,道高一丈。”究竟是道高还是魔高,就看你是站在“道”的一方,还是“魔”的一方。
在构建软件的场景中,软件复杂度显然就是“魔”,控制软件复杂度的方法则是“道”。在软件构建领域,“道”虽非虚无缥缈的玄幻叙述,却也不是绑定在具象之上的具体手段。软件复杂度的应对之道提供了一些基本法则,这些基本法则可以说放之四海而皆准,其中一条基本法则就是:能够控制软件复杂度的,只能是设计(指广泛意义上的设计)方法。因为我们无法改变客观存在的问题空间(参见2.1.2节对问题空间和解空间的阐释),却可以改变设计的质量,让好的设计为控制复杂度创造更多的机会。如果我们将软件系统限制在业务软件系统之上,又可得到另外一条基本法则:“要想克服”(业务系统的)复杂度,就需要非常严格地使用领域逻辑设计方法。[8]1在近20年的时间内,一种有效的领域逻辑设计方法就是EricEvans提出的领域驱动设计(domain-drivendesign)。

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

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

领域驱动设计概览

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

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

领域驱动设计的基本概念

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

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

领域驱动设计元模型

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


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

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

  • 客户方/供应方;
  • 共享内核;
  • 遵奉者;
  • 分离方式;
  • 开放主机服务;
  • 发布语言;
  • 防腐层;
  • 大泥球

领域驱动设计元模型

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

领域驱动设计概览

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

问题空间和解空间

哲学家常常会围绕真实世界和理念世界的映射关系探索人类生存的意义,即所谓“两个世界”的哲学思考。软件世界也可一分为二,分为构成描述需求问题的真实世界与获取解决方案的理念世界。整个软件构建的过程,就是从真实世界映射到理念世界的过程。如果真实世界是复杂的,在映射为理念世界的过程中,就会不断受到复杂度的干扰。根据Allen Newell和Herbert Simon的问题空间理论:“人类是通过在问题空间(problem space)中寻找解决方案来解决问题的”,构建软件(世界)也就是从真实世界中的问题空间寻找解决方案,将其映射为理念世界的解空间(solution space)来满足问题空间的需求。因此,软件系统的构建实则是对问题空间的求解,以获得构成解空间的设计方案,如图所示。

从问题空间到解空间*

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

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

针对软件系统提出的问题,解决方案的推陈出新自然毋庸讳言,无论是技术、工具,还是设计思想与模式,都有了很大变化。解决方案不是从石头里蹦出来的,而必然是为了解决问题而生的。

面对错综复杂的问题,解决方案自然也需要灵活变化。软件开发技术的发展是伴随着复用性和扩展性发展的。倘若问题存在相似性,解决方案就有复用的可能。通过抽象寻找到不同问题的共性时,相同的解决方案也可以运用到不同的问题中。同时,解决方案还需要响应问题的变化,能在变化发生时以最小的修改成本满足需求,同时保障解决方案的新鲜度。无疑,构成解空间的解决方案不仅要解决问题,还要控制软件系统的复杂度。

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

怎么区分方案级需求和问题级需求?方案级需求就好比一个病人到医院看病,不管病情就直接让医生开阿司匹林,而问题级需求则是向医生描述自己身体的症状。病情是医生要解决的问题,处方是医生提供的解决方案。那种站在技术视角展开的需求分析,实际就是没有明确问题空间与解空间的界限。在针对问题空间求解时,必须映射于问题空间定义的问题,如此才能遵循恰如其分的设计原则,在问题空间的上下文约束下寻找合理的解决方案。领域驱动设计为问题空间与解空间提供了不同的设计元模型。对于问题空间,强调运用统一语言来描述需求问题,利用核心子领域、通用子领域与支撑子领域来分解问题空间,如此就可以“揭示什么是重要的以及在何处付出努力”。除去统一语言与子领域,其余设计元模型都将运用于解空间,指导解决方案围绕着“领域”这一核心开展业务系统的战略设计与战术设计。

战略设计和战术设计

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

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

  • 问题空间:对问题空间进行合理分解,识别出核心子领域、通用子领域和支撑子领域,并确定各个子领域的目标、边界和建模策略。
  • 解空间:对问题空间进行解决方案的架构映射,通过划分限界上下文,为统一语言提供知识语境,并在其边界内维护领域模型的统一。每个限界上下文的内部有着自己的架构,限界上下文之间的协作关系则通过上下文映射来体现和表达。

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

战术设计阶段需要在限界上下文内部开展领域建模,前提是你为限界上下文选择了领域模型模式。在限界上下文内部,需要通过分层架构将领域独立出来,在排除技术实现的干扰下,通过与领域专家的协作在统一语言的指导下逐步获得领域模型。战术设计阶段最重要的设计元模型是聚合模式。虽然聚合是实体和值对象的概念边界,然而在获得了清晰表达领域知识的领域模型后,我们可以将聚合视为表达领域逻辑的最小设计单元。如果领域行为是无状态的,或者需要多个聚合的协作,又或者需要访问外部资源,则应该将它分配给领域服务。至于领域事件,则主要用于表达领域对象状态的迁移,也可以通过事件来实现聚合乃至限界上下文之间的状态通知。
战略设计与战术设计并非割裂的两个阶段,而是模型驱动设计过程在不同阶段展现出来的不同视图。战略设计指导着战术设计,这就等同于设计原则指导着设计决策。EricEvans就明确指出,“战略设计原则必须把模型的重点放在捕获系统的概念核心,也就是系统的‘远景’上。”当一个业务系统的规模变得越来越庞大时,战略设计高屋建瓴地通过限界上下文规划了整个系统的架构。只要维护好限界上下文的边界,管理好限界上下文之间的协作关系,限制在该边界内开展的战术设计所要面对的就是一个复杂度得到大幅降低的小型业务系统。

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

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

领域模型驱动设计

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

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

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

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

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

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

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

本文摘录于张逸老师新书《解构领域驱动设计》。

引用:https://mp.weixin.qq.com/s?__biz=MzIxMzEzMjM5NQ==&mid=2651054016&idx=2&sn=b4abfe5bbb09b379b4c68fa6e63ef3f6&chksm=8c4c0ac4bb3b83d27ffcc581969c4ae0196f12891356349a9e941657fb2caaf35a5b0396f2e7&scene=178&cur_album_id=1694423786747428868#rd

你可能感兴趣的:(解构DDD:软件的核心是为用户解决领域相关的问题)