toBeTheLight.github.io 荒原

《领域驱动设计》

2022-07-14
toBeTheLight

在业务第一的原则下,通过对领域层(这个领域层就是业务领域)建模,使得实现和业务互相 1 比 1 真实体现的开发方式?

前言

尽管他们在技术使用方面也值得商榷,但真正挫败他们的是业务逻辑。

错误地将开发人员的角色独立出来,导致建模与实现脱节,因此设计无法反映不断深化的分析。

很多应用程序最主要的复杂性并不在技术上,而是来自领域本身、用户的活动或业务。

领域模型是对知识严格的组织且有选择的抽象,出于某种目的而概括地反映现实。

运用领域模型

先来提一个问题,什么是领域模型?

软件的核心是为其用户解决领域相关的问题的能力,其它特性都要服务于这个基本目的。

  • 注:或许可以说模型是与技术无关的有关业务的描述?

消化知识

通过头脑风暴活动创建新的模型或者修改原有的模型对象,并消化理解这些模型对象中的知识。

模型包含各种类型的知识。

不再使用的或不重要的概念被从模型中移除。当一个不需要的概念与一个需要的概念有关联时,则把重要的概念提取到一个新模型中,不重要的概念可以丢弃。

知识消化由开发人员和领域专家组成的团队来共同完成。

高效的领域建模人员是知识的消化者,努力寻找对大量信息有意义的简单视图,只有找到一组适用于所有细节的抽象概念后,才算成功。

领域模型的不断精化迫使开发人员学习重要的业务原理,而不是机械的进行功能开发。

分析员和程序员将自己的知识输入到模型中,模型的组织更严密,抽象更简洁。领域专家也将他们的知识输入到模型中,模型反映了业务的深层次知识,而且真正的业务原则得以抽象。

项目知识零散地分散在很多人的文档中,我们并没意识到不知道的东西究竟有多少。同时,所有项目都会丢失知识。

模型获得的知识远远不只是“发现名词”。业务活动和规则如同所涉及的实体一样,都是领域的核心。

知识消化所产生的模型能够反映出对知识的深层理解。开发人员对模型实现进行重构,以反映出模型的变化,同时,新的知识就被整合到应用程序中。

当我们的建模不再局限于寻找实体值对象时,我们才能充分吸取知识。

  • 注:此章节埋了一个线,运用了整洁代码的方式,将一条过程式的包含业务知识的逻辑封装成了一个可视的策略调用,使业务规则直接可见。但这应该不是领域模型的应用方式吧,我猜?但显而易见的是,更明确的设计确实使得业务规则明确且显得重要,也可以更方便的与不太懂代码的人展示并对齐理解。

随着对领域和应用程序所需要的理解逐渐加深,一些开始不可能发现的巧妙抽象就会渐渐浮出水面,而它们恰恰切中问题要害。

有了更深刻的认识后,我们对航运业务的认识从“集装箱在各个地点之间的传输”转变为“运输责任在各个实体之间的传递”。

交流与语言的使用

要想创建一种灵活的、蕴含丰富知识的设计,需要一种通用的、共享的团队语言,以及对语言不断的试验。

日常讨论所使用的术语与代码中所使用的术语不一致。导致对领域的深刻表述常常稍纵即逝。同时翻译使得沟通不畅,并削弱了知识消化。项目需要一种公共语言,领域模型可以成为这种公共语言的核心,同时将团队沟通与软件实现紧密联系到一起。

通用语言的词汇包括类和主要操作的名称。

  • 通用语言:Ubiquitous Language

模型可能缺乏开发人员在代码中所创建的更为微妙和灵活的特性,这要么是因为开发人员认为模型不必具备这些特性,要么是因为编码风格是过程式的,只能隐含的表达领域概念。

通用语言是那些以非代码形式呈现的设计的主要载体,这些包括把整个系统组织在一起的大尺度结构、定义了不同系统和模型之间关系的限界上下文,以及在模型和设计中使用的其他模式。

  • 注:感觉很重要,但是不懂,后面再看。

模型驱动设计的构造块

分离领域

我们需要将领域对象与系统中的其他功能分离,这样就能够避免将领域概念和其他只与软件技术相关的概念搞混了。

分层的价值在于每一层都只代表程序中的某以特定方面。大多数的分层架构使用的都是这四个概念层的变体:用户界面层(表示层)、应用层、领域层(或模型层)、基础设施层。虽然项目间会有差异,但是将领域层分离出来才是实现模型驱动设计的关键。

应用层负责对领域对象的行为进行协调。

负责处理基本业务规则的的是领域层,而不是应用层。

各层之间是松散连接的,层与层的依赖关系只能是单向的。上层可以直接使用或操作下层元素,通过调用下层元素的公共接口,保持对下层元素的引用。

最早将用户界面与应用层和领域层相连的模式是 MVC。

  • 注:MVC、MVVM、MVX 三层模型中的桥梁部分往往包含了用户界面层的操作逻辑、应用层逻辑和领域层的操作逻辑。是否能通过某种模式再进行一次区分呢?感觉可能也许纯前端领域复杂度也很难到这个程度。

当使用框架时,项目团队应该明确其使用目的:建立一种可以表达领域模型的实现并且用它来解决重要问题。

软件中所表示的模型

  • 模型:
    • Entity:具有连续性和标识的事物
    • Value Object:描述某种状态的属性
    • Service:动作和操作,稍稍违背了面向对象的建模传统

对象之间的关联使得建模与实现之间的交互更为复杂。

模型中每个可遍历的关联,软件中都要有相同属性的机制(注:即设计要反映模型将关系)。

设计无需如此直接。(注:在这句话之前,作者举了一个具体的实现方式。所以这句话的意思是,在进行设计时,设计好对应模型的方法即可,不必关注具体,能反映模型即可。)

模式:Entity

很多对象不是通过他们的属性定义的,而是通过连续性和标识定义的。

有时,这样的对象必须与另一个具有不同属性的对象相匹配,有时一个对象必须与具有相同属性的另一个对象区分开。

主要由标识定义的对象被称为 Entity(注:即有一个在系统中不重复的标识方式,如 id、uid 等属性或由一些属性组合判断的方式,这个对象可能是跨系统的,可能在不同系统中属性是不同的,但由标识逻辑却可判断是同一个对象。

  • 注:顾名思义,Entity,实体,对应一个由标识区分的具体事物。

Entity 最基本的职责是确保连续性,保持简练是实现这一责任的关键。不要讲注意力集中在属性或行为上,应该摆脱这些细枝末节。

  • 抓住 Entity 对象定义的最基本特征,尤其是那些勇于识别、查找或匹配对象的特征。只添加那些对概念至关重要的行为和这些行为所必需的属性。
  • 将行为和属性转移到与核心 Entity 关联的其他对象中。
  • Entity 往往通过协调其关联对象的操作来完成自己的职责。

模式:Value Object

很多对象没有概念上的标识,它们描述了一个事物的某种特征。

用于描述领域的某个方面而本身没有概念标识的对象称为 Value Object(值对象)。被实例化之后用来表示一些设计元素,对于这些设计元素,我们只关心它们是什么,而不关心它们是谁。

Value Objct 可以是其他对象的集合。

当我们只关心一个模型元素的属性时,应把它归类为 Value Object。我们应该使这个模型元素能够表示出其属性的意义,并为它提供相关功能。Value Object 应该是不可变的。不要为它分配任何标识。

编程语言没有直接支持这些概念上的区别并不说明这些区别没有用处。只是说明我们需要更多的约束机制来确保满足一些重要的规则。命名规则、精心准备的文档和大量讨论都可以强化这些需求。

如果一个 Value 的实现是可变的,那么久不能共享它。无论是否共享 Value Object,在可能的情况下都要将它们设计为不可变的。定义 Value Object 并将其指定为不可变的是一条一般规则。

  • 注:具体情况具体分析吧,作为被传递给其它函数或对象的 Value Object 不可变会使变更管理简单。其它场景下在不同语言可能会有更好的可变 Value 的共享模式。也可能是没理解,继续看吧。

模式:Service

在某些情况下,最清楚、最实用的设计会包含一些特殊的操作,这些操作从概念上不属于任何对象。与其把它们强制地归于哪一类,不如顺其自然地在模型中引入一种新的元素,这就是 Service。

有些操作从本质上讲是一些活动或动作,而不是事物,但由于我们的建模范式是对象,因此要想办法将它们划归到对象这个范畴里。

当我们勉强将一个操作放到不符合对象定义的对象中时,这个对象就会产生概念上的混淆,而且会变得很难理解或重构。复杂的操作很容易把一个简单对象搞乱,使对象的角色变得模糊。由于这些操作常常会牵扯到很多领域对象————需要协调这些对象以便使它们工作,而这会产生对所有这些对象的依赖,将那些本来可以独立理解的概念掺杂在一起

  • 注:Service,一个工具箱。

Service,强调的是与其它对象的关系。Service 也可以有抽象而有意义的定义,也应该有定义的职责,而且这种职责以及履行它的接口也应该作为领域模型的一部分来加以定义。操作名称应该来自于通用语言,如果通用语言中没有这个名称,则应该将其引入到通用语言中。参数和结果应该是领域对象

当领域中某个重要的过程或转换操作不是 Entity 或 Value Object 的自然职责是时,应该在模型中添加一个作为独立接口的操作,并将其声明为 Service。定义接口时要使用模型语言,并确保操作名称是通用语言的术语。此外,应使 Service 成为无状态的。这种无状态是指任何客户都可以使用某个 Service 的任何实例,而不必关心该实例的历史状态。Service 执行时将使用可全局访问的信息,甚至会改变这些全局信息(也就是说,,它可能具有副作用)。但 Service 不保持影响其自身行为的状态。

Service 并不只在领域层中使用,需要区分属于领域层的 Service 和那些属于其它层的 Service,并划分职责,以便将它们明确的区分开。

例如:如果银行应用程序可以把我们的交易进行转换并导出到一个电子表格文件中,以便进行分析,那么这个导出操作就是应用层 Service。“文件格式”在银行领域中是没有意义的,也不涉及业务规则。

模式:Module

Module 的使用有一些技术上的原因,但主要原因却是“认知超载”,Module 为人们提供了两种观察模型的方式,一是可以在 Module 中查看细节,而不会被整个模型淹没,二是观察 Module 之间的关系,而不考虑其内部细节。

  • 注:知识边界?忘了看哪本书的时候得出的这个名词,大概是《架构整洁之道》提到边界的时候。一切组织方式都是边界的划分,不同维度有不同维度的划分粒度,从函数、对象、模块、架构组件、服务,都只应该知道什么、只应该负责什么,是一种通用的实践原则。

Module 之间应该是低耦合的,而在 Module 的内部则是高内聚的。一个人一次考虑的事情是有限的(因此才要低耦合)。不连贯的思想和“一锅粥”似的思想同样难于理解(因此才要高内聚)。

低耦合高内聚作为通用的设计原则既适用于各种对象,也适用于 Module。

  • 注:Bingo!模块层面:模块间低耦合,模块内模型间高内聚;模型间低耦合,模型内方法高内聚;模型内方法间低耦合,方法内,逻辑分块高内聚。内聚即意味着内部处理同一事物或逻辑,“同一”要看所处的维度,如方法维度就是获取用户登录状态,模型维度就是用户权限,模块维度就是用户行为。低耦合则意味着对单体的内部实现修改不影响同一维度的其它单体的实现。

选择能够描述系统的 Module,并使之包含一个内聚的概念集合,这通常会实现 module 之间的低耦合,但如果效果不理想,则应寻找一种更改模型的方式来消除概念之间的耦合,或者找打一个可作为 Module 基础的概念(这个概念先前可能被忽视了),基于这个概念组织的 Module 可以以一种有意义的方法将元素集中到一起。找到一种低耦合的概念组织方式,从而可以相互独立地理解和分许这些概念。对模型进行精化,直到可以根据高层领域概念对模型进行划分。

Module 的名称应该是通用语言中的术语。Module 及其名称反映出领域的深层知识。

仅仅研究概念关系是不够的,它并不能替代技术措施,这二者是相同问题的不同层次,都是必须要完成的。当必须做出一个折中选择时,务必保证概念清晰,即使这意味着 Module 之间会产生更多的引用,或者改变 Module 偶尔会缠身“涟漪效应”,开发人员只要理解了模型所描述的内容,就可以应付这些问题。

领域模型中的每个概念都应该在实现元素中反映出来。实现中的对象、指针和检索机制必须直接、清楚地映射到模型元素,如果没有做到这一点就要重写代码,或者回头修改模型,或者同时修改代码和模型。不要在领域对象中添加任何与领域对象所表示的概念没有紧密关系的元素。领域对象的职责是表示模型。

建模范式

以上 4 中模式为对象模型提供了构造块,但模型驱动设计并不是说必须将每个模型都建模为对象。一些工具还支持其他的模型范式,如规则引擎。这些其它工具和技术是模型驱动设计的补充,而不是要取而代之。目前主流的范式是对象设计,大多数人都比较容易理解面向对象设计的基本知识。

虽然模型驱动设计不一定是面向对象的,但它确实需要一种富有表达力的模型结构实现,无论是对象、规则还是工作流,都是如此。如果可用工具无法提高表达力,就要重新考虑选择工具。缺乏表达力的实现将削弱各种范式的优势。

  • 注:领域模型优先,知识的表达优先,统一性优先。

将非对象元素混合到以面向对象为主的系统中时,要把通用语言作为依靠的基础。即使同居之间没有严格联系,语言使用上的高度一致性也能防止各个设计部分分裂。坚持在多个环境中使用一致的名称,坚持使用通用语言讨论这些名称,将有助于消除两种环境之间的鸿沟。

领域对象的生命周期

主要的挑战有以下两类:在整个生命周期中维护完整性;防止模型陷入管理生命周期复杂性造成的困境中。

采用三种模式解决这些问题:聚合(Aggregate),通过定义清晰的所属关系和边界,并避免混乱、错综复杂的对象关系网来实现模型的内聚;使用工程(Factory)来创建和重建复杂对象和聚合,从而封装它们的内部结构;在生命周期的中间和末尾使用存储库(Repository)来提供查找和检索持久化对象并封装庞大基础设施的手段。

模式:Aggregate

  • 注:总的来说是一种提升内聚性,通过减少关联,减少外部引用的边界划分方式,避免对象修改、销毁对外部或业务实现复杂性(竞态)的影响。

具有复杂关联的模型中,要想保证对象更改的一致性是很困难的,不仅互不关联的对象需要遵守一些固定规则,而且紧密关联的各组对象也要遵守一些固定规则。然而,过于谨慎的锁定机制又会导致多个用户之间毫无意义地互相干扰,从而使系统不可用。

  • 注:将内聚模型中的非强关联的对象挪出去,可以减少一些竞态锁定导致的冲突或干扰问题。如订单中的商品价格,对订单是通用的,会导致很多订单出现竞态,同时修改频率会比订单低,又不需要实时同步,对确定的订单是没有影响的,就可以挪出内聚。

每个 Aggregate 都有一个根和一个边界。边界定义了 Aggregate 的内部都有什么,根则是 Aggregate 所包含的一个特定 Entity。对 Aggregate 而言,外部对象只可以引用根,而边界内部的对象之间则可以互相引用。

为了实现概念上的 Aggregate 需要对所有事务应用一组规则:

  • 根 Entity 具有全局标识,最终负责检查固定规则;
  • 根 Entiry 具有全局标识,边界内的 Entity 具有本地标识,这些标识只在 Aggregate 内才是唯一的;
  • Aggregate 外部的对象不能引用除根 Entity 之外的任何内部对象;
  • 根 Entity 可以把对内部的引用传递给他们(副本、临时引用,非共享的方式);
  • 只有根才能直接通过数据库查询。其它所有对象必须通过遍历关系来发现;
  • Aggregate 内部的对象可以保持对其它 Aggregate 根的引用;
  • 删除操作必须一次删除 Aggregate 边界之内的所有对象(所以要依赖上面的设计原则,外部对象只对其根 Entity 有引用);
  • 当提交对 Aggregate 边界内部的任何对象的修改时,整个 Aggregate 的所有固定规则都必须被满足。
  • 由于根控制访问,因此只能通过根来修改内部对象。

模式:Factory

当创建一个对象或创建整个 Agggregate 时,如果创建工作很复杂,或者暴露了过多的内部结构,则可以使用 Factory 进行封装。

应该将创建复杂对象的实例和 Aggregate 的职责转移给单独的对象,这个对象本身可能没有承担领域模型中的职责,但它仍是领域设计的一部分

模式:Repository

开发人员可能使用查询(注:查询某些属性)从数据库中提取他们所需的数据,或是直接提取具体的对象,而不是通过 Aggregate 的根来得到这些对象。这样就导致领域逻辑进入查询和客户代码中,而 Entity 和 Value Object 则变成单纯的数据容器。这将导致开发人员简化领域层,最终使模型变得无关紧要。

在所有持久化对象中,有一小部分必须通过基于对象属性的搜索来全局访问。随意的数据库查询会破坏领域对象的封装和 Aggregate,妨碍模型驱动的设计。

为美中需要全局访问的对象类型创建一个对象,这个对象相当于该类型的所有对象在内存中的一个集合的“替身”。通过一个众所周知的全局接口来提供访问。提供添加和删除对象的方法,用这些方法来封装在数据存储中实际插入或删除数据的操作,将实际的存储和查询技术封装起来。将所有对象的存储和访问操作交给 Repository 来操作。

  • 注:将于技术相关的代码职责封装进 Repository 中,这个 Repository 应该与一个具体的业务领域对象紧密关联,且不只是与硬件数据库一致的固件 DAO 代码,是负责管理领域生命周期的高层抽象。

Repository 和 Factory 不是冲突的,Repository 内会需要借助 Factory 重建一个已有对象。

  • 注:也就是说,Repository 的对象是已经存在的,有重建过程但是同一个概念对象,处在生命周期的中间。

后续是一些具体的实践方式,总之还是遵循业务第一、核心业务第一、开发资源向核心业务实现倾斜(而非技术难度),分辨好知识边界的情况下,通过一系列手段实现柔性架构,保证实现的健康成长(真实反馈业务、易维护、易扩展)。


相关文章

下一篇 《硬核晋升》

Content