第一部分 运用领域模型
- 在领域驱动的设计中,3个基本用途决定了模型的选择:
- 模型和设计的核心互相影响。正是模型与实现之间的紧密联系才使模型变得有用,并确保我们在模型中所进行的分析能够转化为最终产品(即一个可运行的程序)
- 模型是团队所有成员使用的通用语言的中枢。由于模型与实现之间的关联,开发人员可以使用该语言来讨论程序
- 模型是浓缩的知识。模型是团队一致认同的领域知识的组织方式和重要元素的区分方式。透过我们如何选择术语、分解概念以及将概念联系起来,模型记录了我们看待领域的方式。
- 软件的核心是其为用户解决领域相关的问题的能力
第1章 消化知识
- 模型用来描述人们所关注的现实和想法某方面。
- 用户软件的问题区域就是软件的领域
- 模型的作用:模型和设计的核心互相影响;模型是团队成员使用的语言中枢;模型是浓缩的知识
1.1 有效建模的要素
- 以下几方面因素促使上述案例得以成功
- 模型和实现的绑定
- 建立一种基于模型的语言
- 开发了一种蕴含丰富知识的模型
- 提炼模型
- 头脑风暴和实验
1.2知识消化
- 知识消化并非一项孤立的活动,它一般是在开发人员的领导下,由开发人员与领域专家组成的团队来共同协作。他们共同收集信息,并通过消化而将它组织为有用的形式
- 信息的原始资料来自于领域专家头脑中的知识、现有系统的用户、以及技术团队在现有遗留系统或者同领域其他项目中积累的经验
1.3 持续学习
- 高效率的团队需要有意识的积累知识,并持续学习.对于开发人员来说,这意味着既要完善技术知识,也要培养一般的领域建模技巧(如本书中所讲的那些技巧)。但这也包括认真学习他们正在从事的特定领域的知识
1.4 知识丰富的设计
- 通过像PCB示例这样的模型获得到的知识远远不只是“发现名词”,业务活动和规则如何涉及到的实体一样,都是领域的核心,任何领域都有各种类别的概念
- 当我们的建模不再局限于寻找实体和值对象时,我们才能充分吸收知识,因为业务规则之间可能存在不一致。正是通过和软件专家紧密协作来消化知识的过程才使得规则得以澄清和充实,并消除规则之间的矛盾以及删除无用的规则
1.5 深层次模型
- 知识消化是一种探索,永无止境
第2章 交流与语言的使用
- 领域模型可成为软件项目通用语言的核心。该模型是一组得自于项目人员头脑中的概念,以及反映了领域深层含义的术语和关系。这些术语和相互关系提供了模型语言的语义,虽然语言是为领域量身定制的,但就技术开发而言,其依然足够精确。正是这条至关重要的纽带,将模型与开发活动结合在一起,并使模型与代码紧密绑定。
2.1 模式:UBIQUITOUS LANGUAGE
- 要想创建一种灵活、蕴含丰富知识的设计,需要一种通用的、共享的团队语言,以及对语言的不断试验。
- 如果语言支离破碎的,项目必将遭遇严重问题。领域专家使用他们自己的术语,而技术团队所使用的的语言经过调整,以便于涉及角度讨论领域。
- 通用语言词汇主要包括类和操作名称
- 将模型作为语言的支柱。确保团队在内部的所有交流中以及代码中坚持使用这种语言。在画图、写东西,特别是讲话时也要使用这种语言。
2.2 “大声的”建模
- 改善模型的最佳方式之一就是通过对话进行研究,试着大声的说出可能的模型变化过程中各种结构
- 讨论系统时要结合模型,使用模型元素及其交互来大声的说出来。并且按照模型允许的方式将各种概念结合在一起找到更简单的方式说出你要讲的话。并且将新的想法应用到图和代码中。
2.3 一个团队,一种语言
-
如果连经验丰富的领域模型专家都不能理解模型,那么模型一定出了问题
2.4文档和图
- 简单、非正式的UML图能够维系整个讨论
- 通常的用法是以图为主,辅以文本注释;而我更愿意以文本为主,用精心挑选的简化图作为说明
- 使用uml、对象交互图描述模型
- 设计的细节应该体现在代码中
- 模型不是图
2.4.1 书面设计文档
- 文档应该作为口头交流和代码的补充
- 文档应该鲜活并且保持最新
- 设计文档的最大价值在于解释模型的概念,帮助在代码的细节中指引方向,或许还可以帮助人们深入了解模型预期的使用风格
- 档必须深入到各种项目活动中去。判断是否做到这一点的最简单方法,是观察文档与UBIQUITOUS LANGUAGE之间的交互
2.4.2 完全依赖可执行代码的情况
2.5 解释性模型
- 解释性模型提供了一定的自由度,可以专门为某一个特殊主体定制一些表达能力更加丰富的风格
-
解释性模型不必是对象模型,而且最好不是,避免人们错误的认为这些模型和软件设计是一致的
第3章 绑定模型和实现
3.1模式:model-driven design
- 如果整个程序设计或者其核心部分没有与领域模型相对应,那么这个模型就是没有价值的,软件的正确性也值得怀疑。同时,模块和设计功能之间过于复杂的对应关系也是难于理解的,在实际项目中,当设计改变时也无法维护这种关系。若分析与设计之间存在严重分歧,那么在分析和设计活动中所获得的知识无法彼此分享。
- 软件系统各个部分的设计应该忠实的反应领域模型,以便体现这两者之间的明确对应关系。我们应该反复检查并修改模型,以便软件可以更加自然的实现模型,及时想让模型反应出更深层次的领域概念时也该如此。我们需要的模型不但应该满足这两种需求,还应该能够支持健壮的ubiquitous language (通用语言)
3.2 建模方式和工具支持
- 面向对象设计目前大多数项目所使用的建模模式,也是本书中使用的主要方法
3.3 揭示主旨:为什么模型对用户至关重要
- 分析、设计模型 vs 用户模型
3.4 模式:hands-on modeler
- 如果开发人员发现不需要对模型负责,或许不知道模型如何为应用服务,那么这个模型和应用没有任何关联。如果开发人员没有意识到修改代码就是修改模型,那么他们对程序的重构不但不会增强模型的作用,反而会削弱它的效果。
- model driven design两个要素:模型要支持有效的实现和抽象出关键的领域知识
- 任何参与建模的技术人员,不管在项目中的主要职责是什么,都必须花时间了解代码。任何负责修改代码的人员必须学会用代码来表达模型。每一个开发人员都必须不同程度地参与模型讨论并且与领域专家保持联系。参与不同工作的人都必须有意识地通过UBIQUITOUS LANGUAGE与接触代码的人及时交换关于模型的想法。
第二部分 模型驱动设计的构造块
第4章 分离领域
- 注意模型和对象的生成顺序
-
职责驱动设计、契约式设计
4.1 模式:layer architecture
-
分层:用户界面层、应用层、领域层、基础设施层
给复杂的应用程序划分层次。在每一层内分别进行设计,使其具有内聚性并且只依赖于它的下层。采用标准的架构模式,只与上层进行松散的耦合。将所有与领域模型相关的代码放在一个层中,并把它与用户界面层、应用层以及基础设施层的代码分开。领域对象应该将重点放在如何表达领域模型上,而不需要考虑自己的显示和存储问题,也无需管理应用任务等内容。这使得模型的含义足够丰富,结构足够清晰,可以捕捉到基本的业务知识,并有效地使用这些知识
关注点分离,但是也要保证各个层次的交互
layered architecture 的基本原则是层中的任何元素都仅仅依赖于本层的其它元素或者其下层的元素。向上通信必须通过间接的方式进行
4.1.1 将各层关联起来
- 各层之间是松散链接的,层与层的依赖关系只能是单向的。
- 如果下层元素需要与上层元素进行通信,则需要采用回调模式或者observers模式
- 应用层和领域层可以调用基础设施层所提供的service。然而,并不是所有的基础设施都可以供上层调用的service形式出现(如为所有领域对象提供抽象基类)
4.1.2 架构框架
- 早期的j2ee应用程序通常会将所有领域对象实现为“实体对象”,这种实现不仅影响程序性能,而且会减慢开发速度。取而代之的最佳实践是利用j2ee框架实现大粒度对象,而用普通的java对象实现大部分的业务逻辑
4.2 领域层是模型的精髓
- 领域层则是领域模型以及所有与其直接相关的设计元素的表现,它由业务逻辑的设计和实现组成
4.3 模式:the smart ui“反模式”
- smart ui 是另一种设计方法,与领域驱动设计方法迥然不同且互不兼容。是一种简单快速实现业务的一种方式。
- 如果一个经验并不丰富的项目团队要完成一个简单的项目,却决定使用MODEL-DRIVEN DESIGN以及LAYERED ARCHITECTURE,那么这个项目组将会经历一个艰难的学习过程。团队成员不得不去掌握复杂的新技术,艰难地学习对象建模
-
smart ui 与DDD对比
4.4 其他分离模式
第5章 软件中所表示的模型
- 一个对象是用来表示某种具有连续性和标识的事务呢?还是用来描述某种状态的属性呢?这是entity和value object之间的根本区别
- 领域中还有一些方面适合用动作或者操作来表示,这比用对象来表示更加清楚。这些方面最好用service来表示,而不应该把操作的责任强加到entity或者value object 上
- 每个设计决策都应该是在深入理解领域中的某些知识做出的
5.1 关联
- 对象之间的关联使得模型和实现之间的交互变得更复杂
- 模型中的每个可遍历的关联,软件中都要有同样的属性机制
- 例如一对多关联可以用一个集合类型的实例变量来实现,但设计无需如此,可能没有集合,这时可以用一个访问方法来查询数据库,找到相关记录,并用这些记录初始化对象。这两种设计反应了同一个模型。
- 限定多对多的的关联方向 可以有效的将其简化为一对多的设计,从而实现一种简单的多的设计
- 坚持将关联限定为领域所倾向的方向,不仅可以提高这些关联的表达力并简化其实现,而且还可以突出剩下的双向关联的重要性。
- 当双向关联是领域的一个语义特征时,或者应用程序的功能要求双向关联时,就需要保留它,以便于表达出这些需求。
- 最终的简化是清除那些对当前工作或者模型对象的基本含义来说不重要的关联。
5.2 模式:Entity,又名peference object
- 很多对象不是通过他们的属性定义的,而是通过连续性和标识定义的。
- 实体的基本概念是一种贯穿整个生命周期的抽象的连续性。
- 主要由标识定义的对象被称作entity。entity 具有生命周期。这期间他们的形式和内容可能发生根本性的改变,但必须保持一种内在的连续性。
- entity 的判断条件:在整个生命周期是连续性的;它的区别并不是由那些对客户特别重要的属性决定的
- 在一个模型中,并不是所有的对象都是有意义的entity。标识是entity 一个微妙有意义的属性,不能交给语言的自动特性处理。
- 当一个对象由其标识(而不是属性)区分时,那么在模型中应该主要通过标识来确定该对象的定义。使类定义变得简单,并集中关注生命周期的连续性和标识。定义一种区分每个对象的方式,这
种方式应该与其形式和历史无关。要格外注意那些需要通过属性来匹配对象的需求。在定义标识操作时,要确保这种操作为每个对象生成唯一的结果,这可以通过附加一个保证唯一性的符号来实现。这种定义标识的方法可能来自外部,也可能是由系统创建的任意标识符,但它在模型中必须是唯一的标识。模型必须定义出“符合什么条件才算是相同的事物”
5.2.1 entity 建模
- Entity 的基本职责是保证连续性,以便其行为是可预测和和更清楚。
- 抓住entity 对象的基本特征或者属性,尤其是用于识别,查找,匹配的特征。应该将特性和行为转移到与核心实体相关联的对象上
5.2.2 设计标识操作
- 每个实体都必须有一种建立标识的操作,以便与其他对象区分开
- 某些数据属性或者属性组合可以确保他们在系统中具有唯一性,或者在这些属性加一些简单约束可以使其具有唯一性
- 当对象属性没办法形成唯一键时,经常用到的解决方案是为每个实体附加一个在类中的唯一的符号
5.3 模式:value object
- 很多对象是没有概念上的标识,它们描述了一个事务的某种特征
- 用于描述领域的某个方面而没有概念上的标识称为value object。对于这些元素,我们只关心他们是什么,而不关心他们是谁。
- value object 可以是其他对象的集合。
- Value object 可以引用entity。
- Value object 经常作为参数在对象之间传递。
- 当我们只关心一个模型元素的属性时,应该把它归类为value object。我们应该使用这个模型元素能够标识出其属性的意义,并为它提供相关的功能。value object 应该是不可变的。不要为它分配任何标识,而且不要把它设计成entity那么复杂。
5.3.1 设计value object
- 在设计值对象有多种选择,包括复制,共享或者保证值对象不变
- value object为性能优化提供了更多的选择
- 如果属性值发生改变,我们应该使用一个不同的value object,而不是修改现有的object。但是有些情况出于性能考虑,仍然需要让value object是可变额。
- 以下几种情况最好使用共享,这样可以发挥共享的最大价值并最大限度地减少麻烦:
- 节省数据库空间或减少对象数量是一个关键要求时;
- 通信开销很低时(如在中央服务器中);
- 共享的对象被严格限定为不可变时
- 在有些情况下出于性能考虑,仍需要让VALUEOBJECT是可变的。这包括以下因素:
- 如果VALUE频繁改变;
- 如果创建或删除对象的开销很大;
- 如果替换(而不是修改)将打乱集群(像前面示例中讨论的那样);
- 如果VALUE的共享不多,或者共享不会提高集群性能,或其他某种技术原因
5.3.2 设计包含value object 的关联
- 我们应该避免value object的双向关联,如果存在,请检查是否合理
5.4 模式:Service
- 有些操作是无法放到entity 和value object 上的。这些操作从概念上讲不属于任何对象。与其把他们强制归于哪一类,不如顺其自然的在模型中引入一种新的元素,那就是Service。
- 一种比较常见的错误行为是没有努力为这类行为找到合适的对象,而是转化为面向过程编程
- 一些领域概念不适合被建模为对象。如果勉强把这些重要的领域功能归为entity和value object的职责,那么不是歪曲了基于模型的对象的定义,就是人为的增加了一些无意义的对象。
- Service 是作为接口提供一种操作,它在模型中是独立的,不像entity 封装内部状态。可以在领域层使用。
- 所谓service,强调的是与其他对象的关系。与entity、value object不同,它只是定义了能够为客户做什么。Service往往是以一个活动来命名,而不是一个entity来命名,也就是说,它是一个动词而不是名词。 参数和结果应该是领域对象
- 好的service 有三个特征:
- 与领域概念相关的操作不是entity 或者value 的一个自然组成部分
- 接口是根据领域模型的其他元素定义的
- 操作是无状态的
- 当领域的某个重要过程或者操作不是entity 或者value object的自然职责时,应该在模型中添加这个独立接口操作,并将其声明为service.定义接口时要使用模型语言,并确保操作名称是ubiquitous language的术语。此外,应该使service称为无状态
5.4.1 service 与孤立的领域层
-
在大多数开发系统中,在一个领域对象和外部资源之间直接建立一个接口是很别扭的。我们可以利用一个facade将这样的service包装起来,这样外观可能以模型作为输入,并返回一个“Fund Transfer”对象。
5.4.2 粒度
- service可以控制领域层中的接口的粒度,并且避免客户端与entity和value object耦合。
- 由于应用层负责对领域对象的行为进行协调,因此细粒度的领域对象可能会把领域层的知识蔓延到应用层或者用户界面代码当中。
- 明智地引入领域层服务有助于在应用层和领域层之间保持一条明确的界限
5.4.3 对Service 的访问
5.5 模式:module,也称为package
-
module为人们提供两种观察模型的方式,一是可以在module中查询细节,而不会被整个模型淹没,二是观察module之间的关系,而考虑其内部细节。
- module的名称应该是ubiquitous language中的术语。module及其名称应该反映出领域的深层知识。
- 如果一个类依赖另一个包中的类,但是本地module对于该module并没有概念上的依赖关系,那么或许应该移动一个类,或者考虑重新组织module
5.5.1 敏捷的module
- MODULE需要与模型的其他部分一同演变。这意味着MODULE的重构必须与模型和代码一起进行。但这种重构通常不会发生。更改MODULE可能需要大范围地更新代码。这些更改可能会对团队沟通起到破坏作用,甚至会妨碍开发工具(如源代码控制系统)的使用。因此,MODULE结构和名称往往反映了模型的较早形式,而类则不是这样
5.5.2 通过基础设施打包时存在的隐患
- 除非真正的有必要将代码分布到不同的机器上,否则就把实现单一概念对象的所有代码放在同一模块中
- 利用打包把领域层从其他代码中分离出来,否则,就尽可能让领域开发人员自由决定对象的打包方式,以便于支持他们的模型和设计选择
5.6 建模范式
5.6.1 对象方式流行的原因
5.6.2对象世界中的非对象
5.6.3 在混合范式中坚持使用module-driven design
第6章 领域对象的生命周期
- 聚合,通过定义清晰的所属关系和边界,并避免混乱、错综复杂的对象关系网来实现模型的内聚。聚合对于维护生命周期各个阶段的完整性具有至关重要的作用。
- 使用factory 创建和重建复杂对象和聚合,从而封装他们的内部结构
- 在生命周期的中间和末尾使用repository,来提供查找和检索持久化对象并庞大基础设施的手段
6.1 模式:aggregate
AGGREGATE就是一组相关对象的集合,我们把它作为数据修改的单元。每个AGGREGATE
都有一个根(root)和一个边界(boundary)。边界定义了AGGREGATE的内部都有什么。根则是AGGREGATE所包含的一个特定ENTITY。对AGGREGATE而言,外部对象只可以引用根,而边界内部的对象之间则可以互相引用。除根以外的其他ENTITY都有本地标识,但这些标识只在AGGREGATE内部才需要加以区别,因为外部对象除了根ENTITY之外看不到其他对象。每个aggregate都有一个根和边界。边界定义了aggregate的内部都有什么,根则是aggregate所包含的一个特定实体。对aggregate而言,外部只能引入根,而边界内部的对象之间可以互相引用。
-
固定规则是指在数据变化式必须保持一致性规则,其涉及aggregate之间的内部关系。而任何跨越aggregate的规则将不要求每时每刻都保持最新的状态。通过事件处理、批处理或者其他更新机制,这些依赖会在一定时间内得以解决。为了实现概念上的aggregate,需要对所有的事务应用一组规则:
- 根entity 具有全局意识,它最终负责检查固定规则
- 边界内entity 具有本地标识,这些标识在本地内部是唯一的
- aggregate外部的对象不能引用除entity 之外的任何内部对象。
- 只有aggregte的根才能直接通过数据库查询获取,所有其他的对象必须通过遍历关联获取。
- aggregate内部的对象可以保持对其他aggregate的引用
- 删除操作必须一次删除aggregate边界之内的所有对象
- 当提交对aggregate边界内部的任何修改时,整个aggregate所有的固定规则必须满足
我们应该将entity和value object分门别类的聚集到aggregate中,并定义每个aggregate的边界,在每个aggregate中,选择一个entity作为根,并通过根来控制对边界内其他对象的所有访问。只允许外部对象保持对根的引用。对内部成员的临时引用可以被传递出去,但仅在一次操作中有效。由于根控制访问,因此不能绕过它来修改内部对象。这种设计有利于确保aggregate中的对象满足所有固定规则,也可以确保在任何状态变化时aggreaget作为一个整体满足固定规则。
6.2 模式:factory
- 当创建一个对象或者创建整个aggregate时,如果创建工作过于复杂,或者暴露了过多的内部结构,则可以使用factory封装。
- 对象的创建本身可以是一个主要操作。但是被创建的对象并不适合承担复杂的装配操作。将这些职责混在一起可能产生难以理解的拙劣设计。让客户直接创建对象有会让客户的设计陷入混乱,并且破坏被装配对象或aggregate的封装,而且导致客户与被创建对象的实现之间产生过于紧密的耦合。
- 创建复杂对象的实例和aggregate的职责转交给单独的对象,这个对象并没有承担领域模型中的规则,提供一个封装所有复杂装配操作的接口,而且这个接口不需要客户引用要被实例化的对象的具体类。在创建aggregate时要把它作为一个整体,并确保它满足固定规则。
- 任何好的工厂需要满足以下两个基本需求
- 每个创建方法都是原子的,而且要保证被创建对象和aggregate 的所有固定规则。
- Factory 应该被抽象为所需的类型,而不是所创建的具体类型
6.2.1 选择factory 及其应用位置
6.2.2 有些情况下只需使用构造函数
- 以下情况最好使用构造函数
- 没有继承或者实现的类
- 客户关心的是实现
- 客户可以访问所有的属性
- 构造并不复杂
- 公共构造函数必须遵守与factory 类似的规则:原子,必须满足所有创建对象的固定规则
6.2.3 接口设计
- 设计factory 或factory method需要记住以下两点:
- 每个操作必须是原子的
- factory 将与其参数发生耦合
6.2.4 固定规则的相关逻辑放在哪里
- Factory 可以将固定规则的检查工作委派给被创建对象
- 也可以考虑将固定的规则交给factory ,但是固定规则的相关逻辑却特别不适合放到那些与其他领域对象关联的factory method中。
6.2.5 entity factory 与value object factory
6.2.6 重建已存储对象
- 用于重建对象的factory与用于创建对象的factory类似,主要有以下两点不同
- 用于重建对象的entity factory不分配新的跟踪id
- 当固定规则未被满足时,重建对象的factory采用不同的方式进行处理
- factory 封装了对象创建和重建时的生命周期转换。
6.3 模式:repository
- 客户需要一种有效的方式来获取对已存在的领域对象的引用。如果基础设施提供了这方面的便利,那么开发人员可能会增加很多可遍历的关联,这会使模型变得非常混乱。另一方面,开发人员可能使用查询从数据库中提取他们所需的数据,或是直接提取具体的对象,而不是通过AGGREGATE的根来得到这些对象。这样就导致领域逻辑进入查询和客户代码中,而ENTITY和VALUE OBJECT则变成单纯的数据容器。采用大多数处理数据库访问的技术复杂性很快就会使客户代码变得混乱,这将导致开发人员简化领域层,最终使模型变得无关紧要。
- 除了通过根来遍历查找对象这种方法外,禁止其他方法对于聚合内部的任何对象进行访问
- 为每种需要全局访问的对象创建这个对象,这个对象相当于该类型的所有对象在内存中的这一个集合的替身,通过这个众所周知的全局接口来提供访问。提供添加和删除对象的方法,用这些方法来封装在数据存储中实际插入或者删除的操作。提供根据具体条件来挑选对象的方法,并返回属性值满足查询条件的对象或者对象集合,从而将实际的存储和查询技术封装起来。只为那些确实需要直接访问的aggregate根提供repository。让客户始终聚焦于模型,而将所有对象的存储和访问操作交给repository。
6.3.1 repository 查询
- 返回某些类型的汇总计算也符合repository 的概念
- 基于specification 的查询是一种优雅和灵活的查询方式
6.3.2 开发人员不能忽略的repository的实现
6.3.3 repository 实现
- repository概念在很多情况下都使用。可能实现方法有很多了,这里只能列出如下一些需要谨记的注意事项:
- 对类型进行抽象
- 充分利用与客户解耦的优点
- 将事务的控制权交给客户
6.3.4 在框架内工作
- 持久化框架的选择
6.3.5 repository 和factory 的关系
- Factory 负责处理对象的生命周期的开始,repository 帮助管理生命周期的中间和结束
-
repository可以委托factory来创建一个对象。
-
客户使用repository存储新对象
6.4 为关系数据库设计对象
第七章 使用语言:一个扩展的示例
7.1 货物运输系统简介
7.2 隔离领域:引入应用层
- 为了防止领域的职责与系统的其他部分混杂在一起,我们应用layered architecture 把领域层划分出来。
- 三个功能分配给三个应用层类,应用层类是协调者
- 跟踪查询
- 预定应用
- 事件日志应用
7.3 将entity 和 value object 区别开
- 领域对象分类
- Customer :entity
- Cargo:entity
- HandlingEvent and Carrier Movement:entity
- Location:entity
- Delivery History:entity
- Delivery Specification:value object
- Role 和其他属性
7.4 设计运输领域中的关联
7.5 Aggregate边界
7.6 选择repository
-
在我们的设计中,有5个实体是aggreagte的根,因此在选择存储时只需要考虑这5个实体,因为其他实体都不能有repotitory。
7.7 场景走查
- 为了复核这些决策,我们需要经常走查场景,以确保能够有效的解决应用问题
7.7.1 应用程序特性举例:更新cargo的目的地
7.7.2 应用程序特性举例:重复业务
7.8 对象的创建
7.8.1 Cargo的factory和构造函数
7.8.2 添加handing event
7.9 停一下,重构:Cargo Aggregate的另一种设计
7.10 运输模型中的module
7.11 引入新特性:配额检查
7.11.1 连接两个系统
销售管理系统并不是这里所使用的模型编写的,如果book application 与它直接交互,那应用程序必须适应另一个系统的设计,这将很难保持一个清晰的module driven design,而且会混淆ubiquitous language。相反我们创建一个类,让它充当我们和销售管理系统之间的翻译。但它不是一种通用机制,只是对我们应用所需要的特性进行翻译,并根据我们的领域模型重新对这些特性进行抽象。这个类将作为一个anticorruption layer
应该使用更有价值的语言来重新描述问题
7.11.2 进一步完善模型:划分业务
重新抽象系统系统领域:我们需要增加货物类别的知识,以便使模型更加丰富。而且需要与领域专家一起进行头脑风暴活动,以便于抽象出新的概念。
-
Enterprise Segement:企业部门单元
Allcation Checker 将充当Enterprise Segment 与外部系统类别名称之间的翻译。Cargo Repository还必须提供一种基于Enterprise Segement的查询。
-
发现问题
7.11.3 性能优化
7.12 小结
第三部分 通过重构加深理解
- 当然我们面临真正的挑战是找到深层次的模型,这个模型不仅能捕捉到领域专家的微妙关注点,还可以驱动切实可行的驱动设计。
- 要想成功的开发出实用的模型,需要注意以下三点:
- 复杂巧妙的领域模型是可以实现的,也值得我们去花费力气实现的
- 这样的模型离开不断的重构是很难开发出来的,重构需要领域专家和热爱学习领域知识的开发人员密切参与进来的
- 要实现并有效的运用模型,需要精通设计技巧
深层领域模型能够穿越领域表象,清晰的表达出专家们的主要关注点以及最相关的知识。
恰当反应领域的模型通常都具有功能多样性、简单易用和解释力强的特性
第8章 突破
8.1 一个关于突破的故事
8.1.1 华而不实的模型
8.1.2 突破
- loan 股份和facility 的股份可以在互不影响的情况下独立发生变化
- 需求发生变化,具体参考P134 内容
8.1.3 更深层模型
-
股份抽象模型
-
使用share pie的loan 模型
8.1.4 冷静决策
- 最后银团贷款项目进行了重构
8.1.5 成果
8.2 机遇
- 当突破带来深层次模型时,通常会让人感到不安。与大部分重构相比,这种变化的回报更多,风险也更高。而且突破出现的时机可能不合时宜。
8.3 关注根本
- 不要试图制造突破,那只会使项目陷入困境。通常,只有实现许多适度的重构才有可能出现突破。在大部分的时间里,我们都在进行微小的改进。而在这种持续改进中深层次模型含义也逐渐显现。
- 要为突破做准备,应专注于知识消化过程,同时也要逐渐建立健壮的ubiquitous language。寻找那些重要的领域概念,并在模型中清晰地表达出来。精化模型,使其更具有柔性。提炼模型。利用这些更容易掌握的手段使模型变得更清晰,这通常会带来突破。
8.4 后记:越来越多的新理解
第9章 将隐式概念转换为显示概念
- 若开发人员识别出设计中隐含的某个概念或者讨论中受到启发而发现一个概念时,就会对领域模型和相应的代码进行许多转换,在模型中添加一个或多个对象和关系时,从而将此概念显示的表达出来
9.1 概念挖掘
9.1.1 倾听语言
- 倾听领域专家使用的语言。有没有一些术语能够简洁的表达出复杂的概念?他们有没有纠正过你的用词?当你使用某些特定词语的时候,他们脸上是否已经不再流露出迷惑的表情?这些都暗示了某个概念可以改进模型。
-
运输模型
9.1.2 检查不足之处
-
利息模型
-
重构后的深层模型
-
更深层次模型
-
重构后的深层模型
9.1.3 思考矛盾之处
9.1.4 查阅书籍
9.1.5 尝试、再尝试
9.2 如何为那些不太明显的概念建模
9.2.1 显示的约束
- 约束是模型概念中非常重要的类别。它们通常是隐含的,将它们显示的表现出来可以极大的提高设计质量
9.2.2 将过程建模为领域对象
- 我们讨论的是存在领域中的过程,我们必须在模型中把这些过程表示出来。否则当这些过程显露出来时,往往会使对象设计变得笨拙
9.2.3 模式:specification
9.2.4 specification的应用和实现
- specification最有价值的地方在于他可以将不同的应用功能统一起来。出于以下三个目的中的一个或者多个,我们需要指定对象的状态
- 验证对象,检查它是否满足某些需求或者已经为实现某个目标做好了准备。
- 从集合中选择一个对象
- 指定在创建对象时必须满足某种需求
第10章 柔性设计
- 为了使项目能够随着开发工作的进行加速前进,而 不会由于它自己的老化停滞不前,设计必须让人们乐于使用,而且易于做出修改。这就是柔性设计(supple design)。
- 柔性设计是对深层模型的补充
10.1 模式:intention-revealing interfaces
-
前一章节深入探讨了对于规则和计算进行显示的建模,实现这样的对象要求我们深入理解计算或者规则的大部分细节。对象的强大功能就是把细节隐藏起来,如此一来,客户代码就能很简单,而且可以用高层概念来解释。
2.一些有助于获得柔性设计的模式
Kent Beck 曾经提出通过intention - revealing selector (释意命名选择器)来选择方法的名称,使名称表达其目的。类型名称、方法名称和参数名称组合在一起,共同形成了一个intention-revealing interface。
在命名类和操作时,要描述他们的效果和目的,而不是表达他们是通过何种方式达到目的的。这样可以使用客户开发人员不必去理解内部细节。这些名称应该与ubiquitous language 保持一致,便于团队人员可以迅速推断出他们的意义。在创建一个行为前为它编写一个测试,这样可以站在客户开发人员的角度思考他们。
10.2 模式:side-effect-free function
- 任何对未来操作产生影响的系统状态改变都可以称为副作用
- 多个规则的相互作用或者计算的组合产生的结果是很预测的。开发人员在调用一个操作时,为了预测操作的结果,必须理解它的实现以及它所调用其它方法的实现。如果不得不“揭开接口的面纱”,那么接口的抽象作用就受到了限制。如果没有了可以安全的预见结果的抽象,开发人员就必须限制“组合爆炸”,这就限制了系统行为的丰富性。
- 返回结果而不产生副作用的操作称之为“函数”。
- 把命令和查询严格的放在不同的模型中。确保导致状态改变的方法不返回领域数据,并尽可能的保持简单。在不引起任何副作用的方法中执行所有查询和计算。
5.总有一些替代的模型和设计,他们不要求对现有版本进行任何修改。相反,他们创建并返回一个value object用于表示计算结果 - 尽可能的把程序的逻辑放在函数中,因而函数是只产生结果而不会产生副作用的操作。严格的把命令隔离到不返回领域信息的,非常简单的操作中。当发现一个非常适合承担复杂逻辑的概念时,就把复杂的逻辑转移到value object中,这样就可以进一步的控制副作用。
10.3 模式:assertion
- 契约式设计,向前推进了一小步。通过给出类和方法 的断言使开发人员知道了肯定发生的结果。简而言之,后置条件描述了一个操作的副作用,也就是调用一个方法之后必然发生的结果。前置条件就像是合同条款,即为了满足后置条件而必须要满足的前置条件。类的固定规则规定了在操作结束时对象的状态。也可以将aggregate作为一个整体来为它声明规定规则,这些都是严格定义的完整性规则。
- 把操作的后置条件和类以及aggregate的固定规则表达清楚。如果在你的编程语言中不能直接编写断言,那么就把它写成自动的单元测试。还可以把它写在文档和图中。
10.4 模式:conceptual contour
- conceptual contour:概念轮廓
- 把设计元素(操作,接口,类以及aggregate)分解为内聚的单元,在过程中,你对于领域中一切重要的划分的直观认知也需要考虑在内。在连续的重构中观察发生的变化和保证稳定的规律性,并寻找能够解释这些变化模式的底层conceptual contour。使模型与领域中那些一致的方面相匹配。
10.5 模式:standalone class
- module和aggregate 目的是为了限制互相依赖的关系网。
- 低耦合是对象设计的一个基本要素。尽一切可能保持低耦合,把其它所有无关概念提取到对象之外。这样类就变得完全独立了,这就可以使我们单独研究和理解他们。每个这样独立类都极大的减轻因理解module而带来的负担。
10.6 模式:closure of operation (闭合操作)
- 在适当的情形下,在定义操作时,让它的参数类型与返回类型保持一致。如果实现者的状态在计算中会用到,那么实现者实际上就是操作的一个参数。因此参数和返回值应该与实现者有相同的类型。这样的操作就是在该类型的实例集合中的闭合操作。闭合操作提供了一个高层接口,同时又不会引入其他概念的任何依赖。
10.7 声明式设计
10.8 声明式设计风格
10.9 切入问题的角度
10.9.1 分割子领域
10.9.2 尽可能利用已有的形式
第11章 应用分析模式
- 分析模式是一种概念集合,用来表示业务建模中的常见结构。它可能只与一个领域有关,也可能跨域多个领域。
第12章:将设计模式应用于模型
12.1 模式:strategy
12.2 模式:composite
12.3 为什么没有介绍flyweight
第13章 通过重构得到更深次的理解
- 有三件事情是必须要关注的:
- 以领域为本
- 用一种不同的方式看待事物
- 始终坚持与领域专家对话
13.1 开始重构
13.2 探索团队
13.3 借鉴先前经验
13.4 针对开发人员的设计
13.5 重构的时机
13.6 危机就是机遇
第四部分 战略设计
- 三大主题:上下文、精炼和大型结构
第14章 保持模型的完整性
- 大型系统领域模型的完全统一即不可行,也不划算
- 既然无法维护一个涵盖整个企业的统一模型,那就不要再受到这种思路的限制。通过预先决定什么应该统一,并实际认识到什么不能统一,我们就能创建一个清晰的,共同的视图
- 我们需要使用一种方式来标记出来不同模型之间的边界和关系。
- bounded context(限界上下文)定义了每个模型的应用范围,而context map(上下文图)则给出来项目上下文以及他们之间的关系的总体视图。
- 在这个稳定的基础上,我们就可以开始实施那些在界定和关联context方面更有效的策略了-从通过共享内核来紧密关联上下文,到那些各行其道
14.1 模型:bounded context
- 任何大型项目都会存在多个模型。而当基于不同模型的代码被组合在一起后,软件就会出现bug、变得不可靠和难以理解。团队成员之间的沟通变的混乱。人们往往弄不清楚一个模型不应该在哪个上下文中使用
- 一个模型只在一个上下文使用。
- 明确定义模型所应用的上下文。根据团队的组织,软件系统的各个部分的用法以及物理表现(代码以及数据库模型等)来设置模型的边界。在这些边界中严格保持模型的一致性,而不要受到边界之外问题的干扰和混乱。
- bounded context不是module
- 将不同模型的元素组合在一起可能会引发两类问题:重复的概念和假同源。重复的概念指的是两个模型元素实际上表示同一个概念。假同源指的是使用相同属于的两个人认为是在讨论同一件事,但是实际上不是这样的。
14.2 模式:continuous integration
- 当很多人在同一个bounded context中工作时,模型很容易发生分裂。如果将系统分解为更小的context,最终又难以保持集成度和一致性。
- continuous integration是指把一个上下文中的所有工作足够频繁的合并到一起,并使他们保持一致,以便当模型发生分裂时,可以迅速发现并纠正问题。
- 团队成员之间通过经常沟通来保证概念的集成。团队成员必须对不断变化的模型形成一个共同的理解。
- 建立一个把所有代码和其他实现工件频繁的合并到一起的工程,并通过自动化测试来快速查明模型的分类问题。严格坚持使用ubiquitous lanuage,以便在不同人的头脑中演变出不同的概念时,使所有人对模型达成一个共识。
14.3 模式:context map
- 其他团队中的人员并不是十分清楚context的边界,他们会不知不觉中做出一些更改,从而是边界变得模糊或者互联变得复杂。当不同的上下文必须互相连接时,他们可能会互相重叠。
- 通过定义不同的上下文之间的关系,并在项目中创建一个所有上下文的全局视图,可以减少混乱
- 识别在项目中起作用的每个模型,并定义其bounded context。这包括非面向对象子系统的隐含模型。为每个bounded contetx命名。并把名称添加到ubiquitous language中。描述模型之间的联系点,明确所有通信需要的转换,并突出任何共享的内容。先将当前的情况描绘出来,以后再做改变。
14.3.1 测试context的边界
- 对各个bounded context的联系点的测试特别重要
14.3.2 context map的组织和文档化
- 两个重点
- bounded context应该有名称,以便于可以讨论他们。这些名称应该被添加到团队的ubiquitous language中
- 每个人都应该知道边界在哪里,而且应该能够分辨出任何代码段的context,或者任何情况的context
- 一旦定义了counded context,那么把不同的上下文的代码隔离到不同的module中就再自然不过滤。
- 我们可以用命名规范来表明这一点,后者使用其他简单且不会产生混淆的机制
14.4 bounded context之间的关系
14.5 模式:shared kernel
- 当不同的团队开发一些紧密相关的应用程序时,如果团队之间不进行协调,即使短时间内能够取得快速进展,但他们开发的产品可能无法结合到一起。最后可能不得不耗费大量精力在转换层上,并且频繁的进行改动,不如一开始就使用continuous integration那么省心省力,同时这也造成重复工农工作,并且无法使用公共的ubiquitous language所带来的好处
- 从领域模型中选出两个团队都统一共享的一个子集。当然,除了这个模型子集以外,还包括与该模型部分相关的代码子集,或数据库设计的子集。这部分明确共享的内容具有特殊的地位,一个团队在没有与另外一个团队上商量的情况下不应该擅自修改它。
- 共享内核中必须集成自动测试套件,因为修改共享内核时,必须通过两个团队的所有测试
- shared kernel 通常是core domain,或者是一组generic subdomain(通用子领域),也可能是二者兼有。
14.6 模式:customer/supplier development team
- 在两个团队之间建立一种米孔雀的客户/供应商关系。在计划会议中,下游团队相当于上游团队的客户。根据下游团队的需求来协商需要执行的任务并为这些任务做预算,以便于每个人都知道双方的约定和进度
- 两个团队共同开发自动化验收测试,用于验证逾期的接口,把这些测试添加到上游团队的测试套件中,以便作为其持续集成的一部分来运行。这些测试使上游团队在做出修改时,不比担心对于上游团队产生副作用。
14.7 模式:conformist(跟随者)
- 通过严格遵从上游团队的模型,可以消除在bounded context之间进行转换的复杂性。尽管这会限制下游设计人员的风格,而且可能不会得到理想的应用程序模型,单选择conformity模式可以极大的简化集成。此外,这样还可以与供应商团队共享ubiquitous language。供应商处于统治地位,因此最好使沟通变容易。他们从利他主义的角度出发,会与你分享信息
14.8 模式:anticorruption layer
1.我们需要在不同的模型的关联部分之间建立转换机制,这样模型就不会被未经消化的外来模型元素所破坏
- 创建一个隔离层,以便于根据客户自己的领域模型为客户提供相关功能。这个层通过另一个系统现有接口与其进行对话,而只需对那个系统做出很少的修改,设置无需修改。在内部,这个曾在两个模型之间进行必要的双向转换
14.8.1 设置anticorruption layer接口
- anticorruption layer的公共接口通常以一组service的形式出现,但偶尔也会采用entity的形式
14.8.2 实现anticorruption layer
- 对anticorruption layer设计进行组织的一种方法是把它实现为facade、adaptor和转换器的组合,外加两个系统之间进行对话所需要的通信和传输机制
14.8.3 一个关于防御的故事
14.9 模式:separate way
- 集成总是代价高昂,而有时获益却很小
14.10 模式:open host service
当一个子系统必须与其他系统进行集成时,为每个集成都定制一个转换层可能会减慢团队的工作速度。需要维护的东西越来越多,而进行修改的时候担心的事情也会越来越多
定义一个协议,把你的子系统作为一组service供其他系统访问。开放这个协议,以便于所有需要与你子系统的集成的人都可以使用它。当有新的集成需求时,就增强并扩展这个协议。但个别团队的特殊需求除外。满足这种特属于需求的方案是使用一次性的转换器来扩充协议,以便使共享协议简单且内聚
这种通信形式暗含了一些共享的模型词汇,他们是service接口的基础。这样,其他子系统就变成了open host (开发主机)的模型相连接。而其他团队则必须学习host团队所使用的专用术语。在一些情况下,使用众所周知的published language作为交换模型可以减少耦合并简化理解。
14.11 模式:published language
- 两个bounded context之间的模型转换需要一个公共的语言
- 与现有领域模型进行直接的转换可能不是一种好的解决方案。这些模型可能过于复杂或设计的较差。他们可能没有被很好的文档化。如果把其中一个模型作为数据交换语言,它实质上就被固定住了,而无法满足新的开发需求。
- 把一个良好文档化的、能够表达出所需领域信息的共享语言作为公共的通信媒介,必要时在其他信息与改语言之间进行转换
14.12 “大象”的统一
- 承认多个互相冲突的领域模型实际上正式面对现实的做法,通过明确定义每个模型都使用的上下文,可以维护每个模型的完整性,把清楚的看到要在两个模型之间的创建任何特殊的接口的定义
14.13 选择你的模型上下文策略
14.13.1 团队决策或更高层决策
- 团队必须决定在哪里定义bounded context,以及他们之间有什么样子的关系。这些决策必须由团队做出,或者只是传达给整个团队。
14.13.2 置身上下文中
14.13.3 转换边界
- 权衡以下因素来画出bounded context的边界,首选较大的bounded context
- 当用一个统一模型来处理更多任务时,用户任务之间的流动更顺畅
- 一个内聚模型比两个不同模型再加他们之间的映射更容易理解
- 两个模型之间的转换可能会很难
- 共享语言可以使团队沟通起来更清楚
- 首选较小的bounded context
- 开发人员之间的沟通开销减少了
- 由于团队和代码规模较小,continuous integration 更容易了
- 较大的上下文要求更加通用的抽象模型,而掌握所需技巧的人员会出现短缺
- 不同的模型可以满足一些特殊需求,或者能够把一些特殊用户群的专门术语和ubiquitous language 的专门术语包括进来
14.13.4 接收那些我们无法更改的事务:描述外部系统
14.13.5 与外部系统的关系
14.13.6 设计中的系统
14.13.7 用不同模型满足特殊需要
14.13.8 部署
14.13.9 权衡
14.14 转换
14.14.1 合并context:separate way-》shared kernel
14.14.2 合并context:shared kernel-》continueous integration
14.14.3 逐步淘汰遗留系统
14.14.4 openhost service -》 pulished language
第15章 精炼
- 精炼是把一堆混杂在一起的组件分开的过程,以便通过某种形式从中提取最重要的内容,而这种形式就使它更有价值。
- 领域模型的战略精炼包括一下部分
- 帮助所有团队成员掌握系统的总体设计以及各部分如何协调
- 找到一个具有适度规模的核心模型并把它添加到通用语言中,从而促进沟通
- 指导重构
- 专注于模型中最有价值的部分
- 指导外包、现成组件的使用以及任务委派
-
战略精炼的系统方法
15.1 模式:core domain
- 对模型进行提炼。找到core domain并提供一种易于区分的方法把它与那些起辅助作用的模型和代码分开。最有价值和最专业的概念要轮廓分明,尽量压缩core domain
- 让最有才能的人开发core main。,并据此要求进行相应的招聘。在core domain中牡蛎开发能够确保实现的系统蓝图的深层模型和柔性设计。仔细判断任何其他部分的投入,看它是否能支持这个提炼出来的coore。
15.1.1 选择核心·
- 对core domain的选择取于看问题的角度
15.1.2 工作的分配
- 建立一支由开发人员和一位或者多位领域专家组成联合团队,其中开发人员将必须能力很强,能够长期稳定的工作并且学习领域知识非常感兴趣
- 自主研发的软件最大价值来自于对于core domain 的完全控制
15.2 精炼的逐步提升
15.3 模式:generic subdomain
- 模型中有些部分除了增加复杂性意外并没有捕捉或者传递任何专门的知识。任何外来因素都会对core ddomain愈发的难以分辨和理解。模型中充斥着大量众所周知的一般原则,或者是专门的细节,这些细节并不是我们的主要关注点,而只是起到支持作用。ranger,无论他们是多么的通用的元素,他们对于实现系统功能和充分表达模型都是极为重要的
- 识别那些与项目意图无关的内聚子领域。把这些子领域的通用模型提取出来,并且放到单独的module中,任何专有的东西不应该放在这些模块中
- 把他们分离出来以后,在继续开发的过程中,他们的优先级地域core domain的优先级。可以考虑为这些generic subdomain使用现成的方案或“公开发布的模型”
- 开发generic domain方案:
- 现成的解决方案
- 公开发布的设计或模型
- 把实现外包出去
- 内部实现
15.3.1 通用不等于可重用
15.3.2 项目风险管理
15.4 模式:domain vision statement
- 写一份core domain的简短描述以及它将会创造的价值,也就是“价值主张”。那些不能将你的领域模型与其他领域模型区分开就不要写了。展示出领域模型是如何实现和均衡各方利益的。这份描述要尽量精简。尽早把它写出来,随着新的理解随时修改它
- domain vision statement为团队提供了统一的方向。但是高层次的说明和代码或者模型的完整细节之间通常还需要做一些衔接
15.5 模式:highlighted core
- 尽管团队的成员可能知道核心领域是由什么构成的,但是core domain中到底包含哪些元素,不同的人会有不同的理解,甚至同一个人在不同的时间也会有不同的理解。如果我们总是要不断过滤模型以便于识别出关键部分,那么就会分散本应该投入到设计上的精力,而且这还需要广泛的模型知识。。因为,core domain必须要很容易被分辨出来。
- 对代码所做的重大结构性改动是识别core domain的理想方式,但这些改动往往无法在短期内完成。事实上,如果团队的认识还不够全面,这样的重大代码修改是很难进行的
15.5.1 精炼文档
- 创建一个单独文档来描述和解释core domain。为读者提供一个总体视图,指出了各个部分是如何组合到一起,并且指导读者到相应的代码部分寻找更多细节。
- 编写简短的文档,用于描述core domain以及core 元素之间的主要交互过程
15.5.2 标明core
15.5.3 把精炼文档作为过程工具
- 如果精炼文档概括了core domain的核心元素,那么它可以作为一个指示器-用于指示模型改变的重要程度。当模型或者代码的修改影响到精炼文档时,需要与团队其他成员一起协商。
15.6 模式:cohesive mechanism
- 把概念上的cohesive mechanism(内聚机制)分离到一个单独的轻量级框架中。要特别注意公式或那些有完备文档的算法。
15.6.1 generic subdomain 与cohesive mechanism的比较
15.6.2 mechanism是core domain的一部分
15.7 通过精炼得到声明式风格
15.8 模式:segregated core(分离的核心)
- 对模型进行重构,把核心概念从支持元素中分离出来,并增强core的内聚性,同时较少它与其他代码的耦合。把所有通用预算或者支持性元素提取到其它对象中,并把这些对象放到其他包中。
- 通过重构得到segregated core的一般步骤如下所示
- 识别出一个core的子领域
- 把相关的类转移到新的module中,并根据与这些类有关的概念为模块命名
- 对代码进行重构,把那些不直接标识概念的数据和功能分离出来。
- 对新的segregated core module进行重构,使其中的关系和交互变得更简单、表达更清楚
- 对另一个core 子域重复这个工程,直接完成segregated core的工作
15.8.1 创建segregated core的代价
15.8.2 不断发展演变的团队决策
15.9 模式:abstract core
- 把模型中最基本的概念识别出来,并分离到不同的类、抽象类、接口中。设计这个抽象模型使之能够表达出重要组件之间的大部分交互。把这个完成的抽象模型放到塔自己的module中,而专用的,详细的实现类留在由子领域定义的module中
15.10 深层模型的精炼
15.11 选择重构目标
第16章 大型结构
16.1 模式:evolving order
- 当发现一种大型结构可以明显使系统变得更清晰,而又没有对模型开发施加一些不自然的约束时,就应该采用这种结构。使用不合适的结构还不如不使用它,因此最好不要为了追求设计的完整性而勉强去使用一种结构。
16.2 模式:system metaphor(系统隐喻)
- 隐喻
16.3 模式:responsibility layer
- 如果每个对象的职责都是人为分配的,将没有统一的指导原则和一致性,也没有把领域作为一个整体来处理。为了保持大模型的一致,有必要在职责分配上实施一定的机构化控制。
- relaxed layered:松散分层系统
- 决策支持层
- 作业职责
- 能力职责