序
在领域建模过程中不应将概念与实现割裂开来。
领域模型的最大价值是它提供了一种通用语言,这种语言是将领域专家和技术人员联系在一起的纽带。
领域模型并不是按照“先建模,后实现”这个次序来工作的。
真正强大的领域模型是随着时间演进的,即使是最有经验的建模人员也往往发现他们是在系统的初始版本完成之后才有了最好的想法。
前言
领域驱动设计的实质就是消化吸收大量知识,最后产生一个反映深层次领域知识并聚焦于关键概念的模型。
近年来,反对“精细开发方法学”(elaborate development methodology)的呼声渐起,人们认为无用的静态文档以及死板的预先规划和设计加重了项目的负担。相反,敏捷过程(如XP)强调的是应对变更和不确定性的能力。
极限编程承认设计决策的重要性,但强烈反对预先设计。相反,它将相当大的精力投入到促进沟通和提高项目快速变更能力的工作中。具有这种反应能力之后,开发人员就可以在项目的任何阶段只利用“最简单而管用的方案”,然后不断进行重构,一步一步做出小的设计改进,最终得到满足客户真正需要的设计。
这种极端的简约主义是解救那些过度追求设计的执迷者的良方。那些几乎没有价值的繁琐文档只会为项目带来麻烦。项目受到“分析瘫痪症”的困扰,团队成员十分担心会出现不完美的设计,这导致他们根本没法取得进展。这种状况必须得到改变。
持续重构其实是一系列小规模的重新设计
第一部分 运用领域模型
模型是一种简化。它是对现实的解释——把与解决问题密切相关的方面抽象出来,而忽略无关的细节。
模型正是解决此类信息超载问题的工具。模型这种知识形式对知识进行了选择性的简化和有意的结构化。适当的模型可以使人理解信息的意义,并专注于问题。
领域模型并非某种特殊的图,而是这种图所要传达的思想。它绝不单单是领域专家头脑中的知识,而是对这类知识严格的组织且有选择的抽象。图可以表示和传达一种模型,同样,精心书写的代码或文字也能达到同样的目的。
建模更像是制作电影——出于某种目的而概括地反映现实。
软件的核心是其为用户解决领域相关的问题的能力。
第1章 消化知识
1.1 有效建模的要素
(1) 模型和实现的绑定。
(2) 建立了一种基于模型的语言。
(3) 开发一个蕴含丰富知识的模型。
(4) 提炼模型。
(5) 头脑风暴和实验。
1.2 知识消化
在传统的瀑布方法中,业务专家与分析员进行讨论,分析员消化理解这些知识后,对其进行抽象并将结果传递给程序员,再由程序员编写软件代码。由于这种方法完全没有反馈,因此总是失败。分析员全权负责创建模型,但他们创建的模型只是基于业务专家的意见。他们既没有向程序员学习的机会,也得不到早期软件版本的经验。知识只是朝一个方向流动,而且不会累积。
领域模型的不断精化迫使开发人员学习重要的业务原理,而不是机械地进行功能开发。领域专家被迫提炼自己已知道的重要知识的过程往往也是完善其自身理解的过程,而且他们会渐渐理解软件项目所必需的概念严谨性。
1.3 持续学习
早期工作启动了知识消化的过程,这使得所有后续工作更加高效:团队成员、开发人员和领域专家等都学到了知识,他们开始使用一种公共的语言,而且形成了贯穿整个实现过程的反馈闭环。这样,一个发现之旅悄然开始了。
第2章 交流与语言的使用
2.1 模式:UBIQUITOUS LANGUAGE
由于软件的各个部分不能够浑然一体,因此这就导致无法开发出可靠的软件
尽管模型和基于模型的语言之间的次序像是循环论证,但是,能够产生更有用模型的知识消化过程依赖于团队投身于基于模型的语言。持续使用UBIQUITOUS LANGUAGE可以暴露模型中存在的缺点,这样团队就可以尝试并替换不恰当的术语或组合。当在语言中发现缺失时,新的词语将被引入到讨论中。这些语言上的更改也会在领域模型中引起相应的更改,并促使团队更新类图并重命名代码中的类和方法,当术语的意义改变时,甚至会导致行为也发生改变。
将模型作为语言的支柱。确保团队在内部的所有交流中以及代码中坚持使用这种语言。在画图、写东西,特别是讲话时也要使用这种语言。
2.2 “大声地”建模
讨论系统时要结合模型。使用模型元素及其交互来大声描述场景,并且按照模型允许的方式将各种概念结合到一起。找到更简单的表达方式来讲出你要讲的话,然后将这些新的想法应用到图和代码中。
2.4 文档和图
我们应避免使用包罗万象的对象模型图,甚至不能使用包含所有细节的UML数据存储库。相反,应使用简化的图,图中只包含对象模型的重要概念——这些部分对于理解设计至关重要。
务必要记住模型不是图。图的目的是帮助表达和解释模型。代码可以充当设计细节的存储库。书写良好的Java代码与UML具有同样的表达能力。经过仔细选择和构造的图可以帮助人们集中注意力,并起到指导作用,当然前提条件是不能强制用图来表示全部模型或设计,因为这样会削弱图的清晰表达的能力。
2.4.1 书面设计文档
只用代码做文档与使用大而全的UML图面临着差不多相同的基本问题。
我们应该把书面文档作为代码和口头讨论的补充。
2.5 解释性模型
本书的核心思想是在实现、设计和团队交流中使用同一个模型作为基础。如果各有各的模型,将会造成危害。
第3章 绑定模型和实现
3.1 模式:MODEL-DRIVEN DESIGN
那些压根儿就没有领域模型的项目,仅仅通过编写代码来实现一个又一个的功能,它们无法利用前两章所讨论的知识消化和沟通所带来的好处。如果涉及复杂的领域就会使项目举步维艰。
如果整个程序设计或者其核心部分没有与领域模型相对应,那么这个模型就是没有价值的,软件的正确性也值得怀疑。同时,模型和设计功能之间过于复杂的对应关系也是难于理解的,在实际项目中,当设计改变时也无法维护这种关系。若分析与和设计之间产生严重分歧,那么在分析和设计活动中所获得的知识就无法彼此共享。
软件系统各个部分的设计应该忠实地反映领域模型,以便体现出这二者之间的明确对应关系。我们应该反复检查并修改模型,以便软件可以更加自然地实现模型,即使想让模型反映出更深层次的领域概念时也应如此。我们需要的模型不但应该满足这两种需求,还应该能够支持健壮的UBIQUITOUS LANGUAGE(通用语言)。
3.4 模式:HANDS-ON MODELER
人们总是把软件开发比喻成制造业。这个比喻的一个推论是:经验丰富的工程师做设计工作,而技能水平较低的劳动力负责组装产品。这种做法使许多项目陷入困境,原因很简单——软件开发就是设计。虽然开发团队中的每个成员都有自己的职责,但是将分析、建模、设计和编程工作过度分离会对MODEL-DRIVEN DESIGN产生不良影响。
如果编写代码的人员认为自己没必要对模型负责,或者不知道如何让模型为应用程序服务,那么这个模型就和程序没有任何关联。如果开发人员没有意识到改变代码就意味着改变模型,那么他们对程序的重构不但不会增强模型的作用,反而还会削弱它的效果。同样,如果建模人员不参与到程序实现的过程中,那么对程序实现的约束就没有切身的感受,即使有,也会很快忘记。MODEL-DRIVEN DESIGN的两个基本要素(即模型要支持有效的实现并抽象出关键的领域知识)已经失去了一个,最终模型将变得不再实用。最后一点,如果分工阻断了设计人员与开发人员之间的协作,使他们无法转达实现MODEL-DRIVEN DESIGN的种种细节,那么经验丰富的设计人员则不能将自己的知识和技术传递给开发人员。
HANDS-ON MODELER(亲身实践的建模者)并不意味着团队成员不能有自己的专业角色。
但是如果把MODEL-DRIVEN DESIGN中密切相关的建模和实现这两个过程分离开,则会产生问题。
任何参与建模的技术人员,不管在项目中的主要职责是什么,都必须花时间了解代码。任何负责修改代码的人员则必须学会用代码来表达模型。每一个开发人员都必须不同程度地参与模型讨论并且与领域专家保持联系。参与不同工作的人都必须有意识地通过UBIQUITOUS LANGUAGE与接触代码的人及时交换关于模型的想法。
第二部分 模型驱动设计的构造块
第4章 分离领域
4.1 模式:LAYERED ARCHITECTURE
在面向对象的程序中,常常会在业务对象中直接写入用户界面、数据库访问等支持代码。而一些业务逻辑则会被嵌入到用户界面组件和数据库脚本中。这么做是为了以最简单的方式在短期内完成开发工作。
LAYERED ARCHITECTURE的基本原则是层中的任何元素都仅依赖于本层的其他元素或其下层的元素。向上的通信必须通过间接的方式进行
有些项目没有明显划分出用户界面层和应用层,而有些项目则有多个基础设施层。但是将领域层分离出来才是实现MODEL-DRIVEN DESIGN的关键。
4.3 模式:THE SMART UI“反模式”
领域驱动设计只有应用在大型项目上才能产生最大的收益,而这也确实需要高超的技巧。不是所有的项目都是大型项目;也不是所有的项目团队都能掌握那些技巧。
因此,当情况需要时:
在用户界面中实现所有的业务逻辑。将应用程序分成小的功能模块,分别将它们实现成用户界面,并在其中嵌入业务规则。用关系数据库作为共享的数据存储库。使用自动化程度最高的用户界面创建工具和可用的可视化编程工具。
项目团队常犯的错误是采用了一种复杂的设计方法,却无法保证项目从头到尾始终使用它。另一种常见的也是代价高昂的错误则是为项目构建一种复杂的基础设施以及使用工业级的工具,而这样的项目根本不需要它们。
第5章 软件中所表示的模型
一个对象是用来表示某种具有连续性和标识的事物的呢(可以跟踪它所经历的不同状态,甚至可以跨不同的实现跟踪它),还是用于描述某种状态的属性呢?这是ENTITY与VALUE OBJECT之间的根本区别。
领域中还有一些方面适合用动作或操作来表示,这比用对象表示更加清楚。这些方面最好用SERVICE来表示,而不应把操作的责任强加到ENTITY或VALUE OBJECT上,尽管这样做稍微违背了面向对象的建模传统。SERVICE是应客户端请求来完成某事。在软件的技术层中有很多SERVICE。在领域中也可以使用SERVICE,当对软件要做的某项无状态的活动进行建模时,就可以将该活动作为一项SERVICE。
5.1 关联
尽可能地对关系进行约束是非常重要的。双向关联意味着只有将这两个对象放在一起考虑才能理解它们。当应用程序不要求双向遍历时,可以指定一个遍历方向,以便减少相互依赖,并简化设计。理解了领域之后就可以自然地确定一个方向。
5.2 模式:ENTITY(又称为REFERENCE OBJECT)
很多对象不是通过它们的属性定义的,而是通过连续性和标识定义的。
一些对象主要不是由它们的属性定义的。它们实际上表示了一条“标识线”(A Thread of Identity),这条线跨越时间,而且常常经历多种不同的表示。有时,这样的对象必须与另一个具有不同属性的对象相匹配。而有时一个对象必须与具有相同属性的另一个对象区分开。错误的标识可能会破坏数据。
主要由标识定义的对象被称作ENTITY
它们具有生命周期,这期间它们的形式和内容可能发生根本改变,但必须保持一种内在的连续性。为了有效地跟踪这些对象,必须定义它们的标识。
ENTITY可以是任何事物,只要满足两个条件即可,一是它在整个生命周期中具有连续性,二是它的区别并不是由那些对用户非常重要的属性决定的。
当一个对象由其标识(而不是属性)区分时,那么在模型中应该主要通过标识来确定该对象的定义。使类定义变得简单,并集中关注生命周期的连续性和标识。定义一种区分每个对象的方式,这种方式应该与其形式和历史无关。要格外注意那些需要通过属性来匹配对象的需求。在定义标识操作时,要确保这种操作为每个对象生成唯一的结果,这可以通过附加一个保证唯一性的符号来实现。这种定义标识的方法可能来自外部,也可能是由系统创建的任意标识符,但它在模型中必须是唯一的标识。模型必须定义出“符合什么条件才算是相同的事物”。
5.2.1 ENTITY建模
当对一个对象进行建模时,我们自然而然会考虑它的属性,而且考虑它的行为也显得非常重要。但ENTITY最基本的职责是确保连续性,以便使其行为更清楚且可预测。保持实体的简练是实现这一责任的关键。不要将注意力集中在属性或行为上,应该摆脱这些细枝末节,抓住ENTITY对象定义的最基本特征,尤其是那些用于识别、查找或匹配对象的特征。只添加那些对概念至关重要的行为和这些行为所必需的属性。此外,应该将行为和属性转移到与核心实体关联的其他对象中。这些对象中,有些可能是ENTITY,有些可能是VALUE OBJECT(这是本章接下来要讨论的模式)。除了标识问题之外,实体往往通过协调其关联对象的操作来完成自己的职责。
在图5-5中,customerID是Customer ENTITY的一个(也是唯一的)标识符,但phone number (电话号码)和address(地址)都经常用来查找或匹配一个Customer(客户)。name(姓名)没有定义一个人的标识,但它通常是确定人的方式之一。在这个示例中,phone和address属性被移到Customer中,但在实际的项目上,这种选择取决于领域中的Customer一般是如何匹配或区分的。例如,如果一个Customer有很多用于不同目的的phone number,那么phone number就与标识无关,因此应该放在Sales Contact(销售联系人)中。
5.3 模式:VALUE OBJECT
用于描述领域的某个方面而本身没有概念标识的对象称为VALUE OBJECT(值对象)。
VALUE OBJECT甚至可以引用ENTITY。例如,如果我请在线地图服务为我提供一个从旧金山到洛杉矶的驾车风景游路线,它可能会得出一个‚路线‛对象,此对象通过太平洋海岸公路连接旧金山和洛杉矶。这个‚路线‛对象是一个VALUE,尽管它所引用的3个对象(两座城市和一条公路)都是ENTITY。
当我们只关心一个模型元素的属性时,应把它归类为VALUE OBJECT。我们应该使这个模型元素能够表示出其属性的意义,并为它提供相关功能。VALUE OBJECT应该是不可变的。不要为它分配任何标识,而且不要把它设计成像ENTITY那么复杂。
5.3.1 设计VALUE OBJECT
在设计VALUE OBJECT时有多种选择,包括复制、共享或保持VALUE OBJECT不变。
复制和共享哪个更划算取决于实现环境。
以下几种情况最好使用共享,这样可以发挥共享的最大价值并最大限度地减少麻烦:
节省数据库空间或减少对象数量是一个关键要求时;
通信开销很低时(如在中央服务器中);
共享的对象被严格限定为不可变时。
有些情况下出于性能考虑,仍需要让VALUE OBJECT是可变的。这包括以下因素:
如果VALUE频繁改变;
如果创建或删除对象的开销很大;
如果替换(而不是修改)将打乱集群(像前面示例中讨论的那样);
如果VALUE的共享不多,或者共享不会提高集群性能,或其他某种技术原因。
再次强调:如果一个VALUE的实现是可变的,那么就不能共享它。无论是否共享VALUE OBJECT,在可能的情况下都要将它们设计为不可变的。
这种存储相同数据的多个副本的技术称为非规范化(denormalization),当访问时间比存储空间或维护的简单性更重要时,通常使用这种技术。
在关系数据库中,我们可能想把一个具体的值放到拥有此值的ENTITY的表中,而不是将其关联到另一个单独的表。在分布式系统中,对一个位于另一台服务器上的VALUE OBJECT的引用可能导致对消息的响应十分缓慢,在这种情况下,应该将整个对象的副本传递到另一台服务器上。我们可以随意地使用副本,因为处理的是VALUE OBJECT。
5.3.2 设计包含VALUE OBJECT的关联
我们应尽量完全清除VALUE OBJECT之间的双向关联。如果在你的模型中看起来确实需要这种关联,那么首先应重新考虑一下将对象声明为VALUE OBJECT这个决定是否正确。或许它拥有一个标识,而你还没有注意到它。
5.4 模式:SERVICE
有时,对象不是一个事物。
在某些情况下,最清楚、最实用的设计会包含一些特殊的操作,这些操作从概念上讲不属于任何对象。与其把它们强制地归于哪一类,不如顺其自然地在模型中引入一种新的元素,这就是SERVICE(服务)。
一些领域概念不适合被建模为对象。如果勉强把这些重要的领域功能归为ENTITY或VALUE OBJECT的职责,那么不是歪曲了基于模型的对象的定义,就是人为地增加了一些无意义的对象。
使用SERVICE时应谨慎,它们不应该替代ENTITY和VALUE OBJECT的所有行为。
当领域中的某个重要的过程或转换操作不是ENTITY或VALUE OBJECT的自然职责时,应该在模型中添加一个作为独立接口的操作,并将其声明为SERVICE。定义接口时要使用模型语言,并确保操作名称是UBIQUITOUS LANGUAGE中的术语。此外,应该使SERVICE成为无状态的。
5.4.1 SERVICE与孤立的领域层
SERVICE并不只是在领域层中使用。我们需要注意区分属于领域层的SERVICE和那些属于其他层的SERVICE,并划分责任,以便将它们明确地区分开。
如果银行应用程序可以把我们的交易进行转换并导出到一个电子表格文件中,以便进行分析,那么这个导出操作就是应用层SERVICE。‚文件格式‛在银行领域中是没有意义的,它也不涉及业务规则。
另一方面,账户之间的转账功能属于领域层SERVICE,因为它包含重要的业务规则(如处理相应的借方账户和贷方账户),而且‚资金转账‛是一个有意义的银行术语。在这种情况下,SERVICE自己并不会做太多的事情,而只是要求两个Account对象完成大部分工作。但如果将‚转账‛操作强加在Account对象上会很别扭,因为这个操作涉及两个账户和一些全局规则。
5.5 模式:MODULE(也称为PACKAGE)
领域层中的MODULE应该成为模型中有意义的部分,MODULE从更大的角度描述了领域。
选择能够描述系统的MODULE,并使之包含一个内聚的概念集合。这通常会实现MODULE之间的低耦合,但如果效果不理想,则应寻找一种更改模型的方式来消除概念之间的耦合,或者找到一个可作为MODULE基础的概念(这个概念先前可能被忽视了),基于这个概念组织的MODULE可以以一种有意义的方式将元素集中到一起。找到一种低耦合的概念组织方式,从而可以相互独立地理解和分析这些概念。对模型进行精化,直到可以根据高层领域概念对模型进行划分,同时相应的代码也不会产生耦合。
MODULE的名称应该是UBIQUITOUS LANGUAGE中的术语。MODULE及其名称应反映出领域的深层知识。
5.5.2 通过基础设施打包时存在的隐患
对象的一个最基本的概念是将数据和操作这些数据的逻辑封装在一起。
除非真正有必要将代码分布到不同的服务器上,否则就把实现单一概念对象的所有代码放在同一个模块中(如果不能放在同一个对象中的话)。
本章介绍的4种模式为对象模型提供了构造块。
Entity,Value Object,Service,Module
5.6 建模范式
5.6.1 对象范式流行的原因
对象模型可以解决很多实际的软件问题,但也有一些领域不适合用封装了行为的各种对象来建模。例如,涉及大量数学问题的领域或者受全局逻辑推理控制的领域就不适合使用面向对象的范式。
5.6.3 在混合范式中坚持使用MODEL-DRIVEN DESIGN
将各个部分紧密结合在一起的最有效工具就是健壮的UBIQUITOUS LANGUAGE,它是构成整个异构模型的基础。坚持在两种环境中使用一致的名称,坚持用UBIQUITOUS LANGUAGE讨论这些名称,将有助于消除两种环境之间的鸿沟。
(在使用其他范式时)没有必要放弃MODEL-DRIVEN DESIGN,而且坚持使用它是值得的。
当将非对象元素混合到以面向对象为主的系统中时,需要遵循以下4条经验规则。
不要和实现范式对抗。
把通用语言作为依靠的基础。
不要一味依赖UML。
保持怀疑态度。
关系范式是范式混合的一个特例。作为一种最常用的非对象技术,关系数据库与对象模型的关系比其他技术与对象模型的关系更紧密,因为它作为一种数据持久存储机制,存储的就是对象。
第6章 领域对象的生命周期
AGGREGATE(聚合),它通过定义清晰的所属关系和边界,并避免混乱、错综复杂的对象关系网来实现模型的内聚。聚合模式对于维护生命周期各个阶段的完整性具有至关重要的作用。
接下来,我们将注意力转移到生命周期的开始阶段,使用FACTORY(工厂)来创建和重建复杂对象和AGGREGATE(聚合),从而封装它们的内部结构。最后,在生命周期的中间和末尾使用REPOSITORY(存储库)来提供查找和检索持久化对象并封装庞大基础设施的手段。
尽管REPOSITORY和FACTORY本身并不是来源于领域,但它们在领域设计中扮演着重要的角色。这些结构提供了易于掌握的模型对象处理方式,使MODEL-DRIVEN DESIGN更完备。
6.1 模式:AGGREGATE
AGGREGATE就是一组相关对象的集合,我们把它作为数据修改的单元。每个AGGREGATE都有一个根(root)和一个边界(boundary)。边界定义了AGGREGATE的内部都有什么。根则是AGGREGATE所包含的一个特定ENTITY。对AGGREGATE而言,外部对象只可以引用根,而边界内部的对象之间则可以互相引用。除根以外的其他ENTITY都有本地标识,但这些标识只在AGGREGATE内部才需要加以区别,因为外部对象除了根ENTITY之外看不到其他对象。
根ENTITY具有全局标识。边界内的ENTITY具有本地标识,这些标识只在AGGREGATE内部才是唯一的。
删除操作必须一次删除AGGREGATE边界之内的所有对象。(利用垃圾收集机制,这很容易做到。由于除根以外的其他对象都没有外部引用,因此删除了根以后,其他对象均会被回收。)
我们应该将ENTITY和VALUE OBJECT分门别类地聚集到AGGREGATE中,并定义每个AGGREGATE的边界。在每个AGGREGATE中,选择一个ENTITY作为根,并通过根来控制对边界内其他对象的所有访问。只允许外部对象保持对根的引用。对内部成员的临时引用可以被传递出去,但仅在一次操作中有效。由于根控制访问,因此不能绕过它来修改内部对象。这种设计有利于确保AGGREGATE中的对象满足所有固定规则,也可以确保在任何状态变化时AGGREGATE作为一个整体满足固定规则。
6.2 模式:FACTORY
一个对象在它的生命周期中要承担大量职责。如果再让复杂对象负责自身的创建,那么职责过载将会导致问题。
装配复杂的复合对象的工作也最好与对象要执行的工作分开。
对象的创建本身可以是一个主要操作,但被创建的对象并不适合承担复杂的装配操作。将这些职责混在一起可能产生难以理解的拙劣设计。让客户直接负责创建对象又会使客户的设计陷入混乱,并且破坏被装配对象或AGGREGATE的封装,而且导致客户与被创建对象的实现之间产生过于紧密的耦合。
复杂的对象创建是领域层的职责,然而这项任务并不属于那些用于表示模型的对象。
应该将创建复杂对象的实例和AGGREGATE的职责转移给单独的对象,这个对象本身可能没有承担领域模型中的职责,但它仍是领域设计的一部分。提供一个封装所有复杂装配操作的接口,而且这个接口不需要客户引用要被实例化的对象的具体类。在创建AGGREGATE时要把它作为一个整体,并确保它满足固定规则。
6.2.4 固定规则的相关逻辑应放置在哪里
如果逻辑在对象的有效生命周期内永远也不被用到,那么对象就没有必要携带这个逻辑。在这种情况下,FACTORY是放臵固定规则的合适地方,这样可以使FACTORY创建出的对象更简单。
6.2.6 重建已存储的对象
当创建新对象时,如果未满足固定规则,FACTORY应该简单地拒绝创建对象,但在重建对象时则需要更灵活的响应。如果对象已经在系统的某个地方存在(如在数据库中),那么不能忽略这个事实。但是,同样也不能任凭规则被破坏。必须通过某种策略来修复这种不一致的情况,这使得重建对象比创建新对象更困难。
6.3 模式:REPOSITORY
领域驱动设计的目标是通过关注领域模型(而不是技术)来创建更好的软件。
客户需要一种有效的方式来获取对已存在的领域对象的引用。如果基础设施提供了这方面的便利,那么开发人员可能会增加很多可遍历的关联,这会使模型变得非常混乱。另一方面,开发人员可能使用查询从数据库中提取他们所需的数据,或是直接提取具体的对象,而不是通过AGGREGATE的根来得到这些对象。这样就导致领域逻辑进入查询和客户代码中,而ENTITY和VALUE OBJECT则变成单纯的数据容器。采用大多数处理数据库访问的技术复杂性很快就会使客户代码变得混乱,这将导致开发人员简化领域层,最终使模型变得无关紧要。
在所有持久化对象中,有一小部分必须通过基于对象属性的搜索来全局访问。当很难通过遍历方式来访问某些AGGREGATE根的时候,就需要使用这种访问方式。它们通常是ENTITY,有时是具有复杂内部结构的VALUE OBJECT,还可能是枚举VALUE。而其他对象则不宜使用这种访问方式,因为这会混淆它们之间的重要区别。随意的数据库查询会破坏领域对象的封装和AGGREGATE。技术基础设施和数据库访问机制的暴露会增加客户的复杂度,并妨碍模型驱动的设计。
为每种需要全局访问的对象类型创建一个对象,这个对象相当于该类型的所有对象在内存中的一个集合的“替身”。通过一个众所周知的全局接口来提供访问。提供添加和删除对象的方法,用这些方法来封装在数据存储中实际插入或删除数据的操作。提供根据具体条件来挑选对象的方法,并返回属性值满足查询条件的对象或对象集合(所返回的对象是完全实例化的),从而将实际的存储和查询技术封装起来。只为那些确实需要直接访问的AGGREGATE根提供REPOSITORY。让客户始终聚焦于模型,而将所有对象的存储和访问操作交给REPOSITORY来完成。
6.3.3 REPOSITORY的实现
将事务的控制权留给客户。尽管REPOSITORY会执行数据库的插入和删除操作,但它通常不会提交事务。例如,保存数据后紧接着就提交似乎是很自然的事情,但想必只有客户才有上下文,从而能够正确地初始化和提交工作单元。如果REPOSITORY不插手事务控制,那么事务管理就会简单得多。
6.3.4 在框架内工作
一般来讲,在使用框架时要顺其自然。当框架无法切合时,要想办法在大方向上保持领域驱动设计的基本原理,而一些不符的细节则不必过分苛求。寻求领域驱动设计的概念与框架中的概念之间的相似性。这里的假设是除了使用指定框架之外没有别的选择。很多J2EE项目根本不使用实体bean。如果可以自由选择,那么应该选择与你所使用的设计风格相协调的框架或框架中的一些部分。
6.3.5 REPOSITORY与FACTORY的关系
FACTORY负责处理对象生命周期的开始,而REPOSITORY帮助管理生命周期的中间和结束。
从领域驱动设计的角度来看,FACTORY和REPOSITORY具有完全不同的职责。FACTORY负责制造新对象,而REPOSITORY负责查找已有对象。REPOSITORY应该让客户感觉到那些对象就好像驻留在内存中一样。对象可能必须被重建(的确,可能会创建一个新实例),但它是同一个概念对象,仍旧处于生命周期的中间。
REPOSITORY也可以委托FACTORY来创建一个对象,这种方法(虽然实际很少这样做,但在理论上是可行的)可用于从头开始创建对象,此时就没有必要区分这两种看问题的角度了
另一种情况促使人们将FACTORY和REPOSITORY结合起来使用,这就是想要实现一种“查找或创建”功能,即客户描述它所需的对象,如果找不到这样的对象,则为客户新创建一个。我们最好不要追求这种功能,它不会带来多少方便。
6.4 为关系数据库设计对象
大多数情况下关系数据库是面向对象领域中的持久化存储形式,因此简单的对应关系才是最好的。表中的一行应该包含一个对象,也可能还包含AGGREGATE中的一些附属项。表中的外键应该转换为对另一个ENTITY对象的引用。有时我们不得不违背这种简单的对应关系,但不应该由此就全盘放弃简单映射的原则。
第7章 使用语言:一个扩展的示例
7.8 对象的创建
7.8.2 添加Handling Event
就ENTITY而言,那些非标识作用的属性通常可以过后再添加。
7.11.2 进一步完善模型:划分业务
执行业务规则是领域层的职责,而不应在应用层中执行。
第三部分 通过重构来加深理解
对象分析的传统方法是先在需求文档中确定名词和动词,并将其作为系统的初始对象和方法。这种方式太过简单,只适用于教导初学者如何进行对象建模。事实上,初始模型通常都是基于对领域的浅显认知而构建的,既不够成熟也不够深入。
深层模型能够穿过领域表象,清楚地表达出领域专家们的主要关注点以及最相关的知识。以上定义并没有涉及抽象。事实上,深层模型通常含有抽象元素,但在切中问题核心的关键位臵也同样会出现具体元素。
恰当反映领域的模型通常都具有功能多样、简单易用和解释力强的特性。这种模型的共同之处在于:它们提供了一种业务专家青睐的简单语言,尽管这种语言可能也是抽象的。
第9章 将隐式概念转变为显式概念
若开发人员识别出设计中隐含的某个概念或是在讨论中受到启发而发现一个概念时,就会对领域模型和相应的代码进行许多转换,在模型中加入一个或多个对象或关系,从而将此概念显式地表达出来。
9.1 概念挖掘
9.1.4 查阅书籍
在寻找模型概念时,不要忽略一些显而易见的资源。在很多领域中,你都可以找到解释基本概念和传统思想的书籍。你依然需要与领域专家合作,提炼与你的问题相关的那部分知识,然后将其转化为适用于面向对象软件的概念。但是,查阅书籍也许能够使你一开始就形成一致且深层的认识。
9.2 如何为那些不太明显的概念建模
9.2.1 显式的约束
约束是模型概念中非常重要的类别。它们通常是隐含的,将它们显式地表现出来可以极大地提高设计质量。
将约束条件提取到其自己的方法中,这样就可以通过方法名来表达约束的含义,从而在设计中显式地表现出这条约束。
如果约束的存在掩盖了对象的基本职责,或者如果约束在领域中非常突出但在模型中却不明显,那么就可以将其提取到一个显式的对象中,甚至可以把它建模为一个对象和关系的集合。
9.2.2 将过程建模为领域对象
过程是应该被显式表达出来,还是应该被隐藏起来呢?区分的方法很简单:它是经常被领域专家提起呢,还是仅仅被当作计算机程序机制的一部分?
约束和过程是两大类模型概念,当我们用面向对象语言编程时,不会立即想到它们,然而它们一旦被我们视为模型元素,就真的可以让我们的设计更为清晰。
一个更特殊但也非常常用的概念——规格(specification)。“规格”提供了用于表达特定类型的规则的精确方式,它把这些规则从条件逻辑中提取出来,并在模型中把它们显式地表示出来。
9.2.3 模式:SPECIFICATION
业务规则通常不适合作为ENTITY或VALUE OBJECT的职责,而且规则的变化和组合也会掩盖领域对象的基本含义。但是将规则移出领域层的结果会更糟糕,因为这样一来,领域代码就不再表达模型了。
逻辑编程提供了一种概念,即“谓词”这种可分离、可组合的规则对象,但是要把这种概念用对象完全实现是很麻烦的。同时,这种概念过于通用,在表达设计意图方面,它的针对性不如专门的设计那么好。
我们可以借用谓词概念来创建可计算出布尔值的特殊对象。
这个新对象就是一个规格。
SPECIFICATION(规格)中声明的是限制另一个对象状态的约束,被约束对象可以存在,也可以不存在。SPECIFICATION有多种用途,其中一种体现了最基本的概念,这种用途是:SPECIFICATION可以测试任何对象以检验它们是否满足指定的标准。
为特殊目的创建谓词形式的显式的VALUE OBJECT。SPECIFICATION就是一个谓词,可用来确定对象是否满足某些标准。
9.2.4 SPECIFICATION的应用和实现
SPECIFICATION最有价值的地方在于它可以将看起来完全不同的应用功能统一起来。出于以下3个目的中的一个或多个,我们可能需要指定对象的状态。
(1) 验证对象,检查它是否能满足某些需求或者是否已经为实现某个目标做好了准备。
(2) 从集合中选择一个对象(如上述例子中的查询过期发票)。
(3) 指定在创建新对象时必须满足某种需求。
这3种用法(验证、选择和根据要求来创建)从概念层面上来讲是相同的。
MODEL-DRIVEN DESIGN要求模型与实现保持同步,但它同时也让我们可以自由选择能够准确捕捉模型意义的实现方式。幸运的是,SQL是用于编写SPECIFICATION的一种很自然的方式。
现在的设计有一些问题。最重要的问题是,表结构的细节本应该被隔离到一个映射层中(这个映射层把领域对象关联到关系表),现在却泄漏到了DOMAIN LAYER中。这样一来,这些表结构信息发生了隐性的重复,因此导致对Invoice和Customer对象的修改和维护变得很麻烦,因为现在必须在多个地方跟踪它们的映射变化。
一些对象关系映射框架提供了用模型对象和属性来表达这种查询的方式,并在基础设施层中创建实际的SQL语句。这样就可以两全其美了。
如果无法把SQL语句创建到基础设施中,还可以重写一个专用的查询方法并把它添加到Invoice Repository中,这样就把SQL语句从领域对象中分离出来了。
通过可工作的原型来摆脱开发僵局
有的团队必须要等待另一个团队编写出代码后才可以继续工作。而这两个团队都要等到代码完全整合后才可以测试组件或从用户那里获取反馈。这种僵局通常可以通过关键组件的模型驱动原型来缓解,即使原型并不满足所有需求也可以。当实现与接口分离时,只要有可以工作的实现,项目工作就可以并行地开展下去。时机成熟的时候,可以用更为高效的实现来替代原型。同时,系统中的其他部分也能在开发期间与原型进行交互。
第10章 柔性设计
如果软件没有一个条理分明的设计,那么开发人员不仅不愿意仔细地分析代码,他们更不愿意修改代码,因为修改代码会产生问题——要么加重了代码的混乱状态,要么由于某种未预料到的依赖而破坏了某些东西。在任何一种系统中(除非是一些非常小的系统),这种不稳定性使我们很难开发出丰富的功能,而且限制了重构和迭代式的精化。
为了使项目能够随着开发工作的进行加速前进,而不会由于它自己的老化停滞不前,设计必须要让人们乐于使用,而且易于做出修改。这就是柔性设计(supple design)。
很多过度设计(overengineering)借着灵活性的名义而得到合理的外衣。但是,过多的抽象层和间接设计常常成为项目的绊脚石。
当复杂性阻碍了项目的前进时,就需要仔细修改最关键、最复杂的地方,使之变成一个柔性设计,这样才能突破复杂性带给我们的限制,而不会陷入遗留代码维护的麻烦中。
10.1 模式:INTENTION-REVEALING INTERFACES
如果开发人员为了使用一个组件而必须要去研究它的实现,那么就失去了封装的价值。当某个人开发的对象或操作被别人使用时,如果使用这个组件的新的开发者不得不根据其实现来推测其用途,那么他推测出来的可能并不是那个操作或类的主要用途。如果这不是那个组件的用途,虽然代码暂时可以工作,但设计的概念基础已经被误用了,两位开发人员的意图也是背道而驰。
类型名称、方法名称和参数名称组合在一起,共同形成了一个INTENTION-REVEALING INTERFACE(释意接口)。
在命名类和操作时要描述它们的效果和目的,而不要表露它们是通过何种方式达到目的的。这样可以使客户开发人员不必去理解内部细节。这些名称应该与UBIQUITOUS LANGUAGE保持一致,以便团队成员可以迅速推断出它们的意义。在创建一个行为之前先为它编写一个测试,这样可以促使你站在客户开发人员的角度上来思考它。
10.2 模式:SIDE-EFFECT-FREE FUNCTION
我们可以宽泛地把操作分为两个大的类别:命令和查询。
在计算机科学中,任何对系统状态产生的影响都叫副作用。这里为了便于讨论,我们把它的含义缩小一下,任何对未来操作产生影响的系统状态改变都可以称为副作用。
为什么人们会采用“副作用”这个词来形容那些显然是有意影响系统状态的操作呢?我推测这大概是来自于复杂系统的经验。大多数操作都会调用其他的操作,而后者又会调用另外一些操作。一旦形成这种任意深度的嵌套,就很难预测调用一个操作将要产生的所有后果。第二层和第三层操作的影响可能并不是客户开发人员有意为之的,于是它们就变成了完全意义上的副作用。在一个复杂的设计中,元素之间的交互同样也会产生无法预料的结果。副作用这个词强调了这种交互的不可避免性。
多个规则的相互作用或计算的组合所产生的结果是很难预测的。开发人员在调用一个操作时,为了预测操作的结果,必须理解它的实现以及它所调用的其他方法的实现。如果开发人员不得不“揭开接口的面纱”,那么接口的抽象作用就受到了限制。如果没有了可以安全地预见到结果的抽象,开发人员就必须限制“组合爆炸” [3] ,这就限制了系统行为的丰富性。
返回结果而不产生副作用的操作称为函数。一个函数可以被多次调用,每次调用都返回相同的值。一个函数可以调用其他函数,而不必担心这种嵌套的深度。函数比那些有副作用的操作更易于测试。由于这些原因,使用函数可以降低风险。
尽可能把程序的逻辑放到函数中,因为函数是只返回结果而不产生明显副作用的操作。严格地把命令(引起明显的状态改变的方法)隔离到不返回领域信息的、非常简单的操作中。当发现了一个非常适合承担复杂逻辑职责的概念时,就可以把这个复杂逻辑移到VALUE OBJECT中,这样可以进一步控制副作用。
10.3 模式:ASSERTION
把复杂的计算封装到SIDE-EFFECT-FREE FUNCTION中可以简化问题,但实体仍然会留有一些有副作用的命令,使用这些ENTITY的人必须了解使用这些命令的后果。在这种情况下,使用ASSERTION(断言)可以把副作用明确地表示出来,使它们更易于处理。
由于对象接口并不会限制副作用,因此实现相同接口的两个子类可能会产生不同的副作用。使用它们的开发人员需要知道哪个副作用是由哪个子类产生的,以便预测后果。这样,抽象和多态也就失去了意义。
如果操作的副作用仅仅是由它们的实现隐式定义的,那么在一个具有大量相互调用关系的系统中,起因和结果会变得一团糟。理解程序的唯一方式就是沿着分支路径来跟踪程序的执行。封装完全失去了价值。跟踪具体的执行也使抽象失去了意义。
“契约式设计”(design by contract)向前推进了一步,通过给出类和方法的“断言”使开发人员知道肯定会发生的结果。
把操作的后置条件和类及AGGREGATE的固定规则表述清楚。如果在你的编程语言中不能直接编写ASSERTION,那么就把它们编写成自动的单元测试。还可以把它们写到文档或图中(如果符合项目开发风格的话)。
寻找在概念上内聚的模型,以便使开发人员更容易推断出预期的ASSERTION,从而加快学习过程并避免代码矛盾。
自动单元测试在一定程度上弥补了缺乏语言支持带来的不足。
测试首先设臵前臵条件,在执行之后,再检查后臵条件是否被满足。
10.4 模式:CONCEPTUAL CONTOUR
通过反复重构最终会实现柔性设计,以上就是其中的一个原因。随着代码不断适应新理解的概念或需求,CONCEPTUAL CONTOUR(概念轮廓)也就逐渐形成了。
在做每个决定时,都要问自己:“这是根据当前模型和代码中的特定关系做出的权宜之计呢,还是反映了底层领域的某种轮廓?”
把设计元素(操作、接口、类和AGGREGATE)分解为内聚的单元,在这个过程中,你对领域中一切重要划分的直观认识也要考虑在内。在连续的重构过程中观察发生变化和保证稳定的规律性,并寻找能够解释这些变化模式的底层CONCEPTUAL CONTOUR。使模型与领域中那些一致的方面(正是这些方面使得领域成为一个有用的知识体系)相匹配。
设计即使是按照CONCEPTUAL CONTOUR进行,也仍然需要修改和重构。当连续的重构往往只是做出一些局部修改(而不是对模型的概念产生大范围的影响)时,这就是模型已经与领域相吻合的信号。如果遇到了一个需求,它要求我们必须大幅度地修改对象和方法的划分,那么这就在向我们传递这样一条信息:我们对领域的理解还需要精化。它提供了一个深化模型并且使设计变得更具柔性的机会。
INTENTION-REVEALING INTERFACE使客户能够把对象表示为有意义的单元,而不仅仅是一些机制。SIDE-EFFECT-FREE FUNCTION和ASSERTION使我们可以安全地使用这些单元,并对它们进行复杂的组合。CONCEPTUAL CONTOUR的出现使模型的各个部分变得更稳定,也使得这些单元更直观,更易于使用和组合。
10.5 模式:STANDALONE CLASS
即使把系统分成了各个MODULE,如果不严格控制MODULE内部的依赖的话,那么MODULE也一样会让我们耗费很多精力去考虑依赖关系。
即使是在MODULE内部,设计也会随着依赖关系的增加而变得越来越难以理解。这加重了我们的思考负担,从而限制了开发人员能处理的设计复杂度。隐式概念比显式引用增加的负担更大。
仔细选择模型和设计能够大幅减少依赖关系——常常能减少到零。
低耦合是对象设计的一个基本要素。尽一切可能保持低耦合。把其他所有无关概念提取到对象之外。这样类就变得完全独立了,这就使得我们可以单独地研究和理解它。每个这样的独立类都极大地减轻了因理解MODULE而带来的负担。
我们的目标不是消除所有依赖,而是消除所有不重要的依赖。
尽力把最复杂的计算提取到STANDALONE CLASS(独立的类)中,实现此目的的一种方法是从存在大量依赖的类中将VALUE OBJECT建模出来。
低耦合是减少概念过载的最基本办法。独立的类是低耦合的极致。
消除依赖性并不是说要武断地把模型中的一切都简化为基本类型,这样只会削弱模型的表达能力。
CLOSURE OF OPERATION(闭合操作)就是一种在减小依赖性的同时保持丰富接口的技术。
10.6 模式:CLOSURE OF OPERATION
当我们对集合中的任意两个元素组合时,结果仍在这个集合中,这就叫做闭合操作。
在适当的情况下,在定义操作时让它的返回类型与其参数的类型相同。如果实现者(implementer)的状态在计算中会被用到,那么实现者实际上就是操作的一个参数,因此参数和返回值应该与实现者有相同的类型。这样的操作就是在该类型的实例集合中的闭合操作。闭合操作提供了一个高层接口,同时又不会引入对其他概念的任何依赖。
这种模式更常用于VALUE OBJECT的操作。由于ENTITY的生命周期在领域中十分重要,因此我们不能为了解决某一问题而草率创建一个ENTITY。
ENTITY通常不会成为计算结果。因此,大部分闭合操作都应该到VALUE OBJECT中去寻找。
有时我们会遇到“半个闭合操作”这种情况。参数类型与实现者的类型一致,但返回类型不同;或者返回类型与接收者(receiver)的类型相同但参数类型不同。这些操作都不是闭合操作,但它们确实具有CLOSURE OF OPERATION的某些优点。当没有形成闭合操作的那个多出来的类型是基本类型或基础库类时,它几乎与CLOSURE OF OPERATION一样减轻了我们的思考负担。
MODEL-DRIVEN DESIGN的作用受细节设计的质量和实现决策的质量影响很大,而且只要有少数几个开发人员没有弄清楚它们,整个项目就会偏离目标。
尽管如此,团队只要愿意培养这些建模和设计技巧,那么按照这些模式的思考方式就能够开发出可以反复重构的软件,从而最终创建出非常复杂的软件。
10.7 声明式设计
通常是指一种编程方式—把程序或程序的一部分写成一种可执行的规格(specification)。使用声明式设计时,软件实际上是由一些非常精确的属性描述来控制的。声明式设计有多种实现方式,例如,可以通过反射机制来实现,或在编译时通过代码生成来实现(根据声明来自动生成传统代码)。这种方法使其他开发人员能够根据字面意义来使用声明。它是一种绝对的保证。
据我所知,声明式设计发挥的最大价值是用一个范围非常窄的框架来自动处理设计中某个特别单调且易出错的方面,如持久化和对象关系映射。最好的声明式设计能够使开发人员不必去做那些单调乏味的工作,同时又完全不限制他们的设计自由。
10.8 声明式设计风格
用逻辑运算符来实现SPECIFICATION的方式有很多。如果最简单的方法不适用于你的情况,可以选择其他的方法。
遗憾的是,当把OR和NOT也包括进来时,这些证明会变得更复杂。在大多数情况下,最好避免出现这样的复杂性:要么选择放弃一些运算符,要么不使用包容。如果这二者同时需要,那么要慎重考虑这样做的价值是否多过它所带来的麻烦。
第11章 应用分析模式
我们在现实中总是会遇到这类小的障碍。我们必须做出一些适当的折中选择然后继续前进,而不能因为这些小问题而改变MODEL-DRIVEN DESIGN的方向。
第12章 将设计模式应用于模型
12.1 模式:STRATEGY(也称为POLICY)
我们需要把过程中的易变部分提取到模型的一个单独的“策略”对象中。将规则与它所控制的行为区分开。按照STRATEGY设计模式来实现规则或可替换的过程。策略对象的多个版本表示了完成过程的不同方式。
第四部分 战略设计
第14章 保持模型的完整性
大型系统领域模型的完全统一即不可行,也不划算。
BOUNDED CONTEXT(限界上下文)定义了每个模型的应用范围,而CONTEXT MAP(上下文图)则给出了项目上下文以及它们之间关系的总体视图。
14.1 模式:BOUNDED CONTEXT
细胞之所以能够存在,是因为细胞膜限定了什么在细胞内,什么在细胞外,并且确定了什么物质可以通过细胞膜
任何大型项目都会存在多个模型。而当基于不同模型的代码被组合到一起后,软件就会出现bug、变得不可靠和难以理解。团队成员之间的沟通变得混乱。人们往往弄不清楚一个模型不应该在哪个上下文中使用。
明确地定义模型所应用的上下文。根据团队的组织、软件系统的各个部分的用法以及物理表现(代码和数据库模式等)来设臵模型的边界。在这些边界中严格保持模型的一致性,而不要受到边界之外问题的干扰和混淆。
14.2 模式:CONTINUOUS INTEGRATION
建立一个把所有代码和其他实现工件频繁地合并到一起的过程,并通过自动化测试来快速查明模型的分裂问题。严格坚持使用UBIQUITOUS LANGUAGE,以便在不同人的头脑中演变出不同的概念时,使所有人对模型都能达成一个共识。
14.3 模式:CONTEXT MAP
识别在项目中起作用的每个模型,并定义其BOUNDED CONTEXT。这包括非面向对象子系统的隐含模型。为每个BOUNDED CONTEXT命名,并把名称添加到UBIQUITOUS LANGUAGE中。
正如里根总统在裁减核武器谈判时所说的名言“信任,但要确认” [1] 。
[1].里根把一个俄罗斯谚语翻译成这句名言,这句话一语道破了双边事务的核心——这是连接上下文的又一个隐喻。
14.3.1 测试CONTEXT的边界
测试充当了有用的早期报警系统,特别是在我们必须信赖那些模型细节却又无法控制它们时,它能让我们感到放心。
14.4 BOUNDED CONTEXT之间的关系
开发一个紧密集成产品的优秀团队可以部署一个大的、统一的模型。如果团队需要为不同的用户群提供服务,或者团队的协调能力有限,可能就需要采用SHARED KERNEL(共享内核)或CUSTOMER/SUPPLIER(客户/供应商)关系。有时仔细研究需求之后可能发现集成并不重要,而系统最好采用SEPARATE WAY(各行其道)模式。当然,大多数项目都需要与遗留系统或外部系统进行一定程度的集成,这就需要使用OPEN HOST SERVICE(开放主机服务)或ANTICORRUPTION LAYER(防护层)。
14.5 模式:SHARED KERNEL
从领域模型中选出两个团队都同意共享的一个子集。当然,除了这个模型子集以外,还包括与该模型部分相关的代码子集,或数据库设计的子集。这部分明确共享的内容具有特殊的地位,一个团队在没与另一个团队商量的情况下不应擅自更改它。
共享内核中必须集成自动测试套件,因为修改共享内核时,必须要通过两个团队的所有测试。
使用SHARED KERNEL的目的是减少重复(并不是消除重复,因为只有在一个BOUNDED CONTEXT中才能消除重复),并使两个子系统之间的集成变得相对容易一些。
14.6 模式:CUSTOMER/SUPPLIER DEVELOPMENT TEAM
在极限编程项目中,已经有了实现此目的的机制——迭代计划过程。我们只需根据计划过程来定义两个团队之间的关系。下游团队的代表类似于用户代表,参加上游团队的计划会议,上游团队直接与他们的“客户”同仁讨论和权衡其所需的任务。结果是供应商团队得到一个包含下游团队最需要的任务的迭代计划,或是通过双方商定推迟一些任务,这样下游团队也就知道这些被推迟的功能不会交付给他们。
在两个团队之间建立一种明确的客户/供应商关系。在计划会议中,下游团队相当于上游团队的客户。根据下游团队的需求来协商需要执行的任务并为这些任务做预算,以便每个人都知道双方的约定和进度。
两个团队共同开发自动化验收测试,用来验证预期的接口。把这些测试添加到上游团队的测试套件中,以便作为其持续集成的一部分来运行。这些测试使上游团队在做出修改时不必担心对下游团队产生副作用。
这种模式有两个关键要素。
(1) 关系必须是客户与供应商的关系,其中客户的需求是至关重要的。由于下游团队并不是唯一的客户,因此不同客户的要求必须通过协商来平衡,但这些要求都是非常重要的。这种关系与那种经常出现的“穷亲威”关系相反,在后者的关系中,下游团队不得不乞求上游团队满足其需求。
(2) 必须有自动测试套件,使上游团队在修改代码时不必担心破坏下游团队的工作,并使下游团队能够专注于自己的工作,而不用总是密切关注上游团队的行动。
CUSTOMER/SUPPLIER TEAM涉及的团队如果能在同一个部门中工作,最后会形成共同的目标,这样成功机会将更大一些,如果两个团队分属不同的公司,但实际上也具有这些角色,同样也容易成功。但是,当上游团队不愿意为下游团队提供服务时,情况就会完全不同……
14.7 模式:CONFORMIST
通过严格遵从上游团队的模型,可以消除在BOUNDED CONTEXT之间进行转换的复杂性。尽管这会限制下游设计人员的风格,而且可能不会得到理想的应用程序模型,但选择CONFORMITY模式可以极大地简化集成。此外,这样还可以与供应商团队共享UBIQUITOUS LANGUAGE。供应商处于统治地位,因此最好使沟通变容易。他们从利他主义的角度出发,会与你分享信息。
14.8 模式:ANTICORRUPTION LAYER
创建一个隔离层,以便根据客户自己的领域模型来为客户提供相关功能。这个层通过另一个系统现有接口与其进行对话,而只需对那个系统作出很少的修改,甚至无需修改。在内部,这个层在两个模型之间进行必要的双向转换。
14.10 模式:OPEN HOST SERVICE
定义一个协议,把你的子系统作为一组SERVICE供其他系统访问。开放这个协议,以便所有需要与你的子系统集成的人都可以使用它。当有新的集成需求时,就增强并扩展这个协议,但个别团队的特殊需求除外。满足这种特殊需求的方法是使用一次性的转换器来扩充协议,以便使共享协议简单且内聚。
14.11 模式:PUBLISHED LANGUAGE
把一个良好文档化的、能够表达出所需领域信息的共享语言作为公共的通信媒介,必要时在其他信息与该语言之间进行转换。
14.12 “大象”的统一
承认多个互相冲突的领域模型实际上正是面对现实的做法。通过明确定义每个模型都适用的上下文,可以维护每个模型的完整性,并清楚地看到要在两个模型之间创建的任何特殊接口的含义。盲人没办法看到整个大象,但只要他们承认各自的理解是不完整的,他们的问题就能得到解决。
第15章 精炼
15.1 模式:CORE DOMAIN
一个严峻的现实是我们不可能对所有设计部分进行同等的精化,而是必须分出优先级。为了使领域模型成为有价值的资产,必须整齐地梳理出模型的真正核心,并完全根据这个核心来创建应用程序的功能。但本来就稀缺的高水平开发人员往往会把工作重点放在技术基础设施上,或者只是去解决那些不需要专门领域知识就能理解的领域问题(这些问题都已经有了很好的定义)。
计算机科学家对系统的这些部分更感兴趣,他们认为通过这些工作可以让自己具备一些在其他地方也能派上用场的专业技能,同时也丰富了个人简历。而真正体现应用程序价值并且使之成为业务资产的领域核心却通常是由那些技术水平稍差的开发人员完成的,他们与DBA一起创建数据模式,然后逐个特性编写代码,而根本没有对模型的概念能力加以任何利用。
如果软件的这个部分实现得很差,那么无论技术基础设施有多好,无论支持功能有多完善,应用程序永远都不会为用户提供真正有吸引力的功能。这个严重问题的根源在于项目没有一个明确的整体设计视图,而且也没有认清各个部分的相对重要性。
对模型进行提炼。找到CORE DOMAIN并提供一种易于区分的方法把它与那些起辅助作用的模型和代码分开。最有价值和最专业的概念要轮廓分明。尽量压缩CORE DOMAIN。
让最有才能的人来开发CORE DOMAIN,并据此要求进行相应的招聘。在CORE DOMAIN中努力开发能够确保实现系统蓝图的深层模型和柔性设计。仔细判断任何其他部分的投入,看它是否能够支持这个提炼出来的CORE。
15.1.2 工作的分配
在项目团队中,技术能力最强的人员往往缺乏丰富的领域知识。这限制了他们的作用,并且更倾向于分派他们来开发一些支持组件,从而形成了一个恶性循环——知识的缺乏使他们远离了那些能够学到领域知识的工作。
打破这种恶性循环是很重要的,方法是建立一支由开发人员和一位或多位领域专家组成的联合团队,其中开发人员必须能力很强、能够长期稳定地工作并且对学习领域知识非常感兴趣,而领域专家则要掌握深厚的业务知识。如果你认真对待领域设计,那么它就是一项有趣且充满技术挑战的工作。你肯定也会找到持这种观点的开发人员。
15.3 模式:GENERIC SUBDOMAIN
15.3.1 通用不等于可重用
精炼的基本动机——我们应该尽可能把大部分精力投入到CORE DOMAIN工作中,而只在必要的时候才在支持性的GENERIC SUBDOMAIN中投入工作。
重用确实会发生,但不一定总是代码重用。模型重用通常是更高级的重用,例如,当使用公开发布的设计或模型的时候就是如此。
15.4 模式:DOMAIN VISION STATEMENT
写一份CORE DOMAIN的简短描述(大约一页纸)以及它将会创造的价值,也就是“价值主张”。那些不能将你的领域模型与其他领域模型区分开的方面就不要写了。展示出领域模型是如何实现和均衡各方利益的。这份描述要尽量精简。尽早把它写出来,随着新的理解随时修改它。
15.5 模式:HIGHLIGHTED CORE
尽管团队成员可能大体上知道核心领域是由什么构成的,但CORE DOMAIN中到底包含哪些元素,不同的人会有不同的理解,甚至同一个人在不同的时间也会有不同的理解。如果我们总是要不断过滤模型以便识别出关键部分,那么就会分散本应该投入到设计上的精力,而且这还需要广泛的模型知识。因此,CORE DOMAIN必须要很容易被分辨出来。
15.5.1 精炼文档
编写一个非常简短的文档(3~7页,每页内容不必太多),用于描述CORE DOMAIN以及CORE元素之间的主要交互过程。
独立文档带来的所有常见风险也会在这里出现:
(1) 文档可能得不到维护;
(2) 文档可能没人阅读;
(3) 由于有多个信息来源,文档可能达不到简化复杂性的目的。
控制这些风险的最好方法是保持绝对的精简。剔除那些不重要的细节,只关注核心抽象以及它们的交互,这样文档的老化速度就会减慢,因为这个层次的模型通常更稳定。
精炼文档应该能够被团队中的非技术人员理解。把它当作一个共享的视图,描述每个人都应该知道的东西,而且可以把它作为团队所有成员研究模型和代码的一个起点。
15.5.2 标明CORE
把模型的主要存储库中的CORE DOMAIN标记出来,不用特意去阐明其角色。使开发人员很容易就知道什么在核心内,什么在核心外。
15.5.3 把精炼文档作为过程工具
如果精炼文档概括了CORE DOMAIN的核心元素,那么它就可以作为一个指示器——用以指示模型改变的重要程度。当模型或代码的修改影响到精炼文档时,需要与团队其他成员一起协商。当对精炼文档做出修改时,需要立即通知所有团队成员,而且要把新版本的文档分发给他们。CORE外部的修改或精炼文档外部的细节修改则无需协商或通知,可以直接把它们集成到系统中,其他成员在后续工作过程中自然会看到这些修改。这样开发人员就拥有了XP所建议的完全的自治性。
15.6 模式:COHESIVE MECHANISM
把概念上的COHESIVE MECHANISM(内聚机制)分离到一个单独的轻量级框架中。要特别注意公式或那些有完备文档的算法。用一个INTENTION-REVEALING INTERFACE来暴露这个框架的功能。现在,领域中的其他元素就可以只专注于如何表达问题(做什么)了,而把解决方案的复杂细节(如何做)转移给了框架。
15.6.1 GENERIC SUBDOMAIN与COHESIVE MECHANISM的比较
GENERIC SUBDOMAIN与COHESIVE MECHANISM的动机是相同的——都是为CORE DOMAIN减负。区别在于二者所承担的职责的性质不同。GENERIC SUBDOMAIN是以描述性的模型作为基础的,它用这个模型表示出团队会如何看待领域的某个方面。在这一点上它与CORE DOMAIN没什么区别,只是重要性和专门程度较低而已。COHESIVE MECHANISM并不表示领域,它的目的是解决描述性模型所提出来的一些复杂的计算问题。
模型提出问题,COHESIVE MECHANISM解决问题。
15.8 模式:SEGREGATED CORE
15.8.1 创建SEGREGATED CORE的代价
当系统有一个很大的、非常重要的BOUNDED CONTEXT时,但模型的关键部分被大量支持性功能掩盖了,那么就需要创建SEGREGATED CORE了。
15.9 模式:ABSTRACT CORE
把模型中最基本的概念识别出来,并分离到不同的类、抽象类或接口中。设计这个抽象模型,使之能够表达出重要组件之间的大部分交互。把这个完整的抽象模型放到它自己的MODULE中,而专用的、详细的实现类则留在由子领域定义的MODULE中。
15.10 深层模型精炼
尽管任何带来深层模型的突破都有价值,但只有CORE DOMAIN中的突破才能改变整个项目的轨道。
第16章 大型结构
在一个大的系统中,如果因为缺少一种全局性的原则而使人们无法根据元素在模式(这些模式被应用于整个设计)中的角色来解释这些元素,那么开发人员就会陷入“只见树木,不见森林”的境地。
16.2 模式:SYSTEM METAPHOR
当系统的一个具体类比正好符合团队成员对系统的想象,并且能够引导他们向着一个有用的方向进行思考时,就应该把这个类比用作一种大型结构。围绕这个隐喻来组织设计,并把它吸收到UBIQUITOUS LANGUAGE中。SYSTEM METAPHOR应该既能促进系统的交流,又能指导系统的开发。它可以增加系统不同部分之间的一致性,甚至可以跨越不同的BOUNDED CONTEXT。但所有隐喻都不是完全精确的,因此应不断检查隐喻是否过度或不恰当,当发现它起到妨碍作用时,要随时准备放弃它。
16.3 模式:RESPONSIBILITY LAYER
分层模式有一种变体最适合按职责来分层,我们把这种变体称为RELAXED LAYERED SYSTEM(松散分层系统)[Buschmann et al.1996,p.45],如果采用这种分层模式,某一层中的组件可以访问任何比它低的层,而不限于只能访问直接与它相邻的下一层。
第17章 领域驱动设计的综合运用
17.4 由谁制定策略
17.4.1 从应用程序开发自动得出的结构
通常,让团队中的一个人(或几个人)来承担大型结构的一些监管职责有利于保持结构统一。如果这位承担监管职责的非正式的领导人也是一位负责具体工作的开发人员(仲裁者和协调员),而不是决策的唯一制定者,那么这种方法将特别有效。
17.5 制定战略设计决策的6个要点
决策必须传达到整个团队
在一个沟通良好的项目中,应用开发团队所产生的策略设计实际上会更有效地传播到每个人。这样策略将会实际发挥作用,而且具有权威性,因为它是通过集体智慧制定的决策。
决策过程必须收集反馈意见
我曾经与一个技术架构团队合作过,这个团队把成员轮流派到使用其架构的各个应用开发团队中。这种流动性使架构团队亲身体验到了开发人员所面临的挑战,同时也把如何应用框架的知识传播给了开发人员。战略设计同样需要这种紧密的反馈循环。
事实上,几乎任何事物都会对其他某个事物构成障碍,因此每个元素都必须是确实值得存在的。我们需要有一个谦逊的态度,才能认识到我们自己认为的最佳思路可能会妨碍其它人。
对象的职责要专一,而开发人员应该是多面手
良好的对象设计的关键是为每个对象分配一个明确且专一的职责,并且把对象之间的互相依赖减至最小。
17.5.1 技术框架同样如此
不要编写“傻瓜式”的框架
在划分团队时,如果认为一些开发人员不够聪明,无法胜任设计工作,而让他们去做开发工作,那么这种态度可能会导致失败,因为他们低估了应用程序开发的难度。如果这些人在设计方面不够聪明,就不应该让他们来开发软件。如果他们足够聪明,那么这种隔离只会造成障碍,使他们得不到所需的工具。
注意,把无关的技术细节封装起来与我所反对的这种“傻瓜式”的预打包完全不同。框架可以为开发人员提供有力的抽象和工具,使他们不用去做那么多苦差事。有用的封装和“傻瓜式”的预打包之间的区别很难用一种通用的方式描述出来,但只要问问框架设计人员他们对将要使用工具/框架/组件的那些人有什么期望,就可以看出区别。如果设计人员对框架的用户非常尊重,那么他们的工作方向可能就是正确的。