BOOK 八月 25, 2024

《解构领域驱动设计》书摘

文章字数 17k 阅读约需 15 mins. 阅读次数

cover

豆瓣评分 7.3

第一篇 开篇

第1章 软件复杂度剖析

第2章 领域驱动设计概览

第3章 领域驱动设计统一过程

第二篇 全局分析

第4章 问题空间探索

图4-7 商业模式画布

商业模式画布由9个板块构成。

  • 客户细分(customer segments):企业所服务的一个或多个客户分类群体,可以是企业组织、最终用户等。
  • 价值主张(value propositions):通过价值主张来解决客户难题和满足客户需求,为客户提供有价值的服务。
  • 渠道通路(channels):通过沟通、分销和销售渠道向客户传递价值主张,即企业将销售的商品或服务交付给客户的方式。
  • 客户关系(customer relationships):在每一个客户细分市场建立和维护企业与客户之间的关系。
  • 收益来源(revenue streams):通过成功提供给客户的价值主张获得营业收入,是企业的盈利模式。
  • 核心资源(key resources):企业最重要的资产,也是保证企业保持竞争力的关键,这些资源包括人力和物力。
  • 关键业务(key activities):通过执行一些关键业务活动,运转企业的商业模式。
  • 重要合作(key partnership):需要从企业外部获得资源,就需要寻求合作伙伴。
  • 成本结构(cost structure):该商业模式要获得成功所引发的成本构成。

在询问问题时,选择板块的顺序是有讲究的:顺序代表了思考的方向、认知的递进,或者准确地说就是一种心流,是一种层层递进的因与果的驱动力,如图4-8所示。
图4-8 商业模式画布的驱动方向

针对目标系统而言,首先需要确定目标系统要帮助的各类型细分的客户,以更好地明确它的价值主张。然而,有了意向客户,也有了为客户提供的价值,又该如何将价值传递给客户呢?这就驱动出目标系统的渠道通路。渠道通路的形式取决于目标系统如何与客户互动,因而需定义出客户关系。理清楚了客户关系,就可以思考目标系统能够创造哪些收益,确定收益来源。至此,目标系统的方向与轮廓已大致确定,接下来需要考虑如何落地的问题。这时需要识别企业的可用资源,并与实现该目标系统需要的资源相比对,以确定核心资源。这些核心资源是为目标系统的关键业务服务的。要推进关键业务,只靠一家公司或一个团队独木难支,需要得到合作伙伴的帮助。最后,需要确定要实现该目标系统需要的成本,如此才能确定采用该商业模式的目标系统是否有利润可期。

第5章 价值需求分析

第6章 业务需求分析

经过领域驱动设计十多年的发展,社区就子领域达成了如下共识:

  • 子领域属于问题空间的范畴;
  • 子领域用于分辨问题空间的核心问题和次要问题。

第三篇 架构映射

架构映射对应解空间的战略设计层次。

本阶段,映射成为获得架构的主要设计手段。价值需求中利益相关者、系统愿景和系统范围可映射为系统上下文,业务服务通过对业务相关性的归类与归纳可映射为限界上下文,系统上下文与限界上下文共同构成了系统架构的重要层次,前者勾勒出解空间的控制边界,后者勾勒出领域模型的知识边界,组成了一个稳定而又具有演进能力的领域驱动架构。

限界上下文是架构映射阶段的基本架构单元,封装了领域知识的领域对象在知识语境的界定下,扮演不同的角色,执行不同的活动,对外公开相对完整的业务能力,此为限界上下文的定义。

第7章 同构系统

对于同一个目标系统,问题空间代表了真实世界的真实系统,解空间是一面镜子,利用求解过程照射的光,将这一真实的系统映成理念世界的虚拟系统。虚拟系统是通过对真实世界的概念进行抽象与提炼获得的:如果求解的光足够明亮,解空间这面镜子足够光滑而平整,映出的虚拟系统就足够逼真。除了系统的本质不同,真实系统和虚拟系统的结构应保持一致,形成两个“同构”的系统。侯世达认为所谓同构系统,就是“两个复杂结构可以互相映射,并且每一个结构的每一部分在另一个结构中都有一个相应的部分”,这说明,同构系统的组成部分可以形成一一对应的映射关系,这正是架构映射的存在前提。

IEEE 1471对架构的定义为:“架构是以组件、组件之间的关系、组件与环境之间的关系为内容的某一系统的基本组织结构,以及指导上述内容设计与演化的原则。”

RUP 4+1视图模型的提出者Philippe Kruchten对架构的定义为:“软件架构包含了关于以下内容的重要决策:软件系统的组织;选择组成系统的结构元素和它们之间的接口,以及当这些元素相互协作时所体现的行为;如何组合这些元素,使它们逐渐合成为更大的子系统;用于指导这个系统组织的架构风格:这些元素以及它们的接口、协作和组合。”

卡内基·梅隆大学软件工程研究院的Len Bass等人则将架构定义为:“系统的软件架构是对系统进行推演获得的一组结构,每个结构均由软件元素、这些元素的关系以及它们的属性组成。”

概括而言,一个设计良好的架构应具有如下基本设计要素:

  • 功能分解的软件元素;
  • 软件元素之间的关系;
  • 软件元素与外部环境之间的关系;
  • 指导架构设计与演化的原则。

第8章 系统上下文

第9章 限界上下文

模块:先从技术维度进行横向切分,再从领域维度针对领域层进行纵向切分。业务模块仅包含业务逻辑,需要其他层模块的支持才能提供完整的业务能力。这样的架构没有将业务架构、应用架构、数据架构绑定起来,一旦业务发生变化,就会影响到横向层次的各个模块。

限界上下文:先从领域维度进行纵向切分,再从技术维度对限界上下文进行横向切分,因此限界上下文是一个对外暴露业务能力的架构整体。无论是业务架构、应用架构,还是数据架构,都在一个边界中,一旦业务发生变化,只会影响到与该业务相关的限界上下文。

正交性要求:“如果两个或更多事物中的一个发生变化,不会影响其他事物,这些事物就是正交的。”变化的影响主要体现在变化的传递性,即一个事物的变化会传递到另一个事物引起它的变化,但这个变化影响并不包含彼此正交的点。例如,限界上下文之间存在调用关系,当被调用的限界上下文公开的接口发生变化,自然会影响调用方。这一影响是合理的,也是软件设计很难避免的依赖。故而限界上下文存在正交性,指的是各自边界封装的业务知识不存在变化的传递性。

领域模型违背了正交性,意味着各自定义的领域模型对象代表的领域概念出现了重复。

判断领域模型的重复性,必须将限界上下文作为修饰,将二者组合起来共同评判。

第10章 上下文映射

随着变化不断发生,难免会在协作过程中产生边界的裂隙,导致限界上下文之间产生无人管控的灰色地带。当灰色地带逐渐陷入混沌时,就需要引入上下文映射(context map)让其恢复有序。

上下文映射图将提供服务的限界上下文称为“上游”上下文,与之对应,消费(调用)服务的限界上下文自然称为“下游”上下文。

Eric Evans定义的上下文映射模式包括客户方/供应方、共享内核、遵奉者、分离方式、防腐层、开放主机服务与发布语言。

随着领域驱动设计社区的发展,又诞生了合作关系模式与大泥球模式。

防腐层和开放主机服务都是访问领域模型时建立的一层包装,前者针对发起调用的下游,后者针对响应请求的上游,以避免上下游之间的通信集成将各自的领域模型引入进来,造成彼此之间的强耦合。因此,防腐层和开放主机服务操作的对象都不应该是各自的领域模型,这正是引入发布语言的原因。

任何软件设计决策都要考量成本与收益,只有收益高于成本,决策才是合理的。

既然双方都需要Money值对象,要遵循分离方式模式,就可以通过在两个限界上下文中重复定义Money值对象来完成解耦。不要害怕这样的重复,在领域驱动设计中,我们遵循的原则应该是“只有在一个限界上下文中才能消除重复”

设计总是如此,没有绝对好的解决方案,只能依据具体的业务场景权衡利弊得失,以求得到相对好(而不是最好)的方案。这是软件设计让人感觉棘手的原因,却也是它的魅力所在。

只有当一个领域行为成为另一个领域行为“内嵌”的执行步骤,二者操作的领域逻辑分属不同的限界上下文,才会产生真正的协作,形成除“分离方式”之外的上下文映射模式。

第11章 服务契约设计

Eric Evans定义了领域驱动设计的分层架构,在领域层和用户界面层之间引入了应用层:“应用层要尽量简单,不包含业务规则或者知识,而只为下一层(指领域层)中的领域对象协调任务,分配工作,使它们互相协作。”

应用层定义的内容主要为应用服务(application service),它是外观(facade)模式的体现,即“为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用”

应用服务设计的第一条准则:不包含领域逻辑的业务服务应被定义为应用服务。

应用服务设计的第二条准则:与横切关注点协作的服务应被定义为应用服务。

第12章 领域驱动架构

依赖倒置原则(dependency inversion principle,DIP)提出了对自顶向下依赖的挑战,要求高层模块不应该依赖于低层模块,二者都应该依赖于抽象。

因此,依赖倒置原则隐含的本质是,我们要依赖不变或稳定的元素(类、模块或层),也就是该原则的第二句话:抽象不应该依赖于细节,细节应该依赖于抽象。

倘若高层依赖于低层的抽象,就必然面对一个问题:如何将具体的实现传递给高层的类?在高层通过接口隔离了对具体实现的依赖,意味着这个具体依赖被转移到了外部,究竟使用哪一种具体实现,应由外部的调用者决定,只有在运行调用者代码时,才将外面的依赖传递给高层的类。Martin Fowler形象地将这种机制称为依赖注入(dependency injection)。

面向接口设计带来了低层实现对高层抽象的依赖,观察者模式带来了低层主体对高层观察者的依赖。它们都体现了分层架构中低层对高层的依赖,颠覆了固有思维形成的自顶向下的依赖方向。

Eric Evans指出:“一个严峻的现实是我们不可能对所有设计部分进行同等的精化,而是必须分出优先级。”

第四篇 领域建模

第13章 模型驱动设计

第14章 领域分析建模

第15章 领域模型设计要素

设计元模型规定:只能由实体、值对象、领域服务和领域事件表示模型,如此即可避免将领域逻辑泄露到领域层外的其他地方

聚合用于封装实体和值对象,并维持自己边界内所有对象的完整性。要访问聚合,只能通过聚合根的资源库,这就隐式地划定了边界和入口,有效控制了聚合内所有类型的领域对象。若聚合的创建逻辑较为复杂或存在可变性,可引入工厂来创建聚合内的领域对象。若牵涉到实体的状态变更,领域元模型建议通过领域事件来推动。

领域驱动设计将这些行为分配给了专门的资源库对象,实体无须承担“增删改查”的职责。实体拥有的变更状态的领域行为,修改的只是对象的内存状态,与持久化无关。

一个领域概念到底该用值对象还是实体类型,第一个判断依据是看业务的参与者对它的相等判断是依据值还是依据身份标识。——前者是值对象,后者是实体。

第二个判断依据是确定对象的属性值是否会发生变化,如果变化了,究竟是产生一个完全不同的对象,还是维持相同的身份标识。——前者是值对象,后者是实体。

最后一个判断依据是生命周期的管理。值对象没有身份标识,意味着无须管理其生命周期。从对象的角度看,它可以随时被创建或被销毁,甚至也可以被随意克隆用到不同的业务场景。实体则不同,在创建之后,系统就需要负责跟踪它状态的变化情况,直到它被删除。有的对象虽然通过值进行相等性判断,但在具体业务场景中,又可能面对生命周期管理的需求。这时,就需要将该对象定义为实体。

值对象的名称容易让人误会它只该拥有值,不应拥有领域行为。实际上,只要采用了对象建模范式,无论实体对象还是值对象,都需要遵循面向对象设计的基本原则,如信息专家模式,将操作自身数据的行为分配给它。Eric Evans之所以将其命名为值对象,是为了强调对它的领域概念身份的确认,即关注重点在于值。

值对象拥有的往往是“自给自足的领域行为”。这些领域行为能够让值对象的表现能力变得更加丰富,更加智能。它们通常为值对象提供如下能力:

  • 自我验证;
  • 自我组合;
  • 自我运算。

自我验证的领域行为仅验证外部传入的设置值。倘若验证功能还需求助外部资源,例如查询数据库以检查name是否已经存在,这样的验证逻辑就不再是“自给自足”的,不能交由值对象承担。

图15-9 对象网

图15-10 对象社区组成的对象图

图15-11 简化的对象模型

图15-12 由主对象构成的对象模型

Eric Evans将这种类层次的边界称为聚合,边界内的主对象称为聚合根。

Eric Evans阐释了何谓聚合(aggregate)模式:“将实体和值对象划分为聚合并围绕着聚合定义边界。选择一个实体作为每个聚合的根,并允许外部对象仅能持有聚合根的引用。作为一个整体来定义聚合的属性和不变量,并将执行职责赋予聚合根或指定的框架机制。”这一定义说明了聚合的基本特征。

  • 聚合是包含了实体和值对象的一个边界。
  • 聚合内包含的实体和值对象形成一棵树,只有实体才能作为这棵树的根。这个根称为聚合根(aggregate root),这个实体称为根实体(root entity)。
  • 外部对象只允许持有聚合根的引用,以起到边界的控制作用。
  • 聚合作为一个完整的领域概念整体,其内部会维护这个领域概念的完整性,体现业务上的不变量约束。
  • 由聚合根统一对外提供履行该领域概念职责的行为方法,实现内部各个对象之间的行为协作。

在进行领域设计建模时,我们往往以根实体的名称指代整个聚合,如一个聚合的根实体为订单,则称其为订单聚合。但这并不意味着存在一个订单聚合对象。聚合是边界,不是对象。订单根实体本质上仍然属于实体类型。

我们必须厘清面向对象的聚合(object oriented聚合,OO聚合)与领域驱动设计的聚合(DDD聚合)之间的区别。

也不能将OO合成与DDD聚合混为一谈。

OO聚合与OO合成代表了类与类之间的组合关系,体现了整体包含了部分的意义。DDD聚合是边界,它的边界内可以只有一个实体对象,也可以包含一些具有关联关系、泛化关系和依赖关系的实体与值对象。

对比完整性与独立性,我认为:当聚合边界存在模糊之处时,小聚合显然要优于大聚合。换言之,独立性对聚合边界的影响要高于完整性。

论及聚合的协作,无非就是判断彼此之间的引用采用什么形式。形式分为两种:

  • 聚合根的对象引用;
  • 聚合根身份标识的引用。

只要存在依赖关系的聚合位于同一个限界上下文,就应该允许一个聚合的根实体直接引用另一个聚合的根实体,以形成良好的行为协作。

一个聚合作为另一个聚合方法的参数,就会形成职责的委派。

一个聚合创建另外一个聚合,就会形成实例化(instantiate)的依赖关系。这实际是工厂模式的运用,牵涉到对聚合生命周期的管理。

领域模型对象的主力军是实体与值对象。这些实体与值对象又被聚合统一管理起来,形成一个个具有一致生命周期的“命运共同体”自治单元。管理领域模型对象的生命周期,实则就是管理聚合的生命周期。

图15-20 聚合的生命周期

在Java语言中,可以为每个聚合建立一个包(package),除聚合根之外,聚合内的其他实体和值对象的构造函数皆定义为默认访问修饰符。一个聚合一个包,位于包外的其他类就无法访问这些对象的构造函数。

当我们针对领域行为建模时,需要优先考虑使用值对象和实体来封装领域行为,只有确定无法寻觅到合适的对象来承担时,才将该行为建模为领域服务的方法。领域服务是领域设计建模的最后选择。

回想UML中的状态图以及工作流的状态机(state machine),再来思考业务世界的本质,我们能否提出如下问题:任何业务逻辑是否都可以转换成状态的迁移?

作为已经发生的事实,事件的命名应采用动词的过去时态,如订单完成的事件命名为OrderCompleted。这一命名方式也是领域事件推荐的命名风格,我们无须再为其增加Event后缀。

第16章 领域设计建模

实体、值对象、领域事件、聚合、工厂、领域服务与资源库都属于领域驱动战术设计元模型的一部分,是解决真实世界业务问题的设计元语。实体、值对象与领域事件共同构成了描述真实世界业务问题的基本要素;聚合从设计角度为实体与值对象圈定了概念边界,并引入了工厂和资源库设计模式,用于管理聚合的生命周期;领域服务作为聚合的补充,专注于领域行为的表达,负责协调聚合之间以及聚合与端口之间的协作。

ZenUML工具能够自动将脚本转换为序列图,还允许设置各种角色构造型的颜色,如此即可方便地以可视化图形展现角色构造型的协作序列,图16-29就是转换上述脚本获得的序列图。
图16-29 ZenUML工具生成的序列图

第17章 领域实现建模

软件设计与开发的过程是不可分割的,那种企图打造软件工程流水线的代码工厂运作模式,已被证明难以奏效。

第五篇 融合

第18章 领域驱动设计的战略考量

Feign接口除了不强制规定方法名,接口方法的输入参数与返回值必须与上游远程服务的接口方法保持一致。一旦上游远程服务的接口定义发生了变更,就会影响到下游客户端,这实际上削弱了南向网关引入端口的价值。

ServiceComb Pack属于微服务平台Apache ServiceComb的一部分,为分布式柔性事务提供了整体解决方案,能够同时支持Saga与TCC模式。

ServiceComb Pack通过gRPC与Kyro序列化来实现微服务之间的分布式通信。它包含了两个组件:Alpha与Omega。Alpha作为Saga协调者,负责管理和协调事务。Omega是内嵌到每个参与服务的引擎,可以拦截调用请求并向Alpha上报事务事件。

第19章 领域驱动设计的战术考量

可以认为,如果一个模块定义的对象皆为POJO,那么除了依赖JDK,它不会依赖任何框架或平台。

一个POJO如果遵循了Java Bean的设计规范,可以成为一个Java Bean,但并不意味着POJO一定是Java Bean。反过来,一个Java Bean如果没有依赖任何框架,也可以认为是一个POJO,但Enterprise Java Bean一定不是一个POJO。

POJO可以封装业务逻辑,Java Bean的规范也没有限制它不能封装业务逻辑。

面向对象设计的关键原则,即“数据与行为应该封装在一起”。

与贫血领域模型相对的是富领域模型(rich domain model),也就是封装了领域逻辑的领域模型。

有了Martin Fowler对贫血模型的创造,所谓的“血”就用来指代领域逻辑,故而有人在贫血模型的基础上衍生出各种与“血”有关的各种模型,如失血模型、充血模型和胀血模型。这些模型非但没有进一步将领域模型的正确定义阐述清楚,反而引入太多的概念造成领域模型的混乱不清。

有的观点认为混入了持久化能力的领域模型属于充血模型,这更进一步模糊了×血模型的边界。实际上,Martin Fowler将这种具有持久化能力的领域对象称为活动记录(active record),属于数据源架构模式(data source architectural pattern)。采用这种设计模式并非不可,但在领域驱动设计中,却需要努力避免:如果每个实体都混入了持久化能力,聚合的边界就失去了保护作用,资源库也就没有存在的价值了。

为避免太多定义造成领域模型定义的混乱,我建议回归Martin Fowler对领域模型定义的本质,仅分为两种模型:贫血领域模型与富领域模型,后者需要遵循合理的职责分配,避免一个领域模型对象承担的职责过多。

  • 领域模型对象包含实体、值对象、领域服务和领域事件,有时候也可以单指组成聚合的实体与值对象。
  • 领域模型必须是富领域模型。
  • 远程服务与应用服务接口的输入参数和返回值为遵循DTO模式的消息契约模型,若客户端为前端UI,则消息契约模型又称为视图模型。
  • 领域模型对象中的实体与值对象同时作为持久化对象。
  • 只有资源库对象,没有数据访问对象。资源库对象以聚合为单位进行领域模型对象的持久化,事件存储对象则负责完成领域事件的持久化。

由于单元测试和集成测试的反馈速度不同,且后者还要依赖于真实的数据库环境,因此建议在项目工程中分离单元测试和集成测试,例如在Java项目中使用Maven的failsafe插件。该插件规定了集成测试的命名规范,如规定集成测试类以*IT结尾,只有执行mvn integration-test命令才会执行这些集成测试。

第20章 领域驱动设计体系

领域驱动设计一直强调的核心思想,就是对边界的界定与控制。

问题空间通过核心子领域、通用子领域和支撑子领域进行分解,以更加清晰地呈现问题空间,同时降低问题空间的复杂度。子领域确定的边界是领域驱动设计问题空间的第二重边界

领域驱动设计问题空间的两重边界属于分析边界。

架构映射阶段,利用组织级映射获得的系统上下文成了领域驱动设计解空间的第一重边界。通过系统上下文明确哪些属于目标系统,哪些属于伴生系统,即可清晰地表达当前系统与外部环境之间的关系、确定解空间的规模大小。

通过业务级映射获得的限界上下文是领域驱动设计解空间的第二重边界,可以有效地降低系统规模。

在限界上下文内部,网关层与领域层的隔离成了领域驱动设计解空间的第三重边界。

领域模型引入了聚合这一最小的设计单元。它从完整性与一致性对领域模型进行了有效的隔离,成了领域驱动设计解空间的第四重边界。

领域驱动设计解空间的四重边界属于设计边界。

图20-1 问题空间的分析边界与解空间的设计边界

“领域驱动设计是一种纪律,”

“领域驱动设计本身没有多难,知道了方法的话,认真建模一次还是好搞的,但是持续地保持这个领域模型的更新和有效,并且坚持在工作中用统一语言来讨论问题是很难的。纪律才是关键。”

结合领域驱动设计的知识体系和统一过程,我总结了领域驱动设计的“三大纪律八项注意”,可作为团队的纪律规范。

  • 三大纪律:
  1. 领域专家与开发团队在一起工作;
  2. 领域模型必须遵循统一语言;
  3. 时刻坚守两重分析边界与四重设计边界。
  • 八项注意:
  1. 问题空间与解空间不要混为一谈;
  2. 一个限界上下文不能由多个特性团队开发;
  3. 跨进程协作通过远程服务,进程内协作通过应用服务;
  4. 保证领域分析模型、领域设计模型与领域实现模型的一致;
  5. 不要将领域模型暴露在网关层之外,共享内核除外;
  6. 先有领域模型,后有数据模型,保证二者的一致;
  7. 聚合的关联关系只能通过聚合根ID引用;
  8. 聚合不能依赖访问外部资源的南向网关。

图20-50 EAS的代码模型

考虑到集成测试需要准备测试环境,且它的执行效率也要低于单元测试,故而需要将单元测试和集成测试分为两个不同的构建阶段。

附录A 领域建模范式

他明确地给出了一个简洁的范式定义:“按既定的用法,范式就是一种公认的模型或模式。”

倘若将范式运用在软件领域的建模过程中,就可以认为建模范式是建立模型的一种模式,是针对业务需求提出的问题进行建模时需要遵循的规则。

米特法则(Law of Demeter)

要求任何一个对象或者方法,只能调用下列对象:

  • 该对象本身;
  • 作为参数传进来的对象;
  • 在方法内创建的对象。

Jeff Bay总结了优秀软件设计的9条规则,其中一条规则为“不使用任何getter/setter/property”。

Jeff Bay认为:“如果可以从对象之外随便询问实例变量的值,那么行为与数据就不可能被封装到一处。在严格的封装边界背后,真正的动机是迫使程序员在完成编码之后,一定有为这段代码的行为找到一个适合的位置,确保它在对象模型中的唯一性。”

命令而非询问(tell, don’t ask)原则。这个原则要求一个对象应该命令其他对象做什么,而不是去查询其他对象的状态来决定做什么。

0%