0%

【领域驱动设计】曲高和寡

知识消化是一种探索,它永无止境。


更新历史

  • 2022.12.18:完成初稿

读后感

看完这本书,我理解为什么 DDD 很难流行起来了,因为对人的要求,对组织的要求实在太高了。很难想象在这个 VUCA 时代,有什么领域模型是可以经过几年打磨而日趋完美的,更大的概率是变化快到来不及搞定 DDD。

读书笔记

有很多因素会使软件开发复杂化,但最根本的原因是问题领域本身错综复杂。如果你要为一家人员复杂的企业提高自动化程度,那么你开发的软件将无法回避这种复杂性,你所做的只有控制这种复杂度。

控制复杂性的关键是有一个好的领域模型,这个模型不应该仅仅停留在领域的表面,而是要透过表象抓住领域的实质结构,从而为软件开发人员提供给他们所需的支持。

高效的领域建模人员不仅应该能够在白板上与会计师进行讨论,而且还应该能与程序员一道编写代码。真正强大的领域模型是随着时间演进的,即使是最有经验的建模人员也往往发现他们是在系统的初始版本完成之后才有了最好的想法。

前言

成功的项目有一个共同的特征,那就是都有一个丰富的领域模型,这个模型在迭代设计的过程中不断演变,而且成为项目不可分割的一部分。本书有两个前提:

  1. 在大多数软件项目中,主要的焦点应该是领域和领域逻辑;
  2. 复杂的领域设计应该基于模型。

尽管开发人员个人能够从理解领域驱动设计中学到有价值的设计技术和观点,但最大的好处却来自团队共同应用领域驱动设计方法,并且将领域模型作为项目沟通的核心。这样,团队成员就有了一种公共语言,可以用来进行更充分的沟通,并确保围绕软件来进行沟通。他们将创造出一个与模型步调一致的清晰的实现,从而为应用程序的开发提供帮助。所有人都了解不同团队的设计工作之间的互相联系,而且他们会一致将注意力集中在那些对组织最有价值、最与众不同的特性的开发上。

第一部分 运用领域模型

每个软件程序是为了执行用户的某项活动,或是满足用户的某种需求。这些用户应用软件的问题区域就是软件的领域。

在领域驱动的设计中,3 个基本用途决定了模型的选择:

  1. 模型和设计的核心互相影响。正是模型与实现之间的紧密联系才使模型变得有用,并确保我们在模型中所进行的分析能够转化为最终产品。
  2. 模型是团队所有成员使用的通用语言的中枢。由于模型与实现之间的关联,开发人员可以使用该语言来讨论程序。
  3. 模型是浓缩的知识。模型是团队一致认同的领域知识的组织方式和重要元素的区分方式。透过我们如何选择术语、分解概念以及将概念联系起来,模型记录了我们看待领域的方式。当开发人员和领域专家在将信息组织为模型时,这一共同的语言(模型)能够促使他们高效地协作。

当领域很复杂时,开发人员必须钻研领域以获取业务知识。他们必须磨砺其建模技巧,并精通领域设计。领域工作很繁杂,而且要求掌握很多复杂的新知识,而这些新知识看似对提高计算机科学家的能力并无裨益。

相反,技术人才更愿意从事精细的框架工作,试图用技术来解决领域问题。他们把学习领域知识和领域建模的工作留给别人去做。软件核心的复杂性需要我们直接去面对和解决,如果不这样做,则可能导致工作重点的偏离。

消化知识

有效建模的要素:

  1. 模型和实现的绑定。最初的原型虽然简陋,但是它在模型和实现之间建立了早期链接,而且在所有后续的迭代中一致维护该链接。
  2. 建立了一种基于模型的语言。随着项目的进展,无需翻译即可理解互相要表达的意思。
  3. 开发一个蕴含丰富知识的模型。对象具有行为和强制性规则。模型并不仅仅是一种数据模式,它还是解决复杂问题不可或缺的部分。模型包含各种类型的知识。
  4. 提炼模型。在模型日趋完整的过程中,重要的概念不断被添加到模型中,但同样重要的是,不再使用的或不重要的概念则从模型中被移除。
  5. 头脑风暴和实验。语言和草图,再加上头脑风暴活动,将讨论变成“模型实验室”

好的程序员会自然而然地抽象并开发出一个可以完成更多工作的模型。但如果在建模时只是技术人员唱独角戏,而没有领域专家的协作,那么得到的概念将是很幼稚的。使用这些肤浅知识开发出来的软件只能做基本工作,而无法充分反映出领域专家的思考方式。

在团队所有成员一起消化理解模型的过程中,他们之间的交互也会发生变化。领域模型的不断精化迫使开发人员学习重要的业务原理,而不是机械地进行功能开发。领域专家被迫提炼自己已知道的重要知识的过程往往也是完善其自身理解的过程,而且他们会渐渐理解软件项目所必需的概念严谨性。

高效率的团队需要有意识地积累知识,并持续学习。对于开发人员来说,这意味着既要完善技术知识,也要培养一般的领域建模技巧。那些善于自学的团队成员会成为团队的中坚力量,涉及最关键领域的开发任务要靠他们来攻克。

知识消化是一种探索,它永无止境。

交流与语言的使用

领域模型可成为软件项目通用语言的核心。该模型是一组来自于项目人员头脑中的概念,以及反映了领域深层次含义的术语和关系。这些术语和相互关系提供了模型语言的语义,虽然语言是为领域量身订制的,但就技术开发而言,其依然足够精确。

模式:Ubiquitous Language

  • 将模型作为语言的支柱。确保团队在内部的所有交流中以及代码中坚持使用这种语言。在画图、写东西,特别是讲话时也要使用这种语言。
  • 通过尝试不同的表示方法(它们反映了备选模型)来消除难点。然后重构代码,重新命名类、方法和模块,以便与新模型保持一致。解决交谈中的术语混淆问题,就像我们对普通词汇形成一致的理解一样。
  • 领域专家应该抵制不合适或无法充分表达领域理解的术语或结构,开发人员应该密切关注那些将会妨碍设计的有歧义和不一致的地方。

一个团队,一种语言

如果连经验丰富的领域专家都不能理解模型,那么模型一定出了什么问题。

绑定模型和实现

模式:Model-Driven Design

如果整个程序设计或者其核心部分没有与领域模型相对应,那么这个模型就是没有价值的,软件的正确性也值得怀疑。同时,模型和设计功能之间过于复杂的对应关系也是难于理解的,在实际项目中,当设计改变时也无法维护这种关系。若分析和设计之间产生严重分歧,那么在分析和设计活动中所获得的知识就无法彼此共享。

软件系统各个部分的设计应该忠实地反映领域模型,以便体现出这两者之间的明确对应关系。我们应该反复检查并修改模型,以便软件可以更加自然地实现模型,即使想让模型反映出更深层次的领域概念时也应如此。我们需要的模型不但应该满足这两种需求,还应该能够支持健壮的 Uniquitous Language(通用语言)。

完全以来模型的实现通常需要支持建模范式的软件开发工具和语言,比如面向对象的编程。

如果程序设计基于一个能够反映出用户和领域专家所关心的基本问题的模型,那么与其他设计方式相比,这种设计可以将其主旨更明确地展示给用户。让也用户了解模型,将使他们有更多机会挖掘软件的潜能,也能使软件的行为合乎情理、前后一致。

模式:Hands-on Modeler

如果编写代码的人员认为自己没必要对模型负责,或者不知道如何让模型为应用程序服务,那么这个模型就和程序没有任何关联。如果开发人员没有意识到改变代码就意味着改变模型,那么他们对程序的重构不但不会增强模型的作用,反而还会削弱它的效果。

任何参与建模的技术人员,不管在项目中的主要职责是什么,都必须花时间了解代码。任何负责修改代码的人员则必须学会用代码来表达模型。每一个开发人员都必须不同程度地参与模型讨论并且与领域专家保持联系。参与不同工作的人都必须有意识地通过 Ubiquitous Language 与接触代码的人即使交换关于模型的想法。

第二部分 模型驱动设计的构造块

分离领域

模式:Layered Architecture

在面向对象的程序中,常常会在业务对象中直接写入用户界面、数据库访问等支持代码。而一些业务逻辑则会被嵌入到用户界面组件和数据库脚本中。这么做是为了以最简单的方式在短期内完成开发工作。

如果领域有关的代码分散在大量的其他代码之中,那么查看和分析领域代码就会变得异常困难。对用户界面的简单修改实际上很可能会改变业务逻辑,而想要调整业务规则也很可能需要对用户界面代码、数据库操作代码或者其他的程序元素进行仔细的筛查。这样就不太可能实现一致的、模型驱动的对象了,同时也给自动化测试带来困难。考虑到程序中各个活动所涉及的大量逻辑和技术,程序本身必须简单明了,否则就会让人无法理解。

模式:The Smart UI “反模式”

许多软件项目都采用并且应该会继续采用一种不那么复杂的设计方法,称为 Smart UI(智能用户界面)。但是 Smart UI 是另一种设计方法,与领域驱动设计方法迥然不同且互不兼容。

如果一个经验并不丰富的项目团队要完成一个简单的项目,却决定使用 DDD 以及 Layered Architecutre,那么这个项目组将会经历一个艰难的学习过程。团队成员不得不去掌握复杂的新技术,艰难地学习对象建模。对基础设施和各层的管理工作使得原本简单的任务却要花费很长时间来完成。简单项目的开发周期较短,期望值也不是很高。所以,早在项目团队完成任务之前,该项目就会被取消,更谈不上去论证有关这种方法的许多种令人激动的可行性了。

领域驱动设计只有应用在大型项目上才能产生最大额收益,而这也确实需要高招的技巧。不是所有的项目都是大型项目;也不是所有的项目团队都能掌握那些技巧。

因此,当情况需要时:在用户界面中实现所有的业务逻辑。将应用程序分成小的功能模块,分别将它们实现成用户界面,并在其中嵌入业务规则。用关系数据库作为共享的数据存储库。使用自动化程度最高的用户界面创建工具和可用的可视化编程工具。

在项目中欧使用 Smart UI 之后,除非重写全部的应用模块,否则不能改用其他的设计方法。因此,如果你选择了这条路线,就应该采用与之匹配的开发工具。不要浪费时间去同时采用多种选择。只使用灵活的编程语言并不一定会创建出灵活的软件系统,反而有可能会开发出一个维护代价十分高昂的系统。

软件中所表示的模型

一个对象是用来表示某种具有连续性和标识的事物的呢,还是用于描述某种状态的属性呢?这是 Entity 与 Value Object 之间的根本区别。明确地选择这两种模式中的一个来定义对象,有利于减少歧义,并帮助我们做出特定的选择,这样才能得到健壮的设计。

领域中还有一些方面适合用动作或操作来表示,这比用对象表示更加清楚,这些方面最好用 Service 来表示,而不应把操作的责任强加到 Entity 或 Value Object 上。Service 是应客户端请求来完成某事。

关联

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

模型中每个可遍历的关联,软件中都要有同样属性的机制。

限定多对多关联的遍历方向可以有效地将其实现简化为一对多关联,从而得到一个简单得多的设计。

模式:Entity

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

一些对象主要不是由它们的属性定义的。它们实际上表示了一条“标志线”(A Thread of Identity),这条线跨越时间,而且常常经历多种不同的表示。有时,这样的对象必须与另一个具有不同属性的对象相匹配。而有时一个对象必须与具有相同属性的另一个对象区分开。错误的标识可能会破坏数据。

主要由标识定义的对象被称作 Entity。Entity(实体)有特殊的建模和设计思路。它们具有生命周期,这期间它们的形式和内容可能发生根本改变,但必须保持一种内在的连续性。为了有效地跟踪这些对象,必须定义它们的标识。它们的类定义、职责、属性和关联必须由其标志来决定,而不依赖于其所具有的属性。

当一个对象由其标识(而不是属性)区分时,那么在模型中应该主要通过标识来确定该对象的定义。使类定义变得简单,并集中关注生命周期的连续性和标识。定义一种区分每个对象的方式,这种方式应该与其形式和历史无关。要格外注意那些需要通过属性来匹配对象的需求。在定义标识操作时,要哦确保这种操作为每个对象生成唯一的结果哦,这可以通过附加一个保证唯一性的符号来实现。这种定义标识的方法可能来自外部,也可能是由系统创建的任意标识符,但它在模型中必须是唯一的标识。模型必须定义出“符合什么条件才算是相同的事物”。

模式:Value Object

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

跟踪 Entity 的标识是非常重要的,但为其他对象也加上标识会影响系统性能并增加分析工作,而且会使模型变得混乱,因为所有对象看起来都是相同的。

软件设计要时刻与复杂性作斗争。我们必须区别对待问题,仅在真正需要的地方进行特殊处理。然而,如果仅仅把这类对象当做没有标识的对象,那么就忽略了它们的工具价值或术语价值。事实上,这些对象有其自己的特征,对模型也有着自己的重要意义。这些是用来描述事物的对象。

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

常见的 Value Object 例子:颜色、字符串、数字(我们不会关心使用的是哪个“4”)。

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

模式:Service

有些重要的领域操作无法放到 Entity 或 Value Object 中。这当中有些操作从本质上讲是一些活动或动作,而不是事物。

当领域中的某个重要的过程或转换操作不是 Entity 或 Value Object 的自然职责时,应该在模型中添加一个作为独立接口的操作哦,并将其声明为 Service。定义接口时要使用模型语言,并确保操作名称是 Ubiquitous Language 中的术语。此外,应该使 Service 成为无状态的。

模式:Module

选择能够描述系统的 Module,并使之包含一个内聚的概念集合。这通常会实现 Module
之间的低耦合,但如果效果不理想,则应寻找一种更改模型的方式来消除概念之间的耦合,或者找到一个可作为 Module 基础的概念,基于这个概念组织的 Module 可以以一种有意义的方式将元素集中到一起。Module 的名称应该是 Ubiquitous Language 中的术语。Module 及其名称反映出领域的深层知识。

第三部分 通过重构来加深理解

要想成功地开发出实用的模型,需要注意以下 3 点:

  1. 复杂巧妙的领域模式是可以实现的,也是值得我们去花费力气实现的。
  2. 这样的模型离开不断地重构是很难开发出来的,重构需要领域专家和热爱学习领域知识的开发人员密切参与进来。
  3. 要实现并有效地运用模型,需要精通设计技巧。

柔性设计

当具有复杂行为的软件缺乏良好的设计时,重构或元素的组合会变得很困难。一旦开发人员不能十分肯定地预知计算的全部含义,就会出现重复。当设计元素都是整块的而无法重新组合的时候,重复就是一种必然的结果。

为了使项目能够随着开发工作进行加速前进,而不会由于它自己的老化停滞不前,设计必须要让人们乐于使用,而且易于做出修改。这就是柔性设计(supple design)。

如果开发人员为了使用一个组件而必须要去研究它的实现,那么就失去了封装的价值。当某个人开发的对象或操作被别人使用时,如果使用这个组件的新的开发者不得不根据其实现来推测其用途,那么他推测出来的可能并不是那个操作或类的主要用途。如果这不是那个组件的用途,虽然代码暂时可以工作,但设计的概念基础已经被误用了,两位开发人员的意图也是背道而驰。

在命名类和操作时要描述它们的效果和目的,而不要表露它们是通过何种方式达到目的的。这样可以使客户开发人员不必去理解内部细节。这些名称应该与 Ubiquitous Language 保持一致,以便团队成员可以迅速推断出它们的意义。

一旦你的设计中有了 Intention-Revealing Interface, Side-Effect-Free Fcuntion 和 Assertion,那么你就具备了使用声明式设计的条件。

第四部分 战略设计

大型系统领域模型的完全统一既不可行,也不划算。

模式:Bounded Context

细胞之所以能够存在,是因为细胞膜限定了什么在细胞内,什么在细胞外,并且确定了什么物质可以通过细胞膜。

任何大型项目都会存在多个模型。而当基于不同模型的代码被组合到一起后,软件就会出现 bug,变得不可靠和难以理解。团队成员之间的沟通变得混乱。人们往往弄不清楚一个模型应不应该在哪个上下文中使用。

明确得定义你行所应用的上下文。根据团队的组织、软件系统的各个部分的用法以及物理表现(代码和数据库模式等)来设置模型的边界。在这些边界中严格保持模型的一致性,而不要受到边界之外问题的干扰和混淆。

精炼

模型就是知识的精炼。

模式:Core Domain

对模型进行提炼。找到 Core Domain 并提供一种易于区分的方法把它与那些起辅助作用的模型和代码分开。最有价值和最专业的概念要轮廓分明。尽量压缩 Core Domain。

让最有才能得人来开发 Core Domain,并据此要求进行相应的招聘。在 Core Domain 中努力开发能够确保实现系统蓝图的深层模型和柔性设计。仔细判断任何其他部分的投入,看它是否能够支持这个提炼出来的 Core。

大型结构

在一个大的系统中,如果因为缺少一种全局性的原则而使人们无法根据元素在模式(这些模式被应用于整个设计)中的角色来解释这些元素,那么开发人员就会陷入“只见树木不见森林”的境地。

制定战略设计决策的 6 个要点:

  1. 决策必须传达到整个团队
  2. 决策过程必须收集反馈意见
  3. 计划必须允许演变
  4. 架构团队不必把所有最好、最聪明的人员都吸收进来
  5. 战略设计需要遵守简约和谦逊的原则
  6. 对象的职责要专一,而开发人员应该是多面手