El Psy Congroo

领域驱动设计读书笔记 PART I & II

Part 1 运用领域模型

模型

定义
对现实的简化,忽略无关的细节,把与解决问题密切相关的方面抽象出来。概括而非模拟。
作用
模型和设计的核心相互影响,对模型的分析转化为产品,相反可以基于对模型的理解来解释代码
模型是团队所有成员使用的通用语言的中枢(ubiquitous language)
模型是浓缩的知识

软件的核心

为用户解决领域相关的问题的能力

不要试图仅通过技术来解决领域问题,学习领域知识和领域建模是解决复杂性的必经之路

有效建模的要素

  • 模型和实现绑定,在后续迭代中持续维护这个绑定关系
  • 建立通用语言
  • 开发一个蕴含丰富知识的模型。模型不仅仅是一种数据模式,还需要包含对象的行为和规则
  • 提炼模型。添加重要的概念,移除不重要或废弃的概念
  • 头脑风暴和实验

Chapter 1 消化知识

在开发人员领导下,由开发人员与领域专家组成的团队协作完成

传统方法的缺陷

  • 瀑布方法
    分析人员与业务专家进行讨论,得到的知识抽象后传递给开发人员。知识单向流动,缺乏反馈,并且难以积累。

  • 迭代方法
    开发人员听业务专家描述所需的特性,构建完成后展示结果并询问下一步动作。没有对知识进行抽象,无法建立知识体系,最终开发出的软件只是功能的堆砌。好的程序员会自发的抽象并开发出一个整洁并易于扩展的模型,但如果建模过程是开发人员独自完成的,得到的概念是很幼稚的,无法充分反映出领域专家的思考方式。

因此团队成员一起消化理解模型是必需的,提炼模型的过程会迫使开发人员学习重要的业务原理,而非机械的进行功能开发,同时使领域专家在提炼重要知识的过程中完善其自身的理解,并了解到软件项目所必需的概念严谨性。
开发人员的知识使模型更严密,抽象更整洁,从而易于实现。领域专家的知识使模型反映了业务的深层次知识,使真正的业务原则得到抽象。

持续学习

开始编写软件时,有大量的unknown unknowns,项目过程中,知识也在不断丢失,例如团队成员离开。因此高效率的团队需要持续学习并有意识的累积知识,这就要求开发人员认真学习领域知识并培养领域建模技巧。

知识丰富的设计

建模不仅仅是发现名词,业务活动和规则与实体和值对象一样,都是领域的核心。包括解决规则之间的矛盾,弥补规则的不足,消除无用的规则。模型改变时,对实现进行重构以反映模型的变化,由此将新的知识合并到系统中。

  • Case:提取隐藏的概念
    需求:航运业允许10%的超售
    实现:判断是否超载的函数中返回cargoSize > 110% * maxSize
    缺陷:一个重要逻辑被隐藏在一行代码中
    修改:将超售策略作为模型中的一条规则,对应的在系统中使用strategy模式实现

通过领域模型和相应的设计来保护和共享知识
设计更明确,团队成员必须理解超售的本质,并且明白这是一个重要的业务规则,而非一个不起眼的计算
开发人员可以向业务专家展示设计结果,以便形成反馈闭环

深层模型

有用的模型很少停留在表面,需要通过持续的知识消化过程,不断加深对领域和需求的理解,发掘出深层模型。

Chapter 2 交流和语言的使用

Ubiquitous Language(通用语言)

项目中的公共语言,以模型作为语言的支柱,在所有内部交流和代码中使用这种语言。对ubiquitous language的更改就是对模型的更改,反之亦然。避免交流过程中翻译和误解带来的成本。
词汇包括类和主要操作的名称。

持续使用ubiquitous language可以暴露出模型的不足,例如术语的缺失或是错误,通过引入和提炼术语,可以逐步得到一个完整的易于理解的模型。注意:语言上的更改除了影响领域模型,还应在实现层面做出相应改变。

使用过程中,领域专家应抵制不合适或无法充分表达领域理解的术语或结构,开发人员应密切关注那些会妨碍设计的有歧义和矛盾的地方。

  • Case:制定货运路线
    不使用领域模型
    用户:那么,当更改清关地点时,需要重新制定整个路线计划啰。
    开发人员:是的。我们将从货运表(shipment table)中删除所有与该货物id相关联的行,然后将出发地、目的地和新的清关地点传递给Routing Service,它会重新填充货运表。Cargo中必须设立一个布尔值,用于指示货运表中是否有数据。
    使用领域模型
    开发人员:是的。当更改Route Specification(路线说明)的任意属性时,都将删除原有的Itinerary(航线),并要求Routing Service(路线服务)基于新的Route Specification生成一个新的Itinerary。

讨论系统时要结合模型。使用模型元素及其交互来大声描述场景,并且按照模型允许的方式将各种概念结合到一起。找到更简单的表达方式来讲出你要讲的话,然后将这些新的想法应用到图和代码中。

一个团队,一种语言

文档和图

图作为一种沟通和解释手段,可以促进头脑风暴。UML图善于表达对象的关系和属性,以及交互,但不善于传达模型中的两个重要部分,模型所表示的概念的意义,以及对象该做那些事(行为的职责和约束),因此作者建议以文本为主,用简化图(只包含对象模型的重要概念-对于理解设计至关重要的部分)作为说明,帮助集中注意力,并起到指导作用。相反涵盖整个模型的大图会让人淹没在细节中,缺乏目的性。

注意:务必要记住模型不是图。图的目的是帮助表达和解释模型。模型设计的重要细节应该在代码中体现出来,良好的实现应该是透明的,清楚展示其背后的模型。用代码充当设计细节的存储库,书写良好的Java代码与UML具有同样的表达能力。

文档

代码作为设计文档会将读代码的人淹没在细节中。代码的行为是非常明确的,但不意味着其行为是显而易见的,行为背后的意义也难以表达。良好的代码具有很强的表达能力,但它所传递的信息不能确保是准确的。例如方法名称可能会有歧义、会产生误导或者因为已经过时而无法表示方法的本质含义。测试中的断言是严格的,但变量和代码组织方式所表达出来的意思未必严格。好的编程风格会尽力使这种联系直接化,但其仍然主要靠开发人员的自律。编码时需要一丝不苟的态度,只有这样才能编写出“言行全部正确”的代码。

口头交流很短暂且范围很小,不利于在团队范围内共享知识

文档作为代码和口头交流的补充,澄清设计意图,解释模型的概念,帮助理解大尺度结构,在代码的细节中指引方向,但不应该再重复表示代码已经明确表达出的内容。

保持鲜活:观察文档和ubiquitous language是否保持一致,如果文档中的术语不再出现在讨论和代码中,说明文档就没有起到作用,要不是是太复杂,要不是没有关注到重点。

通过将文档减至最少,并且主要用它来补充代码和口头交流,就可以避免文档与项目脱节。根据ubiquitous language及其演变来选择那些需要保持更新并与项目活动紧密交互的文档。

解释性模型

本书的核心思想是在实现、设计和团队交流中使用同一个模型作为基础。如果各有各的模型,将会造成危害。
但为了学习领域,还可以引入其他视图,解释性模型从不同的角度及方式来呈现领域,有助于更好的学习

Chapter 3 绑定模型和实现

领域驱动设计要求模型不仅能指导分析工作,还应该成为设计的基础。

模式:Model-Driven Design

如果没有领域模型,仅通过代码来堆砌功能,那就无法利用前面提到的知识消化和沟通带来的好处,如果涉及复杂的领域,最终得到的产品会充斥着大量的功能,难以理解与维护。
如果在使用领域模型的过程中,将分析模型与设计分离,分析模型仅作为理解工具。由于分析模型不能指导设计,导致开发人员需要重新对设计进行抽象,过程中必然会损失分析过程中的领域知识,设计过程中获得的知识也无法反馈。

模型驱动设计寻求一个满足分析和程序设计两方面需求的单一模型,以达到绑定模型和实现的要求。但是这种绑定不会因为技术考虑而削弱分析的功能,也不接受只反映领域概念却舍弃软件设计原则的拙劣设计。

软件系统各个部分的设计应该忠实地反映领域模型,以便体现出这二者之间的明确对应关系。我们应该反复检查并修改模型,以便软件可以更加自然地实现模型,即使想让模型反映出更深层次的领域概念时也应如此。我们需要的模型不但应该满足这两种需求,还应该能够支持健壮的UBIQUITOUS LANGUAGE(通用语言)。从模型中获取用于程序设计和基本职责分配的术语。让程序代码成为模型的表达,代码的改变可能会是模型的改变。

  • Case:通过bus绑定net(从过程设计到模型驱动设计)

模式:Hands-on Modeler

人们总是把软件开发比喻成制造业。这个比喻的一个推论是:经验丰富的工程师做设计工作,而技能水平较低的劳动力负责组装产品。这种做法使许多项目陷入困境,原因很简单——软件开发就是设计

如果编写代码的人员认为自己没必要对模型负责,或者不知道如何让模型为应用程序服务,那么这个模型就和程序没有任何关联。如果开发人员没有意识到改变代码就意味着改变模型,那么他们对程序的重构不但不会增强模型的作用,反而还会削弱它的效果。同样,如果建模人员不参与到程序实现的过程中,那么对程序实现的约束就没有切身的感受,即使有,也会很快忘记。MODEL-DRIVEN DESIGN的两个基本要素(即模型要支持有效的实现并抽象出关键的领域知识)已经失去了一个,最终模型将变得不再实用。最后一点,如果分工阻断了设计人员与开发人员之间的协作,使他们无法转达实现MODEL-DRIVEN DESIGN的种种细节,那么经验丰富的设计人员则不能将自己的知识和技术传递给开发人员。

HANDS-ON MODELER(亲身实践的建模者)并不意味着团队成员不能有自己的专业角色。但是如果把MODEL-DRIVEN DESIGN中密切相关的建模和实现这两个过程分离开,则会产生问题。

整体设计的有效性有几个非常敏感的影响因素——那就是细粒度的设计和实现决策的质量和一致性。在MODEL-DRIVEN DESIGN中,代码是模型的表达,改变某段代码就改变了相应的模型。程序员就是建模人员,无论他们是否喜欢。所以在开始项目时,应该让程序员完成出色的建模工作。

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

注意:大型项目仍然需要技术负责人来协调高层次的设计和建模,并作出关键决策(见Part 4 战略设计)

项目组通过知识消化将信息提炼成模型,MODEL-DRIVEN DESIGN将模型和程序实现绑定,UBIQUITOUS LANGUAGE则成为开发人员、领域专家和软件产品之间传递信息的渠道。最终的软件产品能在完全理解核心领域的基础上提供丰富的功能。

Part 2 模型驱动设计的构造块

本书采用的软件设计风格:
职责驱动设计
契约式设计

Chapter 4 分离领域

将用于解决领域问题的部分(通常只占一小部分)与系统其他功能分离,避免将领域概念和其他技术相关的概念混淆在一起

Layered Architecture 分层架构

  • 用户界面层/表示层:界面或接口
  • 应用层:定义软件要完成的任务,并通过指挥表达领域概念的对象来解决问题。不包含业务规则或知识。
  • 领域层/模型层:业务软件的核心。负责表达业务概念,状态信息及规则。
  • 基础设施层:为上面三层提供通用的技术支持。包括为领域层提供持久化机制,为应用层传递消息等。

通过给复杂应用划分层次,在每层分别进行设计,使其具有内聚性并只依赖下层,与上层松耦合。重点在于将领域模型相关的代码放在一个层中,专注于表达领域模型,而不用关心界面、应用以及持久化等。

  • Case:银行转账

层与层的依赖是单向的,上层通过接口调用下层元素,如果下层元素需要与上层进行主动通信,则需要采用架构模式来连接上下层,如回调或观察者模式,一个典型的例子是MVC(70年代为Smalltalk发明的设计模式)。层与层之间连接的原则是保持领域层的独立性,在选择架构框架时也要考虑这一点,避免过多的侵入性。

Chapter 5 软件中表示的模型

关联

对象之间的关联使模型和实现变得更复杂,模型层面关联代表两个对象的关系的抽象,实现层面关联相当于对象之间的引用。现实中存在大量多对多及双向的关联,但这样的关联大都并不代表关系的本质。因此我们需要简化这样的关联,方法有如下三种

  • 规定一个遍历方向,将多对多关联简化为一对多关联
  • 添加一个限定符,减少一对多关联
  • 消除不必要的关联

Entity(Reference Object):用来表示某种具有连续性和标识的事物,例如帐户
Value Object:用来描述某种状态的属性,例如账单地址
Service:领域中适合用操作来表示的概念,例如转账,不要把操作的职责强加到Entity或Value Object中

模式:Entity

职责:确保连续性,使对象行为清楚且可预测
注意点:保持entity简练,仅保留用于识别查找或匹配对象的特征,以及对概念至关重要的行为和这些行为所必需的属性。考虑将行为和属性转移到关联的对象中(可以是Entity或VO)。
标识设计:确保在系统中唯一,通过唯一性的属性或属性组合,但一般是数据库ID,在分布式系统中会复杂些,需要专门的ID生成服务

模式:Value Object(VO)

职责:描述事务的某种特征
注意点:不需要加上标识,这只会带来额外的复杂度,对于VO我们只关心它们是什么,而不关心它们是谁。VO所包含的属性应该形成一个概念整体。

设计VO
不用关心使用的是哪个VO实例,设计时不受ID的约束。从简化设计或优化性能角度出发,常用的选择包括:
复制:例如分布式系统中共享成本较高,以及数据库中冗余减少访问次数
共享:节约空间及对象维护成本
Immutable:简化实现,确保共享的安全性。除非维护Immutable性能开销大并且不共享,否则VO都应该是不可变的

模式:Service

职责:代表领域中那些不是Entity或VO的自然职责的操作或过程
注意点:Service是无状态的。接口及操作名称要使用ubiquitous language。Service可能属于多个层。

粒度
中等粒度:Service
细粒度:Entity,VO
中等粒度的领域层Service避免了应用层需要处理过于复杂的逻辑(从而导致领域知识泄露),并且增加了复用性。

模式:Module(Package)

职责:从高层对模型进行概念划分,包含一个内聚的概念集合。
注意点:高内聚低耦合。Module需要同模型和代码一起演变,即重构代码时也要重构Module。

技术模型分层
接口/功能/持久层
优点:更容易在技术层面理解各层
缺点:将实现概念对象的元素分散到各层,代码无法清楚的表示模型,最终得到一个贫血的领域模型。

尽量将实现单一概念对象的所有代码放在同一个模块中!

建模范式

使用面向对象范式作为MODEL-DRIVEN DESIGN的核心是目前的主流,并不意味着应该局限于面向对象技术,例如数学问题就不适合使用对象来建模。

混合范式
即使用合适的范式对领域中的某些部分进行建模,例如对象范式中引入规则引擎,但是引入混合范式后,我们必须找到一个适用于两种范式的单一模型,否则规则和对象就会被割裂开。

尽量避免使用多范式,例如规则不复杂时可以使用对象来建模规则,使用多范式时把UBIQUITOUS LANGUAGE作为依靠的基础,防止各个部分设计分裂。

Chapter 6 领域对象的生命周期

在整个生命周期中维护领域对象的完整性

  • 通过聚合(AGGREGATE)定义对象的关系和边界,实现模型的内聚
  • 使用工厂(FACTORY)创建复杂对象和AGGREGATE,从而封装其内部结构
  • 使用存储库(REPOSITORY)提供AGGREGATE持久化及查找的功能

模式:AGGREGATE

一组相关对象的集合,作为数据修改单元,每个聚合中有一个root和一个boundary

聚合根(root):聚合中特定的Entity,外部对象只能引用root
边界(boundary):定义了聚合内部有什么
固定规则(invariant):数据变化时必须保持的一致性规则。事务完成时,聚合内部的固定规则必须满足,外部的只需保证最终一致性

  • 聚合根Entity具有全局标识,并负责检查固定规则
  • 边界内的Entity具有本地标识,内部唯一
  • 外部对象只能引用根,通过根获取内部Entity的引用(只可以临时使用,不允许保持引用并用于修改,或更安全的只传递VO副本)
  • (推论:只有聚合根可以直接通过数据库查询,其他对象必须通过遍历关联发现)
  • 聚合内部的对象可以保持对其他聚合根的引用

Case:采购订单的并发修改

模式:FACTORY

创建对象或聚合时,如果创建过程很复杂或需要暴露过多内部结构,则可以使用FACTORY进行封装

  • 创建职责交给被创建的对象?创建工作和对象是无关的,混在一起提高了复杂度,试想汽车发动机包含组装自己的功能
  • 创建职责交给客户?客户同被创建对象实现产生了紧耦合,若客户是外部的,那么部分领域知识被泄露到了外部。

FACTORY是领域设计的一部分,封装了创建复杂对象或聚合的知识,提供了创建对象的抽象视图,使客户同对象具体实现解耦。FACTORY实现方式包括工厂方法,抽象工厂,BUILDER等。注意:

  • 创建方法应该是原子的,且要保证被创建对象或聚合的所有固定规则(当FACTORY在聚合外部时,检查固定规则的逻辑最好委托给聚合,某些情况如规则只在创建时检查(创建后不可变),则检查逻辑放在FACTORY中更合适)
  • FACTORY应返回抽象类型而非具体实现
  • FACTORY方法设计时注意参数选择,尽量使用较低层的,因为参数会被调用方依赖产生耦合。
  • FACTORY与被构建对象之间是紧耦合的,因此FACTORY应该只被关联到与被构建对象有密切关联的对象上。

只需使用构造函数的情况

  • 对象不具有多态性
  • 构造简单
  • 客户关心实现类(例如在STRATEGY模式中)

重建vs创建
创建时不满足固定规则可以直接抛错,重建时则要灵活处理(例如历史数据不完整)

模式:REPOSITORY

封装聚合的CRUD操作为抽象接口。只为需要直接访问的聚合根提供REPOSITORY,让客户始终聚焦于模型,对象的存储和访问操作都委托给REPOSITORY完成。

  • 体现了对象访问的设计决策
  • 与持久化技术解耦(便于替换,例如在测试中)

直接通过数据库查询所有对象的问题:导致领域逻辑进入查询和客户代码中,而ENTITY和VO变为单纯的数据容器,领域层不再重要,成为贫血领域模型

REPOSITORY查询:特定参数或封装SO

关系数据库设计

简单映射 - 映射保持透明并易于理解,例如一个对象到一个表的映射
数据模型与对象模型不必保持完全一致,但差别不应太大,可以牺牲规范化来帮助简单映射
利用UBIQUITOUS LANGUAGE统一对象名称和关系表中的对应项

Chapter 7 示例 - 货物运输系统