【第一部分】运用领域模型
第1章:消化知识
有效的建模要素
(1)模型和实现的绑定
(2)建立了一种基于模型的语言
(3)开发一个蕴含丰富知识的模型
(4)提炼模型
(5)头脑风暴和实验
【学习心得】:千万不要用自己有限的思维规划完整的图形,持续学习、消化、输出(讨论)、沉淀,所有道理都是一致的。
第2章:交流语言与使用
模式:UBIQUITOUS LANGUAGE(通用语言)
想要创建种灵活的、蕴含丰富知识的设计,需要一种通用的、共享的团队语言,以及对语言不断的试验——然而,软件项目上很少出现这样的试验。
如果语言支离破碎,项目必将遭遇严重问题。领域专家使用他们自己的术语,而技术团队所使用的语言则经过调整,以便从设计角度讨论领域。日常讨论所使用的术语与代码(软件项目的最重要产品)中使用的术语不一致,甚至同一个人在讲话和写东西时使用的言语也不一致,这导致的后果是,对领域的深刻表达常常稍纵即逝,根本无法记录到代码或文档中。翻译使得沟通不畅,并削弱了知识消化。然而任何一方的语言都不能成为公共语言,因为它们无法满足所有的需求。
【学习心得】:在自己有限的项目经验里,说沟通成本占据项目总成本的八成都不为过,就像本书一开始的重点,就是无处不在的语言。这语言可以是人话、可以是图形、可以是表格,重点在于可以帮助项目高质量高效率的落地。这里引用歌德的一句话:“世界上的误解和懈怠,也许比奸诈和恶意更误事”。
第3章:绑定模型和实现
模式:MODEL-DRIVEN-DESIGN
严格按照基础模型来编写代码,能够使代码更好地表达设计含义,并且使模型与实际的系统想契合。
如果整个程序设计或者其核心部分没有与领域模型相对应,那么这个模型就是没有价值的,软件的正确性也值得怀疑。同时,模型和设计功能之间过于复杂的对应关系也是难于理解的,在实际项目中,当设计改变时也无法维护这种关系。若分析与设计之间产生严重分歧,那么在分析和设计活动中所获得的知识就无法彼此共享。
软件系统各个部分的设计应该忠实地反映领域模型,以便体现出这二者之间的明确对应关系。我们应该反复检查并修改模型,以便软件可以更加自然地实现模型,即使想让模型反映出更深层次的领域概念时也应如此。我们需要的模型不但应该满足这种需求,还应该能够支持健壮的UBIQUITOUS LANGUAGE(通用语言)。
从模型中获取用于程序设计和基本职责分配的术语。让程序代码成为模型的表达,代码的改变可能会是模型的改变。而其影响势必要波及接下来相应的项目活动。
【学习心得】:模型、范式与设计的基本认知时候所有沟通的基石,无论是技术人员还是领域业务人员都有必要对这些知识有一个深入的理解,切记把自己局限在自己的细节当中,用人话讲就是钉子思维。对其他工作小组的认识是一种促进大家更好合作的责任心态度,还是那句话,用宏观的视野做微观的事情。
【 第二部分 】模型驱动设计的构造块
第4章:分离领域
模式:LAYERED ARCHTECTURE
想要创建出能够处理复杂任务的程序,需要做到关注点分离——使设计中的每个部分得到单独的关注。在分离的同时,也需要维持系统内部复杂的交互关系。
分层的价值在于每一层都只代表程序中的某一特定方面的。这种限制使每个方面的设计都更具内聚性,更容易理解。
而领域层是模型的精髓。领域模型是一些列概念的集合。“领域层”则是领域模型以及所有与其直接相关的设计元素的表现,他由业务逻辑的设计和实现组成。在MODEL-DRIVEN-DESIGN中,领域层的软件构造反映出了模型概念。
【学习心得】:分离意味着原始的复杂,这是发展的一个趋势,技术的进步往往在于精细化的分工,而这种分层的另一个好处是,分离核心,聚焦问题。
第5章:软件中所表示的模型
模式:ENTITY(又称为REFERENCE OBJECT)
一些对象主要不是由它们的属性定义的。它们实际上表示了一条“标识线”(A Thread of Identity),这条线跨越时间,而且常常经历多种不同的表示。有时,这样的对象必须与另一个具有不同属性的对象相匹配。而有时一个对象必须与具有相同属性的另一个对象区分开。错误的标识可能会破坏数据。
当一个对象由其标识(而不是属性)区分时,那么在模型中应该主要通过标识来确定该对象的定义。是类定义变得简单,并集中关注生命周期的连续性和标识。定义一种区分每个对象的方式,这种方式应该与其形式和历史无关。要格外注意那些需要通过属性来匹配对象的需求。在定义标识操作时,要确保这种操作为每个对象生成唯一的结果,这可以通过附加一个保证唯一性的符号来实现。这种定义标识的方法可能来自外部,也可能是由系统创建的任意标示符,但它在模型中必须是唯一的标识。模型必须定义“符合什么条件才算是相同的事物”。
模式:VALUE OBJECT
很多对象没有概念上的标识,它们描述了一个事务的某种特征。而这类用于描述领域的某个方面而本身没有概念标识的对象称为VALUE OBJECT(值对象)。
当我们只关心一个模型元素的属性时,应把它归类为VALUE OBJECT。我们应该使这个模型元素能够表示出其属性的意义,并为它提供相关功能。VALUE OBJECT应该是不可变的。不要为它分配任何标识,而且不要把它设计成像ENTITY那么复杂。
模式:SERVICE
有时,对象不是一个事物。在某些情况下,最清楚、最实用的设计会包含一些特殊的操作,这些操作从概念上讲不属于任何对象。与其把它们强制地归于哪一类,不如顺其自然地在模型中引入一种新的元素,这就是SERVICE(服务)。
所谓SERVICE,它强调的是与其他对象的关系。与ENTITY和VALUE OBJECT不同,它只是定义了能够为客户做什么。SERVICE往往是一个一活动来命名,而不是以一个ENTITY来命名,也就是说,它是动词而不是名词。
好的SERVICE有以下3个特征:
(1)与领域概念相关的操作不是ENTITY或VALUE OBJECT的一个自然组成部分。
(2)接口是根据领域模型的其他元素定义的。
(3)操作是无状态的。
当领域中的某个重要的过程或转换操作不是ENTITY或VALUE OBJECT的自然职责时,应该在模型中添加一个作为独立接口的操作,并将其声明为SERVICE。定义接口时要使用模型语言,并确保操作名称是UBIQUITOUS LANGUAGE中的术语。此外,应该使SERVICE成为无状态的。
SERVICE与孤立的领域层:这种模式只重视那些在领域中具有重要意义的SERVICE,但SERVICE并不只是在领域中使用。我们需要注意区分属于领域层的SERVICE和那些属于其他层的SERVICE,并划分责任,以便将它们明确地区分开。
模式:MODULE(也称为PACKAGE)
MODULE是一个传统的、较成熟的设计元素。虽然使用模块有一些技术上的原因,但主要原因却是“认知超载”。MODULE为人们提供了两种观察模型的方式,一是可以在MODULE中查看细节,而不会被整个模型淹没,二是观察MODULE之间的关系,而不考虑其内部细节。
每个人都会使用MODULE,但却很少有人它们当作模型中的一个成熟的组成部分。代码按照各种各样的类别进行分解,有时是按照技术架构来分割的,有时是按照开发人员的任务分工来分割的。甚至那些从事大量重构工作的开发人员也倾向于使用项目早期形成的一些MODULE。
众所周知,MODULE之间应该是低耦合的,而在MODULE的内部则是高内聚的。耦合和内聚的解释使得MODULE听上去像是一种技术指标,仿佛根据关联和交互的分布情况来机械地判断它们。然而,MODULE并不仅仅是代码的划分,而且也是概念的划分。一个人一次考虑的事情是有限的(因此才要低耦合)。不连贯的思想和“一锅粥”似的思想同样难于理解(因此才要高内聚)。
因此:选择能够描述系统的MODULE,并使之包含一个内聚的概念集合。这通常会实现MODULE之间的低耦合,但如果效果不理想,则应寻找一种更改模型的方式来消除概念之间的耦合,或者找到一个可以作为MODULE基础的概念(这个概念先前可能被忽视了),基于这个概念组织的MODULE可以以一种有意义的方式将元素集中到一起。找到一种低耦合的概念组织方式,从而可以相互独立地理解和分析这些概念。对模型进行精化,直到可以根据高层领域概念对模型进行划分,同时相应的代码也不会产生耦合。MODULE的名称应该是UBIQUITOUS LANGUAGE中的术语。MODULE及其名称应反映出领域的深层知识。
【学习心得】:每一个概念或方法,都有其含义来源和出处。学会寻找信息的源头,学会给自己的认知指明来源和出处,具备严谨的逻辑思维,科学地学习和认知,是一切成功的基础。杜绝垃圾二手信息资料,杜绝自我局限性拍脑袋的认知决策过程。
第6章:领域对象的什么周期
模式:AGGREGATE
在具有复杂关联的模型中,要想保证对象更改的一致性是很困难的。不仅互不关联的对象需要遵守一些固定规则,而且紧密关联的各组对象也要遵守一些固定规则。然而,过于谨慎的锁定机制又会导致多个用户之间毫无意义地互相干扰,从而使系统不可用。
固定规则(invariant)是指在数据变化时必须保持一致性规则,其涉及AGGREGATE成员之间的内部关系。而任何跨越AGGREGATE的规则将不要求每时每刻都保持最新状态。通过事件处理,批处理或其他更新机制,这些依赖会在一定时间内得以解决。但在每个事务完成时,AGGREGATE内部所应用的固定规则必须得到满足。为了实现这个概念上的AGGREGATE,需要对所有事务应用一组规则:
□ 根ENTITY具有全局标识,它最终负责检查固定规则。
□ 根ENTITY具有全局标识。边界内的ENTITY具有本地标识,这些标识只在AGGREGATE内部才是唯一的。
□ AGGREGATE外部的对象不能引用除根ENTITY之外的任何内部对象。根ENTITY可以把对内部ENTITY的引用传递给它们,但这些对象只能临时使用这些引用,而不能保持引用。根可以把一个VALUE OBJECT的副本传递给另外一个对象,而不必关心它发生什么变化,因为它只是一个VALUE,不再与AGGREGATE有任何关联。
□ 作为上一条规则的推论,只有AGGREGATE的根才能直接通过数据库查询获取。所有其他对象必须通过遍历关联来发现。
□ AGGREGATE内部的对象可以保持对其他AGGREGATE根的引用。
□ 删除操作必须一次删除AGGREGATE边界之内的所有对象。(利用垃圾回收机制,这很容易做到。由于除了根以外的其他对象都没有外部引用,因此删除了根以后,其他对象均会被回收。)
□ 当提交对AGGREGATE边界内部的任何对象的修改时,整个AGGREGATE的所有固定规则都必须满足。
我们应该将ENTITY和VALUE OBJECT分门类别地聚集到AGGREGATE中,并定义每个AGGREGATE的边界。在每个AGGREGATE中,选择一个ENTITY作为根,并通过根来控制对边界内其他对象的所有访问。只允许外部对象保持对根对象的引用。对内部成员的临时引用可以被传递出去,但仅在一次操作中有效。由于根控制访问,因此不能绕过它来修改内部对象。这种设计有利于确保AGGREAGATE中的对象满足所有固定规则,也可以确保在任何状态变化时AGGREGATE作为一个整体满足固定规则。
模式:FACTORY
当创建一个对象或创建整个AGGREGATE时,如果创建工作很复杂,或者暴露了过多的内部结果,则可以使用FACTORY进行封装。
对象的创建本身可以是一个主要操作,但被创建的对象并不适合承担复杂的装配操作。将这些职责混在一起可能产生难以理解的拙劣设计。让客户直接负责创建对象又会使客户的设计陷入混乱,并且破坏被装配对象或AGGREGATE的封装,而且导致客户与被创建对象的实现之间产生过于紧密的耦合。
因此:应该讲创建复杂对象的实例和AGGREGATE的职责转移给单独的对象,这个对象本身可能没有承担领域模型中的职责,但它仍是领域设计的一部分。提供一个封装所有复杂装配操作的接口,而且这个接口不需要客户引用要被实例化的对象的具体类。在创建AGGREGATE时要把它作为一个整体,并确保它满足固定规则。
模式:REPOSITORY
在所有持久化对象中,有一小部分必须通过基于对象属性的搜索来全局访问。当很难通过遍历方式来访问某些AGGREGATE根的时候,就需要使用这种访问方式。它们通常是ENTITY,有时是具有复杂内部结构的VALUE OBJECT,还可能是枚举VALUE。而其他对象则不宜使用这种方式,因为这会混淆它们之间的重要区别。随意的数据库查询会破坏领域对象的封装和AGGREGATE。技术基础设施和数据库访问机制的暴露会增加客户的复杂度,并妨碍模型驱动的设计。
REPOSITORY是一个简单的概念框架,它可用来封装这些解决方案,并将我们的注意力重新拉回到模型上。REPOSITORY将某种类型的所有对象表示为一个概念集合(通常是模拟的)。它的行为类似于集合(collection),只是具有更复杂的查询功能。
因此:为每种需要全局访问的对象类型创建一个对象,这个对象相当于该类型的所有对象在内存中的一个集合的“替身”。通过一个众所周知的全局接口来提供访问。提供添加和删除对象的方法,用这些方法来封装在数据存储中实际插入或删除数据的操作。提供根据具体条件来挑选对象的方法,并返回属性值满足查询条件的对象或对象集合(所返回的对象是完全实例化的),从而将实际的存储和查询技术封装起来。只为那些确实需要直接访问的AGGREGATE根提供REPOSITORY。让客户始终聚焦于模型,而将所有对象的存储和访问操作交给REPOSITORY来完成。
FACTORY与REPOSITORY的关系是:FACTORY负责处理对象生命周期的开始,而REPOSITORY帮助管理生命周期的中间和结束。从领域驱动设计的角度来看,FACTORY和REPOSITORY具有完全不同的职责。FACTORY负责制造新对象,而REPOSITORY负责查找已有对象。
【学习心得】:有时候学习上的困难不是因为自己的理解能力差,而是缺乏一定的基础沟通语言。急于求成和半路出家的问题就在于基础的不扎实,也就是我们所说的野路子。我曾经也会认为用到了再来学,这都是技术圈子的一个悖论。就好像等自己需要用钱了再来理财一样可笑。
第7章:使用语言:一个扩展的示例
略
【 第三部分 】通过重构来加深理解
第8章:突破
一般来说,持续重构让事物逐步变得有序。代码和模型的每一次精化都让开发人员有了更加清晰的认识。这使得理解上的突破成为可能。之后,一系列快速的改变得到了更符合用户需要并更加切合实际的模型。其功能性及说明性急速增强,而复杂性却随之消失。这种突破不是某种技巧,而是一个事件。它的困难之处在于你需要判断发生了什么,然后再决定如何处理。
当突破带来更深层次的模型时,通常会令人感到不安。与大部分重构相比,这种变化的回报更多,风险也更高。而且突破出现的时机可能很不合时宜。尽管我们希望进展顺利,但往往事与愿违。过渡到真正的深层次模型需要从根本上调整思路,并且对设计做大幅修改。在很多项目中,建模和设计工作最重要的进展都来自于突破。
不要试图去制造突破,那只会使项目陷入困境。通常,只有在实现了许多适度的重构后才有可能出现突破。在大部分时间里,我们都在进行微小的改进,而在这种连续的改进中模型深层含义也会逐渐显现。
要为突破做好准备,应专注于知识消化过程,同时也要逐渐建立健壮的UBIQUITOUS LANGUAGE。寻找那些重要的领域概念,并在模型中清晰地表达出来。精化模型,使其更具有柔性。提炼模型。利用这些更容易掌握的手段使模型变得更清晰,这通常会带来突破。
不要犹豫着不去做小的改进,这些改进即使脱离不开常规的概念框架,也可以逐渐加深我们对模型的理解。不要因为好高骛远而使项目陷入困境。只要随时注意可能出现的机会就够了。
【学习心得】:我自己手头上目前持有一个运行了近十年的千万级用户系统,至今还在持续运营和追加功能。对于以上那些突破的感受,我太有体会了。整个项目总共经历了两次大的突破,以及无数次小突破。而每一次突破都十分痛苦,但快乐着。离不开团队的坚持,离不开团队的持续学习,更离不开团队吃苦精神。
第9章:将隐式概念转变为显示概念
深层建模听起来很不错,但是我们要如何时间它呢?深层模型之所以强大是因为它包含了领域的核心概念和抽象,能够以简单灵活的方式表达出基本的用户活动、问题以及解决方案。
若开发人员识别出设计中隐含的某个概念或者在讨论中收到启发而发现一个概念时,就会对领域建模和响应的代码进行许多转换,在模型中加入一个或多个对象或关系,从而将此概念显示地表达出来。有时,这种从隐式概念到显示概念的转换可能就是一次突破。
概念挖掘
1、倾听领域专家使用的语言。
有没有一些术语能够简洁地表达出复杂的概念?他们有没有纠正过你的用词(也许是很委婉的提醒)?当你使用某个特定词语时,他们脸上是否已经不再流露出迷惑的表情?这些都是暗示了某个概念也许可以改进模型。
2、检查不足之处。
你所需要的概念并不总是浮在表面上,也绝不仅仅是通过对话和文档就能让它显现出来。有些概念可能需要你自己去挖掘和创造。要挖掘的地方就是设计中最不足的地方,也就是操作复杂且难于理解的地方。每当有新需求时,似乎都会让这个地方变得更加复杂。有时,你很难意识到模型中丢失了什么概念。也许你的对象能够实现所有的功能,但是有些职责的实现却很笨拙。而有时,你虽然能够意识到模型中丢失了某些东西,但是却无法找到解决方案。
3、思考矛盾之处。
由于经验和需求的不同,不同的领域专家对同样的事情会有不同的看法。即使是同一个人提供的信息,仔细分析后也会发现逻辑上不一致的地方。在挖掘程序需求的时候,我们会不断遇到这种令人烦恼的矛盾,但它们也为深层模型的实现提供了重要线索。有时矛盾只是术语说法上的不一致,有些则是由于误解而产生的。但还有一种情况是专家们会给出相互矛盾的两种说法。
4、查阅书籍。
在寻找模型概念时,不要忽略一些显而易见的资源。在很多领域中,你都可以找到解释基本概念和传统思想的书籍。你依然需要与领域专家合作,提炼与你的问题相关的那部分知识,然后将其转化为适用于面向对象软件的概念。但是,查阅书籍也许能够使你一开始就形成一致且深层的认识。
5、尝试,再尝试。
并不是所有这些方向性的改变都毫无用处。每次改变都会把开发人员更深刻的理解添加到模型中。每次重构都使设计变得更灵活且为那些可能需要修改的地方做好准备。我们其实别无选择。只有不断尝试才能了解什么有效什么无效。企图避免设计上的失误将会导致开发出来的产品质量劣质,因为没有更多的经验可用来借鉴,同时也会比进行一系列快速实验更加费时。
如何为那些不太明显的概念建模?
1、显示的约束。
约束是模型概念中非常重要的类别。它们通常是隐含的,将它们显式地表现出来可以极大地提高设计质量。
2、将过程建模为领域对象。
首先要说明的是,我们都不希望过程变成模型的主要部分。对象是用来封装过程的,这样我们只需要考虑对象的业务目的或意图就可以了。就像我们以上用来安排货运路线的运输系统例子,安排路线的过程具有业务意义。SERVICE是显示表达这种过程的一种方式,同时它还会降异常复杂的算法封装起来。
如果过程的执行有多种方式,那么我们也可以用另一种方法来处理它,那就是将算法本身或其中的关键部分放到一个单独的对象中。这样,选择不同的过程就变成了选择不同的对象,每个对象都表示一种不同的STRATEGY(策略)。
那么,过程是应该被显示表达出来,还是应该被隐藏起来呢?区分的方法很简单:它是经常被领域专家提起呢,还是仅仅被当作计算机程序机制的一部分?
约束和过程是两大类概念模型,当我们用面向对象语言编程时,不会立即想到它们,然而它们一旦被我们视为模型元素,就真的可以让我们的设计更为清晰。
3、模式:SPECIFICATION
业务规则通常不适合作为ENTITY和VALUE OBJECT的职责,而且规则的变化和组合也会被掩盖领域对象的基本含义。但是将规则移出领域层的结果会更糟糕,因为这样一来,领域代码就不再表达模型了。
逻辑编程提供了一个概念,即“谓词”这种可分离、可组合的规则对象,但是要把这种概念用对象完全实现是很麻烦的。同时,这种概念过于通用,在表达设计意图方面,它的针对性不如专门的设计那么好。
因此:为特殊目的创建谓词形式的显式的VALUE OBJECT。SPECIFICATION就是一个谓词,可用来确定对象是否满足某些标准。
【学习心得】:项目的沟通成本之大,正是因为许多人的内心都会有一颗“自尊心”,我的领域我最懂,我的技术牛逼,你们业务人员可以一边站。所以,想成为一个合格的团队成员,至少得让自己能成为一个合格的聆听者。看似简单,但我在生活中就发现了自己并非一位很好的聆听者。错过了许多对自己有用的信息,更多还是用了许多自以为是的拍脑袋决策。
第10章:柔性设计
为了使项目能够随着开发工作的进行加速前进,而不会由于它自己的老化将停滞不前,设计必须要让人们乐于使用,而且易于做出修改。这就是柔性设计(supple design)。
很多过度设计(overengineering)借着灵活性的名义而得到合理的外衣。但是,过多的抽象层和间接设计常常成为项目的绊脚石。看一下真正为用户带来强大功能的软件设计,你常常会发现一些简单的东西。简单并不容易做到。
模式:INTENTION-REVEALING INTERFACES(意图揭示接口)
如果开发人员为了使用一个组件而必须要去研究它的实现,那么就失去了封装的价值。当某个人开发的对象或操作被别人使用时,如果使用这个组件的新的开发者不得不根据其实现来推测其用途,那么他推测出来的可能并不是那个操作或类的主要用途。如果这不是那个组件的用途,虽然代码暂时可以工作,但设计的概念基础已经被误用了,两位开发人员的意图也是背道而驰。
因此:在命名类和操作时要描述它们的效果和目的,而不是表露它们是通过何种方式达到目的的。这样可以使客户开发人员不必去理解内部细节。这些名称应该与UBIQUITOUS LANGUAGE保持一致,以便团队成员可以迅速推断出它们的意义。在创建一个行为之前先为它编写一个测试,这样可以促使你站在客户开发人员的角度上来思考它。所有复杂的机制都应该封装到抽象接口的后面,接口只表明意图,而不表明方式。
模式:SIDE-EFFECT-FREE FUNCTION(无副作用功能)
大多数操作都会调用其他的操作,而后者又会调用另外一些操作。一旦形成这种任意深度的嵌套,就很难预测调用一个操作将要产生的所有后果。第二层和第三层操作的影响可能并不是客户开发人员有意为之的,也是它们就变成了完全意义上的副作用(任何对未来操作产生影响的系统状态改变都可以成为副作用)。
多个规则的相互作用或计算的组合产生的结果是很难预测的。开发人员在调用一个操作时,为了预测操作的结果,必须理解它的实现以及它所调用的其他方法的实现。如果开发人员不得不“揭开接口的面纱”,那么接口的抽象作用就受到了限制。如果没有了可以安全地预见到结果的抽象,开发人员就必须限制“组合爆炸”,这就限制了系统行为的丰富性。
因此:尽可能把程序的逻辑放到函数(返回结果而不产生副作用的操作称为函数)中,因为函数是只返回结果而不产生明显副作用的操作。严格地把命令(引起明显的状态改变的方法)隔离到不返回领域信息的、非常简单的操作中。当发现了一个非常适合承担复杂逻辑职责的概念时,就可以把这个复杂逻辑移到VALUE OBJECT中,这样可以进一步控制副作用。
模式:ASSERTION(断言)
把复杂的计算封装到SIDE-EFFECT-FREE FUNCTION中可以简化问题,但实体仍然会留有一些有副作用的命令,使用这些ENTITY的人必须了解使用这些命令的后果。
如果操作的副作用仅仅是由它们的实现隐式定义的,那么在一个具有大量相互调用关系的系统中,起因和结果会变得一团糟。理解程序的唯一方式就是沿着分支路径来跟踪程序的执行,封装完全失去了价值。跟踪具体的执行也使抽象失去了意义。
因此:把操作的后置条件和类及AGGREGATE的固定规则表达清楚。如果在你的编程语言中不能直接编写ASSERTION,那么就把它们编写成自动的单元测试。还可以把它们写到文档或图中(如果符合项目开发风格的话)。寻找在概念上内聚的模型,以便使开发人员更容易推断出预期的ASSERTION,从而加快学习过程并避免代码矛盾。
INTENTION-REVEALING INTERFACE清楚地表明了用途,SIDE-EFFECT-FREE FUNCTION和ASSERTION使我们能够更准确地预测结果,因此封装和抽象更加安全。
模式:CONCEPTUAL CONTOUR(概念轮廓)
如果把模型或设计的所有元素都放在一个整体的大结构中,那么它们的功能就会发生重复。外部接口无法给出客户可能关心的全部信息。由于不同的概念被混合在一起,它们的意义变得很难理解。
而另一方面,把类和方法分解开也可能是毫无意义的,这会使客户更复杂,迫使客户对象去理解各个细微部分是如何组合在一起的。更糟的是,有的概念可能会完全丢失。铀原子的一半并不是铀。而且,粒度的大小并不是唯一要考虑的问题,我们还要考虑粒度是在哪种场合下使用的。
因此:把设计元素(操作、接口、类和AGGREGATE)分解为内聚单元,在这个过程中,你对领域中一切重要划分的直观认识也要考虑在内。在连续的重构过程中观察发生变化和保证稳定的规律性,并寻找能够解释这些变化模式的底层CONCEPTUAL CONTOUR。使模型与领域中那些一致的方面(正是这些方面使得领域成为一个有用的知识体系)相匹配。
我们的目标是得到一组可以在逻辑上组合起来的简单接口,使我们可以用UBIQUITOUS LANGUAGE进行合理的表述,并且使那些无关的选项不会分散我的注意力,也不增加维护负担。但这通常是通过重构才能得到的结果,很难在前期就实现。而且如果仅仅是从技术角度进行重构,可能永远也不会出现这种结果;只有通过重构得到更深层次的理解,才能实现这样的目标。
INTENTION-REVEALING INTERFACE使客户能够把对象表示为有意义的单元,而不仅仅是一些机制。SIDE-EFFECT-FREE FUNCTION和ASSERTION使我们可以安全地使用这些单元,并对它们进行复杂的组合。CONCEPTUAL CONTOUR的出现使模型的各个部分变得更加稳定,也使得这些单元更直观,更易于使用和组合。
模式:STANDALONE CLASS(独立的类)
即使是在MODULE内部,设计也会随着依赖关系的增加而变得越来越难以理解。这加重了我们的思考负担,从而限制了开发人员能处理的设计复杂度。隐式概念比显式引用增加的负担更大了。
低耦合是对象设计的一个基本要素。尽一切可能保持低耦合。把其他所有无关概念提取到对象之外。这样类就变得完全独立了,这就使得我们可以单独地研究和理解它。每个这样的独立类都极大地减轻了因理解MODULE而带来的负担。
尽力把最复杂的计算提取到STANDALONE CLASS(独立的类)中,实现此目的的一种方法是从存在大量依赖的类中将VALUE OBJECT建模出来。低耦合是减少概念过载的最基本方法。独立的类是低耦合的极致。
模式:CLOSURE OF OPERATION(闭合操作)
两个实数相乘,结果仍为实数(实数是所有有理数和所有无理数的集合)。由于这一点永远成立,因此我们说实数的“乘法运算是闭合的”:乘法运算的结果永远无法脱离实数这个集合。当我们对集合中的任意两个元素组合时,结果仍在这个集合中,这就叫做闭合操作。
——The Math Forum,Drexel University
加法运算是实数集中的闭合运算。数学家们都极力避免去引入无关的概念,而闭合运算的性质正好为他们提供了这样一种方式。
因此:在适当情况下,在定义操作时让它的返回类型与其参数的类型相同。如果实现者(implementer)的状态在计算中会被用到,那么实现者实际上就是操作一个参数,因此参数和返回值应该与实现者有相同的类型。这样的操作就是在该类型的实例集合中的闭合操作。闭合操作提供了一个高层接口,同时又不会引人对其他概念的任何依赖。
【学习心得】:经历了(还在经历)一个近十年的项目,我想自己还是比较有资格谈谈柔性设计的感受。没有学习这些柔性概念之前,我们能持续高效并运行开发一个项目那么长时间,功劳归于一个重要的原则:简单。起初,整个团队都缺乏以上这些实用的模式理论作为参考,但大家都有秉承着一个“简单”的共同原则,其实不知不觉中摸着石头过河,在无数次重构中逐渐跟以上模式契合起来。当然,如果我们能提前认识这些基础的理论基础知识,我想不必要的弯路会少走许多。也当然,系统还在不断完善中,现在认识也不晚。
第11章:应用分析模式
在《分析模式》一书中,Martin Fowler这样定义分析模式:
分析模式是一种概念集合,用来表示业务建模中的常见结构。它可能只与一个领域有关,也可能跨多个领域。
Fowler所提出的分析模式来自于实践经验,因此只要用在合适的情形下,它们会非常实用。对于那些面对着具有挑战性领域的人们,这些模型为他们的迭代开发过程提供了一个非常有价值的起点。“分析模式”这个名字本身就强调了其概念本质。分析模式不是技术方案,他们只是参考,用来指导人们设计特定领域中的模型。
分析模式最大的作用是借鉴其他项目的经验,把那些项目中有关设计方向和实现结构的广泛讨论与当前模型的理解结合起来。脱离具体的上下文来讨论模型思想不但难以落地,而且还会造成分析与设计严重脱节的风险,而这一点正是MODEL-DRIVEN DESIGN坚决反对的。
当你可以幸运地使用一种分析模式时,它一般并不会直接满足你的需求。但它为你的研究提供了有价值的线索,而且提供了明确抽象的词汇。它还可以知道我们的实现,从而省去很多麻烦。
我们应该把所有分析模式的知识融入知识消化和重构的过程中,从而形成更深刻的理解,并促进开发。当我们应用一种分析模式时,所得到的结果通常与该模式的文献中记载的形式非常想像,只是因具体情况不同而略有差异。但有时完全看不出这个结果与分析模式本身有关,然而这个结果仍然是受该模式思想的启发而得到的。
但有一个误区是应该避免的。当使用众所周知的分析模式中的术语时,一定要注意,不管其表面形式的变化有多大,都不要改变它所表示的基本概念。这样做有两个原因,一是模式中蕴含的基本概念将帮助我们避免问题,二是(也是更重要的原因)使用被广泛理解或至少是被明确理解的术语可以增强UBIQUITOUS LANGUAGE。如果在模型的自然演变过程中模型的定义也发生改变,那么就要修改模型名称了。
【学习心得】:本章节主要还是借助《分析模式》一书中的例子,用实践例子来分析系统是如何在演绎过程使用模型的。这种科学谨慎的做法,才是一个工程师的基本观念要求。
第12章:将设计模式应用于模型
在《设计模式》中,有些(但并非所有)模式可用作领域模式,但在这样使用的时候,需要变换一下重点。有些模式反映了一些在领域中出现的深层概念。这些模式都有很大的利用价值。为了在领域驱动设计中充分利用这些模式,我们必须同时从两个角度看待它们:从代码的角度来看它们是技术设计模式,从模型的角度来看它们就是概念模式。
模式:STRATEGY(也称POLICY)
策略模式:定义了一组算法,将每个算法封装起来,并使它们可以互换。STRATEGY允许算法独立于使用它的客户而变化[Gamma et al. 1995]
领域模型包含一些并非用于解决技术问题的过程,将它们包含进来是因为它们处理问题领域具有实际的价值。当必须从多个过程中进行选择时,选择的复杂性再加上多个过程本身的复杂性使局面失去控制。
因此:我们需要把过程中的易变部分提取到模型的一个单独的“策略”对象中。将规则与它所控制的行为区分开。按照STRATEGY设计模式来实现规则或可替换的过程。策略对象的多个版本表示了完成过程的不同方式。
模式:COMPOSITE
组合模式:将对象组织为树来表示部分—整体的层次结构。利用COMPOSITE,客户可以对单独的对象和对象组合进行同样的处理。[Gamma et al.1995]
当嵌套容器的关联性没有在模型中反映出来时,公共行为必然会在层次结构的每一层重复出现,而且嵌套也变得僵化(例如,容器通常不能包含同一层中的其他容器,而且嵌套的层数也是固定的)。客户必须通过不同的接口来处理层次结构中的不同层,尽管这些层在概念上可能没有区别。通过层次结构来递归地收集信息也变得非常复杂。
因此:定义一个把COMPOSITE的所有成员都包含在内的抽象类型。在容器上实现那些查询信息的方法时,这些方法返回由容器内容所汇总的信息。而“叶”节点则基于它们自己的值来实现这些方法。客户只需使用抽象类型,而无需区分“叶”和容器。
【学习心得】:很多时候,技术人员钉子思维是无法区分技术角度和模型角度。虽然许多方法是相通的,但不通维度的思考方式也会产生巨大的效果。学以致用,不是停留在嘴巴上,是在实践中证明的。
第13章:通过重构得到更深层次的理解
通过重构得到更深层次的理解是一个涉及很多方面的过程。我们有必要暂停一下,把一些要点归纳到一起。有三件事情是必须要关注的:
(1)以领域为本;
(2)用一种不同的方式来看待事物;
(3)始终坚持与领域专家对话。
在寻求理解领域的过程中,可以发现更广泛的重构机会。但一提到传统意义上的重构,我们头脑中就会出现这样一幅场景:一两位开发人员坐在键盘前面,发现一些代码可以改进,然后立刻动手修改代码(当然还要用单元测试来验证结果)。这个过程应该是一直进行下去,但它并不是重构过程的全部。
1、开始重构
与传统重构观点不同的是,即使在代码看上去很整洁的时候也可能需要重构,原因是模型的语言没有与领域专家保持一致,或者新需求不能被自然地添加到模型中。重构的原因也可能来自学习:当开发人员通过学习获得了更深刻的理解,从而发现了一个得到更清晰或更有用的模型的机会。
2、探索团队
不管问题的根源是什么,下一步都是要找到一种能够使模型表达变得更清楚和更自然的改进方案。这可能只需要做一些简单、明显的修改,只需几小时即可完成。在这种情况下,所做的修改类似于传统重构。但寻找新模型可能需要更多时间,而且需要更多人参与。
修改的发起者会挑选几位开发人员一起工作,这些开发人员应该擅长思考该类问题,了解领域,或者掌握深厚的建模技巧。如果涉及一些难以捉摸的问题,他们还要请一位领域专家加入。想要保证重构迭代过程的效率,需要注意几个关键事项:自主决定,注意范围和休息,以及练习使用UBIQUITOUS LANGUAGE。
3、借鉴先前的经验
我们没有必要总去做一些无谓的重复工作。用于查找缺失概念或改进模型的头脑风暴过程具有巨大的作用,通过这个过程可以收集来自各个方面的想法,并把这些想法与已有知识结合起来。随着知识消化的不断开展,就能找到当前问题的答案。
4、针对开发人员的设计
软件不仅仅是为用户提供的,也是为开发人员提供的。开发人员必须把他们编写的代码与系统的其他部分集成到一起。在迭代过程中,开发人员反复修改代码。开发人员应该通过重构得到更深层的理解,这样既能够实现柔性设计,也能够从这样一个设计中获益。
5、重构的时机
如果一直等到完全证明了修改的合理性之后才去修改,那么可能要等待太长时间了。项目正承受巨大的耗支,推迟修改将使修改变得更难执行,因为要修改的代码已经变得更加复杂,并更深地嵌入到其他代码中。持续重构渐渐被认为是一种“最佳实践”,但大不部分团队仍然对它抱有很大的戒心。
在探索领域的过程中,在培训开发人员的过程中,以及在开发人员与领域专家进行思想交流的过程中,必须始终坚持把“通过重构得到更深层次理解”作为这些工作的一部分。因此,当发生一下情况时,就应该进行重构了:
□ 设计没有表达出团队对领域的最新理解;
□ 重要的概念被隐藏在设计中了(而且你已经发现了把它们呈现出来的方法);
□ 发现了一个能令某个重要的设计部分变得更灵活的机会。
6、危机就是机遇
传统意义上的重构听起来是一个非常稳定的过程。但通过重构得到更深层理解往往不是这样的。在对模型进行一段时间稳定的改进后,你可能突然有所顿悟,而这会改变模型中的一切。这些突破不会每天都发生,然而很大一部分深层模型和柔性设计都来自这些突破。
这样的情况往往看起来不像是机遇,而更像危机。例如,你突然发现模型中一些明显的缺陷,在表达方面显示出一个很大的漏洞,或存在一些没有表达清楚的关键区域。或者有些描述是完全错误的。这些都表明团队对模型的理解已经达到了一个新的水平。他们现在站在更高的层次上发现了原有模型的弱点。他们可以从这种角度构思一个更好的模型。
【学习心得】:我曾几何时一直认为,发现自己问题是一种耻辱。这种思维极其可怕,当我不再发现自己问题的时候,那才叫可怕。在软件领域,新思维的提升叫重构,在生活方面,新观念的形成叫重生。
【第四部分】战略设计
第14章:保持模型的完整性
模型最基本的要求是它应该保持内部一致,术语总具有相同的意义,并且不包含相互矛盾的规则:虽然我们很少明确地考虑这些要求。模型的内部一致性又叫统一(unification),这种情况下,每个术语都不会有模棱两可的意义,也不会有规则冲突。除非模型在逻辑上是一致的,否则它就没有意义。在理想世界中,我们可以得到涵盖整个企业领域的单一模型。这个模型将是统一的,没有任何相互矛盾或相互重叠的术语定义。每个有关领域的逻辑声明都是一致的。
但,大型系统开发并非如此理想。在整个企业系统中保持这种水平的统一是一件得不偿失的事情。在系统的各个不同部分中开发多个模型是很有必要的,但我们必须慎重地选择系统的哪些部分可以分开,以及它们之间是什么关系。我们需要用一些方法保持模型关键部分的高度统一。所有这些都不会自行发生,而且光有良好的意愿也没用的。它只有通过有意识的设计决策和建立特定过程才能实现。大型系统领域模型的完全统一既不可行,也不划算。
模式:BOUNDED CONTEXT(边界上下文)
细胞之所以能够存在,是因为细胞膜限定了什么在细胞内,什么在细胞外,并且确定了什么物质可以通过细胞膜。
任何大型项目都会存在多个模型。而当基于不同模型的代码被组合到一起后,软件就会出现bug,变得不可靠和难以理解。团队成员之间的沟通变得混乱。人们往往弄不清楚一个模型不应该在哪个上下文中使用。
因此:明确地定义模型所应用的上下文。根据团队的组织,软件系统的各个部分的用法以及物理表现(代码和数据库模式等)来设置模型的边界。在这些边界中严格保持模型的一致性,而不要收到边界之外问题的干扰和混淆。
但记住,BUOUNDED CONTEXT不是MODULE。有时这两个概念易引起混淆,但它们是具有不同动机的不同模式。确实,当两组对象组成两个不同模型时,人们几乎总是把它们放在不同的MODULE中。这样做的确提供了不同的命名空间(对不同的CONTEXT很重要)和一些划分方法。但人们也会在一个模型中用MODULE来组织元素,它们不一定要表达划分CONTEXT的意图。MODULE在BOUNDED CONTEXT内部创建的独立命名空间实际上使人们很难发现意外产生的模型分裂。
我们通过定义这个BOUNDED CONTEXT,最终得到了什么?对CONTEXT内的团队而言:清晰!对于CONTEXT之外的团队而言:自由。当然,边界只不过是一些特殊的位置。各个BUONDED CONTEXT之间的关系需要我们仔细地处理。CONTEXTMAP(上下文图)画出了上下文范围,并给出了CONTEXT以及它们之间联系的总体视图,而几种模式定义了CONTEXT之间的各种关系的性质。CONTINUOUS INTEGRATION(持续集成)的过程可以使模型在BOUNDED CONTEXT中保持一致。
如何识别BOUNDED CONTEXT中的不一致?很多征兆都可能表明模型出现了差异。最明显的是已编码的接口不匹配。对于更微妙的情况,一些意外行为也可能是一种信号。采用了自动测试的CONTINUOUS INTEGRATION可以帮助捕捉到这类问题,但语言上的混乱往往是一种早期信号。
将不同模型的元素组合到一起可能会引发两类问题:重复的概念和假同源。重复的概念是指两个模型元素(以及伴随的实现)实际上表示同一个概念,而假同源是指使用相同术语(或已实现的对象)的两个人认为他们是在谈论同一件事情,但实际上并不是这样。假同源可能稍微少见一点,但它潜在的危害更大。
模式:CONTINUOUS INTEGRATION(持续集成)
当很多人在同一个BOUNDED CONTEXT中工作时,模型很容易发生分裂。团队越大,问题就越大,但即使3、4个人的团队也有可能会遇到严重的问题。然而,如果将系统分解为更小的CONTEXT,最终又难以保持集成度和一致性。
因此:建立一个把所有代码和其他实现工件频繁地合并到一起的过程,并通过自动化测试来快速查明模型的分类问题。严格坚持使用UBIQUITOUS LANGUAGE,以便在不同人的头脑中演变出不同的概念时,是所有人对模型都能达成一个共识。
模式:CONTEXT MAP(上下文整体关联图)
其他团队中的人员并不是十分清楚CONTEXT的边界,他们会不知不觉地做出一些更改,从而使边界变得模糊或者使互连变得复杂。当不同的上下文必须互相连接时,它们可能会互相重叠。
因此:识别在项目中起作用的每个模型,并定义其BOUNDED CONTEXT。这包括非面向对象子系统的隐含模型。为每个BOUNDED CONTEXT命名,并把名称添加到UBIQUITOUS LANGUAGE中。描述模型之间的联系点,明确所有通信需要的转换,并突出任何共享的内容。先将当前的情况描绘出来,以后再做改变。
CONTEXT MAP无需拘泥于任何特定的文档格式。我发现类似本章的简图在可视化和沟通上下文图方面很有帮助。有些人可能喜欢使用较多的文本描述或别的图形表示。在某些情况下,团队成员之间的讨论就足够了。需求不同,细节层次也不同。不管CONTEXT MAP采用什么形式,它必须在所有项目人员之间共享,并被他们理解。它必须为每个BOUNDED CONTEXT提供一个明确的名称,而且必须阐明联系点和它们的本质。
下面介绍的这些模式涵盖了将两个模型关联起来的众多策略。这些模式的主要区别包括你对另一个模型的控制程度、两个团队之前合作水平和合作类型,以及特性和数据的集成程度。
模式:SHARED KERNEL(共享内核)
当不同团队开发一些紧密相关的应用程序时,如果团队之间不进行协调,即使短时间内能够获得快速进展,但他们开发出的产品可能无法结合到一起。租后可能不得不耗费大量精力在转换层上,并且频繁地进行改动,不如一开始就用CONTINUOUS INTEGRATION那么省心省力,同时这也造成重复工作,并且无法实现公共的UBIQUITOUS LANGUAGE所带来的好处。
因此:从模型中选出两个团队都同意共享的一个子集。当然,除了这个模型子集以外,还包括与该模型部分相关的代码子集,或数据库设计的子集。这部分明确共享的内容具有特殊的地位,一个团队在没与另一个团队商量的情况下不应擅自更改它。功能系统要经常进行集成,但集成的频率应该比团队中CONTINUOUS INTEGRATION的频率低一些。在进行这些集成的时候,两个团队都要运行测试。
模式:CUSTOMER/SUPPLIER DEVELOPMENT TEAM(客户/供应商 开发团队)
我们经常会碰到这样的情况:一个子系统主要服务于另外一个子系统;“下游”组件执行分析或其他功能,这些功能向“上游”组件反馈的信息非常少,所有依赖都是单向的。两个子系统通常服务于完全不同的用户群,其执行的任务也不同,在这种情况下使用不同的模型会很有帮助。
如果下游团队对变更具有否决权,或请求变更的程序太复杂,那么上游团队的开发自由度就会受到限制。由于担心破坏下游系统,上游团队甚至会受到抑制。同时,由于上游团队掌握优先权。下游团队有时也会无能为力。
因此:在两个团队之间建立一种明确的客户/供应商关系。在计划会议中,下游团队相当于上游团队的客户。根据下游团队的需求来协商需要执行的任务并为这些任务做预算,以便每个人都知道双方的约定和进度。两个团队共同开发自动化验收测试,用来验证预期的接口。把这些测试添加到上游团队的测试套件中,以便作为其持续集成的一部分来运行。这些测试使上游团队在做出修改时不必担心对下游团队产生副作用。
模式:CONFORMIST(承诺)
当两个具有上游/下游关系的团队不归同一个管理者指挥时,CUSTOMER/SUPPLIER TEAM这样的合作模式就不会凑效。勉强应用这种模式会给下游团队带来麻烦。
当两个开发团队具有上/下游关系时,如果上游团队没有动力来满足下游团队的需求,那么下游团队将无能为力。出于利他主义的考虑,上游开发人员可能会做出承诺,但他们可能不会履行承诺。下游团队出于良好的意愿会相信这些承诺,从而根据一些永远不会实现的特性来制定计划。下游项目只能被搁置,直到团队最终学会利用现有条件自力更生为止。下游团队不会得到根据他们的需求而量身定做的接口。
因此:通过严格遵从上游团队的模型,可以消除在BOUNDED CONTEXT之间进行转换的复杂性。尽管这会限制下游设计人员的风格,而且可能不会得到理想的应用程序模型,但选择CONFORMITY模式可以极大地简化集成。此外,这样还可以与供应商团队共享UBIQUITOUS LANGUAGE。供应商处于统治地位,因此最好使沟通变容易。他们从利他主义的角度出发,会与你分享信息。
SHARED KERNEL是两个高度协调的团队之间的合作模式,而CONFORMIST模式则是应对一个对合作不感兴趣的团队进行集成。
模式:ANTICORRUPTION LAYER(隔离层)
新系统几乎总是需要与遗留系统或其他系统进行集成,这些系统具有自己的模型。当把参与集成的BOUNDED CONTEXT设计完善并且团队相互合作时,转换层可能很简单,甚至很优雅。但是,当边界那侧发生渗透时,转换层就要承担起更多的防护职责。
当正在构建的新系统与另一个系统的接口很大时,为了克服连接两个模型而带来的困难,新模型所表达的意图可能会被完全改变,最终导致它被修改得像另一个系统的模型了(以一种特定风格)。遗留系统的模型通常很弱。即使对于那些模型开发得很好的例外情况,它们可能也不符合当前项目的需要。然而,集成遗留系统仍然具有很大的价值,而且有时还是绝对必要的。
因此:创建一个隔离层,以便根据客户自己的领域模型来为客户提供相关功能。这个层通过另一个系统现有接口与其进行对话,而只需对那个系统作出很少的修改,甚至无需修改。在内部,这个层在两个模型之间进行必要的双向转换。
这种连接两个系统的机制可能会使我们想到把数据从一个程序传输到另一个程序,或者从一个服务器传输到另一个服务器。我们很快就会讨论技术通信机制的使用。但这些细节问题不应与ANTICORRUPTION LAYER混淆,因为ANTICORRUPTION LAYER并不是向另一个系统发送消息的机制。想反,它是不同的模型和协议之间转换概念对象和操作的机制。
如何设计ANTICORRUPTION LAYER的接口?
ANTICORRUPTION LAYER的公共接口通常以一组SERVICE形式出现,但偶尔也会采用ENTITY的形式。在我们的模型中,把外部系统表示为一个单独组件可能是没有意义的。最好是使用多个SERVICE(或偶尔使用ENTITY),其中每个SERVICE都使用我们的模型来履行一致的职责。
如何实现ANTICORRUPTION LAYER?
对ANTICORRUPTION LAYER设计进行组织的一种方法是把它实现为FACEDE、ADAPTER和转换器的组合,外加两个系统之间进行对话所需的通信和传输机制。
模式:SEPARATE WAY(各行其道)
我们必须严格划定需求的范围。如果两组功能之间的关系并非必不可少,那么二者完全可以彼此独立。因为集成总是代价高昂,而有时获益却很小。
因此:声明一个与其他上下文毫无关联的BOUNDED CONTEXT,使开发人员能够在这个小范围内找到简单、专用的解决方案。
采用SEPARATE WAY(各行其道)模式需要预先决定一些选项。尽管持续重构最后可以撤销任何决策,但完全隔离开发的模型是很难合并的。如果最终仍需要集成,那么转换层将是必要的,而且可能很复杂。当然,不管怎样,这都是我们将要面对的问题。现在,让我们回到更为合作的关系上,来看一下几种提高集成度的模式。
模式:OPEN HOST SERVICE(开放主机服务)
当一个子系统必须与大量其他系统进行集成时,为每个集成都定制一个转换层可能会减慢团队的工作速度。需要维护的东西会越来越多。而且进行修改的时候担心的事情也会越来越多。
因此:定义一个协议,把你的子系统作为一组SERVICE供其他系统访问。开放这个协议,以便所有需要与你的子系统集成的人都可以使用它。当有新的集成需求时,就增强并扩展这个协议,但个别团队的特殊需求除外。满足这种特殊需求的方法是使用一次性的转换器来扩充协议,以便使共享协议简单且内聚。
这种通信形式暗含一些共享的模型词汇,它们是SERVICE接口的基础。这样,其他子系统就变成了与OPEN HOST(开放主机)的模型相连接,而其他团队则必须学习HOST团队所使用的专用术语。在一些情况下,使用一个众所周知的PUBLISHED LANGUAGE(公开发布的语言)作为交换模型可以减少耦合并简化理解。
模式:PUBLISHED LANGUAGE(公开发布的语言)
与现有领域模型进行直接的转换可能不是一种好的解决方案。这些模型可能过于复杂或设计得较差。它们可能没有被很好地文档化。如果把其中一个模型作为数据交互语言,它实际上就被固定住了,而无法满足新的开发需求。
因此:把一个良好文档化的、能够表达出所需领域信息的共享语言作为公共的通信媒介,必要时在其他信息与该语言之间进行转换。
“大象”的统一
讲一个盲人摸象的故事:
第一个盲人碰巧摸到了大象宽阔结实的身躯,就以为大象就像一堵墙;
······
第三个盲人碰巧把扭动着的象鼻抓在书中,就大胆认为大象就像一条蛇;
第四个盲人急切伸出双手,摸到了大象的膝盖,就很明显地认为大象就像一颗树;
······
第六个盲人一开始摸这头大象,就抓住了它摆动着的尾巴,就认为大象就像一根绳子。
即便他们对大象的本质不能达成完成的一致,这些盲人仍然可以根据他们所触摸到的大象身体的部位来扩展各自的认识。如果并不需要集成,那么模型统不统一就无关紧要。如果他们需要进行一些集成,那么实际上并不需要对大象是什么达成一致,而只要接受各种不同意见就会获得很多价值。这样,他们就会在不知不觉中各执己见。
当盲人想要分享更多大象的信息时,他们会共享单个BOUNDED CONTEXT得到更大的价值。但统一多个模型几乎总是意味着创建一个新模型:
经过一些想象和讨论(也许是激烈的讨论)之后,盲人们最终可能会认识到他们正在对一个更大整体的不同部分进行描述和建模。从很多方面来讲,部分-整体的统一可能不需要花费很多工作。至少集成的第一步只需弄清楚各个部分是如何相连的就够了。
尽管我们已经把部分合并成一个整体,但得到的模型还是很简陋的。他缺乏内聚性,也没有形成任何潜在的领域的轮廓。在持续精华的过程中,新的理解可能会产生更深层的模型。
承认多个相互冲突的领域模型实际上正式面对现实的做法。通过明确定义每个模型都适用的上下文,可以维护每个模型的完整性,并清楚地看到要在两个模型之间创建的任何特殊接口的含义。盲人没办法看到整个大象,但只要他们承认各自的理解是不完整的,他们的问题就能得到解决。
选择你的模型上下文策略
在任何时候,绘制出CONTEXT MAP来反映当前状况都是很重要的。但是,一旦绘制好CONTEXT MAP之后,你很可能想要改变现状。现状,你可以开始有意识地选择CONTEXT的边界和关系。以下是一些指导原则:
1、团队决策或更高层决策
按照本身价值来说,在决定是否扩展或分割BOUNDED CONTEXT时,应该权衡团队独立工作的价值以及能产生直接且丰富集成的价值,以这两种价值的成本-效益作为决策的依据。
在实践中,团队之间的行政关系往往决定了系统的集成方式。由于汇报结构,有技术优势的统一可能无法实现。管理层所要求的合并可能并不实用。你不会总能得到你想要的东西,大你至少可以评估出这些决策的代价,并反映给管理层,以便采取相应的措施来减少代价。
2、置身上下文中
开发软件项目时,我们首先是对自己团队正在开发的那些部分感兴趣(“设计中的系统”),其次是对那些与我们交互的系统感兴趣。这是一种简单、典型的情况,能让你对可能遇到的情形有一些粗略的了解。
实际上,我们正式自己所处理的主要CONTEXT的一部分,这会在我们的CONTEXT MAP中反映出来。只要我们知道自己存在偏好,并且超过该CONTEXT MAP的应用边界时能够意识到已越界,那么就不会有什么问题。
3、转换边界
在画出BOUNDED CONTEXT的边界时,有无数种情况,也有无数种选择。但权衡时所要考虑的通常是下面所列出的某些因素。
首选较大的BOUNDED CONTEXT:
□ 当用一个统一模型来处理更多任务时,用户任务之间的流动更顺畅。
□ 一个内聚模型比两个不同模型再加它们之间的映射更容易理解。
□ 两个模型之间的转换可能会很难(有时甚至是不可能的)。
□ 共享语言可以使团队沟通起来更清楚。
首选较小的BOUNDED CONTEXT:
□ 开发人员之间的沟通开销减少了。
□ 由于团队和代码规模较小,CONTINOUS INTEGRATION更容易了。
□ 较大的上下文要求更加通用的抽象模型,而掌握所需技巧的人员会出现短缺。
□ 不同模型可以满足一些特殊需求,或者是能够把一些特殊用户群的专门术语和UBIQUITOUS LANGUAGE的专门术语包括进来。
4、接受那些我们无法更改的事物:描述外部系统
最好从一些简单的决策开始。一些子系统显然不在开发中的系统的任何BOUNDED CONTEXT中。一些无法立即淘汰的大型遗留系统和那些提供所需服务的外部系统就是这样的例子。我们很容易就能识别出这些系统,并把它们与你的设计隔离开。
在做出假设时必须要保持谨慎。我们会轻易地认为这些系统构成了其自己的BOUNDED CONTEXT,但大多数外部系统只是勉强满足定义。
5、与外部系统的关系
这里可以应用3种模式。首先,可以考虑SEPARATE WAY模式。当然,如果你不需要集成,就不用把它们包括进来。但一定要真正确定不需要集成。只为用户提供对两个系统的简单访问确实够用吗?集成要花费很大的代价而且还会分散精力,因此要尽可能为你的项目减轻负担。
如果集成确实非常重要,可以在两种极端的模式之中选择:CONFORMIST模式或ANTICORRUPTION LAYER模式。
6、设计中的系统
你的项目团队正在构建的软件就是设计中的系统。你可以在这个区域内声明BOUNDED CONTEXT,并在每个BOUNDED CONTEXT中应用CONTINOUS INTEGRATION,以便保持它们的统一。但应该有几个上下文呢?各个上下文之间又应该是什么关系呢?
情况可能非常简单:设计中的整个系统使用一个BOUNDED CONTEXT。例如,当一个少于10个人的团队正在开发高度相关的功能时,这可能就是一个很好的选择。
随着团队规模的增大,CONTINOUS INTEGRATION可能会变得困难起来(尽管我也曾看过一些较大的团队仍能保持持续集成)。你可能希望采用SHARED KERNEL模式,并把几组相对独立的功能划分到不同的BOUNDED CONTEXT中,使得在每个BOUNDED CONTEXT中工作的人员少于10人。在这些BOUNDED CONTEXT中,如果有两个上下文之间的所有依赖都是单向的,就可以建成CUSTOMER/SUPPLIER DEVELOPMENT TEAM。
你可能认识到两个团队的思想截然不同,以致他们的建模工作总是发生矛盾。如果这种矛盾的原因是你无法改变或不想改变的,那么可以让他们的模型采用SEPARATE WAY模式。在需要集成的地方,两个团队可以共同开发维护一个转换层,把它作为唯一的CONTINOUS INTEGRATION点。这与同外部系统的集成正好相反,在外部集成中,一般由ANTICORRUPTION LAYER来起调节作用,而且从另一端得不到太多的支持。
一般来说,每个BOUNDED CONTEXT对应一个团队。一个团队也可以维护多个BOUNDED CONTEXT,但多个团队在一个上下文中工作却是比较难的。
7、用不同模型满足特殊需求
你可能决定通过不同的BOUNDED CONTEXT来满足这些特殊需求,除了转换层的CONTINOUS INTEGTATION以外,让模型采用SEPARATE WAY模式。UBIQUITOUS LANGUAGE的不同专用术语将围绕这些模型以及它们所基于的行话来发展。如果两种专用术语有很多重叠之处,那么SHARED KERNEL模式就可以满足特殊化要求,同时又能把转换成本减至最小。
最重要的是:这个用户群的专门术语有多大的价值?你必须在团队独立操作的价值与转换的风险之间做出权衡,并且留心合理地处理一些没有价值的术语变化。但记住,在需要大量集成的地方,转换成本会大大增加。
8、部署
在复杂系统中,对打包和部署进行协调是一项繁琐的任务,这类任务总是要比看上去难得多。BOUNDED CONTEXT策略的选择将影响部署。由于部署环境和技术存在不同,有很多技术因素需要考虑。但BOUNDED CONTEXT关系可以为我们指出重点问题。转换接口已经被标出。所以,绘制CONTEXT边界时应该反映出部署计划的可行性。
9、权衡
通过总结这些知道原则可知有很多统一或集成模型的策略。一般来说,我们需要在无缝功能集成的益处和额外的协调和沟通工作之间做出权衡。
10、当项目正在进行时
很多情况下,我们不是从头开发一个项目,而是会改进一个正在开发的项目。在这种情况下,第一步是根据当前的状况来定义BOUNDED CONTEXT。这很关键。为了有效地定义上下文,CONTEXT MAP必须反映出团队的实际工作,而不是反映那个通过遵守以上描述的指导原则而得出的理想组织。
描述了当前真实的BOUNDED CONTEXT以及它们的关系以后,下一步就是围绕当前组织结构来加强团队的工作。在CONTEXT中加强CONTINOUS INTEGRATION。把所有分散的转换代码重构到ANTICORRUPTION LAYER中。命名现有的BOUNDED CONTEXT,并确保它们处于项目的UBIQUITOUS LANGUAGE中。
下一节将讨论如何修改CONTEXT边界:转换。
转换
像建模和设计的其他方面,有关BOUNDED CONTEXT的决策并非不可改变的。在很多情况下,我们必须改变最初有关边界以及BOUNDED CONTEXT之间关系的决策,这是不可避免的。一般而言,分割CONTEXT是很容易,但合并它们或改变它们之间的关系却很难。下面将介绍几种有代表性的修改,它们很难,但也很重要。
1、合并CONTEXT:SEPARETE WAY→SHARED KERNEL
合并BOUNDED CONTEXT的动机很多:翻译开销够高、重复现象很明显。合并很难,但什么时候做都不晚,只是需要一些耐心。
即使你的最终目标是完全合并一个采用CONTINUOS INTEGRATION的CONTEXT,也应该先过渡到SHARED KERNEL。
(1)评估现状。在开始统一两个CONTEXT之前,一定要确信它们确实需要统一。
(2)建立合并过程。你需要决定代码的共享方式以及模块应该采用哪种命名约定。SHARED KERNEL的代码至少每周要集成一次,而且它必须有一个测试套件。在开发任何共享代码之前,先把它设置好。(测试套件将是空的,因此很容易通过!)
(3)选择某个小的子领域作为开始,它应该是两个CONTEXT中重复出现的子领域,但不是CORE DOMAIN的一部分。
(4)从两个团队中共选出2~4位开发人员组成一个小组,有他们来为子领域开发一个共享的模型。
(5)来自两个团队的开发成员一起负责实现模型(或修改要共享的现有代码)、确定各种细节并使模型开始工作。如果这些开发人员在模型中遇到了问题,就从第(3)步开始重新组织团队,并进行必要的概念修订工作。
(6)每个团队的开发人员都承担与新的SHARED KERNEL集成的任务。
(7)清除那些不再需要的翻译。
2、合并CONTEXT:SHARED KERNEL→CONTINOUS INTEGRATION
如果你的KERNEL正在扩大,你可能会被完全统一两个BOUNDED CONTEXT的优点所吸引。但这并不只是一个解决模型差异的问题。你将改变团队的结构,而且最终会改变人们所使用的语言。这个过程从人员和团队开始准备。
(1)确保每个团队都已经建立了CONTINOUS INTEGRATION所需的所有过程(共享代码所有权、频繁集成等)。两个团队协商集成步骤,以便所有人都以同一步调工作。
(2)团队成员在团队之间流动。这样可以形成一大批同时理解两个模型的人员,并且可以把两个团队的人员联系起来。
(3)澄清每个模型的精髓。
(4)现在,团队应该有了足够的信心把核心领域合并到SHARED KERNEL中。
(5)随着SHARED KERNEL的增长,把集成频率提高到每天一次,最后实现CONTINOUS INTEGRATION。
(6)当SHARED KERNEL逐渐把先前两个BOUNDED CONTEXT的所有内容都包括进来的时候,你会发现要么形成了一个大的团队,要么形成了两个较小的团队,这两个较小的团队共享一个CONTINOUS INTEGRATION的代码库,而且团队成员可以经常在两个团队之间来回流动。
3、逐步淘汰遗留系统
好花美丽不常开,好景怡人不常在,就算遗留计算机软件也一样会走向终结。但这可不会自动自发地出现。这些老的系统可能与业务及其他系统紧密交织在一起,因此淘汰它们可能需要很多年。好在我们并不需要一次就把所有东西都淘汰掉。
首先要执行的步骤是确定测试策略。应该为新系统中的新功能编写自动的单元测试,但逐步淘汰遗留系统还有一些特殊的测试要求。一些组织会在某段时间内同时运行新旧两个系统。在任何一次迭代中:
(1)确定遗留系统的哪个功能可以在一个迭代中被添加到某个新系统中;
(2)确定需要在ANTICORRUPTION LAYER中添加功能;
(3)实现;
(4)部署;
(5)找出ANTICORRUPTION LAYER中那些不必要的部分,并去掉它们;
(6)考虑删除遗留系统中目前未被使用的模块,虽然这种做法未必实际。
不断重复这几个步骤。遗留系统应该越来越少地参与业务,最终,替换工作会看到希望的曙光并完全停止遗留系统。
4、OPEN HOST SERVICE→PUBLISHED LANGUAGE
我们已经通过一系列特地的协议与其他系统进行了集成,但随着需要访问的系统逐渐增多,维护负担也不断增加,或者交互变得很难理解。我们需要通过PUBLISHED LANGUAGE来规范系统之间的关系。
(1)如果有一种行业标准语言可用,则尽可能评估并使用它。
(2)如果没有标准语言或预先公开发布的语言,则完善作为HOST的系统的CORE DOMAIN。
(3)使用CORE DOMAIN作为交换语言的基础,尽可能使用像XML这样的标准交互范式。
(4)(至少)向所有参与协作的各方发布语言。
(5)如果涉及新的系统架构,那么也要发布它。
(6)为每个协作系统构建转换层。
(7)切换。
现在,当加入更多协作系统时,对整个系统的破坏已经减至最小了。
【学习心得】:学以致用,具体问题具体分析。模式毕竟是巨人的肩膀,要学会站着巨人肩膀看事情,无论项目多大还是多下,又或者团队多大还是多小,总有属于当前自己的模式。结合自身情况,找准定位。我们所做的大部分事情几乎都有方法或模式借鉴,千万不要埋头单干。就像耗子叔所说,学会Evidence Driven:任何讨论和分析都要基于权威的证据、数据或是引用。在我们做设计的时候,或是有争论的时候,说服对方最好的方式就是拿出证据、数据或是权威引用。比如:我的XX设计参考了TCP协议中的XX设计,我的XX观点是基于XX开源软件的实现……如果争论不休就停止争论,然后各自收集和调查自己观点的佐证。
第15章:精炼
如何才能专注于核心问题而不被大量的次要问题淹没呢?LAYERED ARCHITECTURE可以把领域概念从技术逻辑中(技术逻辑确保了计算机系统能够运转)分离出来,但在大型系统中,即使领域被分离出来,它的复杂性也可能仍然难以管理。
精炼是把一堆混杂在一起的组件分开的过程,以便通过某种形式从中提取出最重要的内容,而这种形式将使它更有价值,也更有用。模型就是知识的精炼。通过每次重构所得到的更深层的理解,我们得以把关键的领域知识和优先级提取出来。
本章将展示对CORE DOMAIN进行战略精炼的系统性方法,解释如何在团队中有效地统一认识,并提供一种用于讨论工作的语言。
模式:CORE DOMAIN(核心领域)
在设计大型系统时,有非常多的组成部分——它们都很复杂而且对开发的功能也至关重要,到导致真正的业务资产——领域模型最为精华的部分——被掩盖和忽略了。
一个严峻的现实是我们不可能对所有设计部分进行同等的精化,而是必须分出优先级。为了使领域模型成为有价值的资产,必须整齐地梳理出模型的真正核心,并完全根据这个核心来创建应用程序的功能。但本来就稀缺的高水平开发人员往往会把工作重点放在技术基础设施上,或者只是去解决那些不需要专门领域知识就能理解的领域问题(这些问题都已经有了很好的定义)。
因此:对模型进行提炼。找到CORE DOMAIN并提供一种易于区分的方法把它与那些去辅助作用的模型和代码分开。最有价值和最专业的概念要轮廓分明。尽量压缩CORE DOMAIN。让最有才能的人来开发CORE DOMAIN,并据此要求进行相应的招聘。在CORE DOMAIN中努力开发能够确保现实系统蓝图的深层模型和柔性设计。仔细判断任何其他部分的投入,看它是否能够支持这个提炼出来的CORE。
1、选择核心
我们需要关注的是那些能够表示业务领域并解决业务问题的模型部分。一个应用程序中的CORE DOMAIN在另一个应用程序中可能只是通用的支持组件。尽管如此,仍然可以在一个项目中(而且通常在一个公司中)定义一个一致的CORE。像其他设计部分一样,人们对CORE DOMAIN的认识也会随着迭代而发展。开始时,一些特殊关系可能显得不重要。而最初被认为是核心对象可能逐渐被证明只是起支持作用。
2、工作的分配
在项目团队中,技术能力最强的人员往往缺乏丰富的领域知识。这限制了他们的作用,并且更倾向于分派他们来开发一些支持组件,从而形成了一个恶性循环——知识的缺乏使他们远离了那些能够学到领域知识的工作。
打破这种恶心循环是很重要的,方法是建立一支由开发人员和一位或多位领域专家组成的联合团队,其中开发人员必须能力很强、能够长期稳定地工作并且学习领域知识非常感兴趣,而领域专家则要掌握深厚的业务知识。如果你认真对待领域设计,那么它就是一项有趣且充满技术挑战的工作。
3、精炼的逐步提升
本章接下来将要介绍各种精炼技术,它们在使用顺序上基本没什么要求,但对设计的改动却大不相同。请往下看:
模式:GENERIC SUBDOMIAN(通用子领域)
模型中有些部分除了增加复杂性以外并没有捕捉或传递任何专门的知识。任何外来因素都会是CORE DOMAIN愈发的难以分辨和理解。模型中充斥着大量众所周知的一般原则,或者专门的细节,这些细节并不是我们的主要关注点,而只是起到支持作用。然而,无论它们是多么通用的元素,它们对实现系统功能和充分表达模型都是极为重要的。
因此:识别出那些与项目意图无关的内聚子领域。把这些子领域的通用模型提取出来,并放到单独的MODULE中。任何专有的东西都不应该放在这些模块中。把它们分离出来以后,在继续开发的过程中,它们的优先级应低于CORE DOMAIN的优先级,并且不要分派核心开发人员来完成这些任务(因为他们很少能够从这些任务中获得领域知识)。此外,还可以考虑为这些GENERIC SUBDOMAIN使用现成的解决方案或“公开发布的模型”(PUBLISHED MODEL)。
当开发这样的软件包时,有以下几种选择:
1、现成的解决方案
2、公开发布的设计或模型
3、把实现外包出去
4、内部实现
模式:DOMAIN VISION STATEMENT(领域愿景说明)
在项目开始时,模型通常并不存在,但是模型开发的需求是早就确定下来的重点。在后面的开发阶段,我们需要解释清楚系统的价值,但这并不需要深入地分析模型。此外,领域模型的关键方面可能跨越多个BOUNDED CONTEXT,而且从定义上看,无法将这些彼此不同的模型组织起来表明其共同的关注点。
因此:写一份CORE DOMAIN的简短描述(大约一页纸)以及它将会创造的价值,也就是“价值主张”。那些不能将你的领域模型与其他领域模型区分开的方面就不要写了。展示出领域模型是如何实现和均衡各方面利益的。这份描述要尽量精简。尽早把它写出来,随着新的理解随时修改它。
DOMAIN VISION STATEMENT可以用作一个指南,它帮助开发团队在精炼模型和代码的过程中保持统一的方向。团队中的非技术成员,管理层甚至是客户也都可以共享领域愿景说明。
模式:HIGHLIGHTED CORE(突出核心)
DOMAIN VISION STATEMENT从宽泛的角度对CORE DOMAIN进行了说明,但它把什么是具体核心模型元素留给人们自己去解释和猜测。除
非团队的沟通极其充分,否则单靠VISION STATEMENT是很难产生什么效果的。
尽管团队成员可能大体上知道核心领域是由什么构成的,但CORE DOMIAN中到底包含哪些元素,不同的人会有不同的理解,甚至同一个人在不同的时间也有会不同的理解。如果我们总是要不断过滤模型以便识别出关键部分,那么就会分散本应该投入到设计上的精力,而且这还需要广泛的模型知识。因此,CORE DOMAIN必须要很容易被分辨出来。
对代码所做的重大结构性改动是识别CORE DOMAIN的理想方式,但这些改动往往无法在短期内完成。事实上,如果团队的认识还不够全面,这样的重大代码修改是很难进行的。
通过修改模型的组织结构(如划分GENERIC SUBDOMIAN和本章后面要介绍的一些改动),可以用MODULE表达出核心领域。但如果把它作为表达CORE DOMAIN的唯一方法,那么对模型的改动会很大,因此很难马上看到结果。
我们可能需要用一种轻量级的解决方案来补充这些激进的技术手段。可能有一些约束使你无法从物理上分离出CORE,或者你可能是从已有代码开始工作的,而这些代码并没有很好地区分出CORE,但你确实很需要知道什么是CORE并建立共识,以便有效地通过重构进行更好的精炼。我们可以通过以下两种典型的代表性技术来突出核心:
1、精炼文档
编写一个非常简短的文档(3~7页,每页内容不必太多),用于描述CORE DOMAIN以及CORE元素之间的主要交互过程。但独立文档带来的所有常见风险也会在这里出现(如下所示),控制这些风险的最好方法是保持绝对的精简。
(1)文档可能得不到维护;
(2)文档可能没人阅读;
(3)由于多个信息来源,文档可能达不到简化复杂性的目的。
2、标明CORE
可能你会遇到一份数百页的“领域模型”文档等资料,但无需慌张。把模型的主要存储库中的CORE DOMAIN标记出来,不用特意去阐明其角色。是开发人员很容易就知道什么在核心内,什么在核心外。只需做很少的处理和维护工作,即可让处理模型的人员很清晰地看到CORE DOMAIN了。
因此:把精炼文档作为过程工具
如果精炼文档概括了CORE DOMAIN的核心元素,那么它就可以作为一个指示器——用以指示模型改变的重要程度。当模型或代码的修改影响到精炼文档时,需要与团队其他成员一起协商。当对精炼文档做出修改时,需要立即通知所有团队成员,而且要把心版本的文档分发给他们。CORE外部的修改或精炼文档外部的细节修改则无需协商或通知,可以直接把它们集成到系统中,其他成员在后续工作过程中自然会看到这些修改。这样开发人员就拥有了XP所建议的完全的自治性。
模式:COHESIVE MECHANISM(内聚机制)
计算有时会非常复杂,使设计开始变得膨胀。机械性的“如何做”大量增加,把概念性的“做什么”完全掩盖了。为了解决问题提供算法的大量方法掩盖了那些用于表达问题的方法。
因此:把概念上的COHESIVE MECHANISM(内聚机制)分离到一个单独的轻量级框架中。要特别注意公式或那些有完备文档的算法。用一个INTENTION-REVEALING INTERFACE来暴露这个框架的功能。现在,领域中的其他元素就可以只专注于如何表达问题(做什么)了,而把解决方案的复杂细节(如何做)转移给了框架。
GENERIC SUBDOMAIN和COHESIVE MECHANISM有什么不同?
GENERIC SUBDOMIAN与COHESIVE MECHANISM的动机是相同的——都是为CORE DOMAIN减负。区别在于二者所承担的职责的性质不同。GENERIC SUBDOMAIN是以描述性的模型作为基础的,它用这个模型表示出团队会如何看待领域的某个方面。在这一点上与CORE DOMIAN没什么区别,只是重要性和专门程度较低而已。COHESIVE MECHANISM并不表示领域,它的目的是解决描述性模型所提出来的一些复杂的计算问题。模型提出问题,COHESIVE MECHANISM解决问题。所以GENERIC SUBDOMAIN是模型级别维度,而COHESIVE MECHANISM是CORE DOMAIN的一部分。
模式提升:通过精炼得到声明式风格
声明式设计是一种精简的设计风格,在本书中也多处提及。精炼的价值在于使你能够看到自己正在做什么,不让无关细节分散你的注意力,并通过不断削减得到核心。如果领域中那些起到支持作用的部分提供了一种简练的语言,可用于表示CORE的概念和规则,同时又能够把计算或实施这些概念和规则的方式封装起来,那么CORE DOMAIN的重要部分就可以采用声明式设计。
COHESIVE MACHANISM用途最大的地方是它通过设计一个INTENTION-REVEALING INTERFACE来提供访问,并且具有概念上一致的ASSERTION和SIDE-EFFECT-FREE FUNCTION。利用这些MECHANISM和柔性设计,CORE DOMAIN可以使用有意义的声明,而不必调用难懂的函数。但最不同寻常的回报来自于使CORE DOMAIN的一部分产生突破,得到一个深层模型。
把GENERIC SUBDOMAIN提取出来可以减少混乱,而COHESIVE MECHANISM可以把复杂操作封装起来。这样可以得到一个更专注的模型,从而减少了那些对用户活动没什么价值、分散注意力的方面。但我们不太可能为领域模型中所有非CORE元素安排一个适当的去处。SEGREGATED CORE(分离的核心)采用直接的方法从结构上把CORE DOMAIN划分出来。
模式:SEGREGATED CORE(分离的核心)
模型中的元素可能有一部分属于CORE DOMAIN,而另一部分起支持作用。核心元素可能与一般元素紧密耦合在一起。CORE的概念内聚性可能不很强,看上去也不明显。这种混乱性和耦合关系抑制了CORE。设计人员如果无法清晰地看到最重要的关系,就会开发出脆弱的设计。
通过把GENERIC SUBDOMAIN提取出来,可以从领域中清除一些干扰性的细节,使CORE变得更清楚。但识别和澄清所有这些子领域是很困难的工作,而且有些工作看起来并不值得去做。同事,最重要的CORE DOMAIN仍然与剩下的那些元素纠缠在一起。
因此:对模型进行重构,把核心概念从支持性元素(包括定义得不清楚的那些元素)中分离出来,并增强CORE的内聚性,同时减少它与其他代码的耦合。把所有通用元素或支持性元素提取出来到其他对象中,并把这些对象放到其他的包中——即使这会把一些紧密耦合的元素分开。
这里基本上采用了与GENERIC SUBDOMAIN一样的原则,只是从另一个方向考虑而已。就目前来看,使用哪种简单解决方案都可以,只需把注意力集中在SEGREGATED CORE(分离的核心)上即可。
模式:ABSTRACT CORE
通常,即便是CORE DOMAIN模型也会包含太多的细节,以至于它很难表达出整体视图。当不同MODULE的子领域之间有大量交互时,要么需要在MODULE之间创建很多引用,这在很大程度上抵消了划分模块的价值;要么就必须间接地实现这些交互,而后者会使模型变得晦涩难度。
因此:把模型中最基本的概念识别出来,并分离到不同的类、抽象类或接口中。设计这个抽象模型,使之能够表达重要组件之间的大部分交互。把这个完整的抽象模型放到它自己的MODULE中,而专用的、详细的实现类则留在由子领域定义的MODULE中。
【学习心得】:我很幸运,能遇到一个近做了近十年的大型应用项目,我记得从刚开始只有两台刀片机服务发展至目前百来台高配置级别的PC规模。虽然我现在才看到这本书,但这十年的摸索过程其实就是这章节的实现。实在是非常宝贵的经验。
第16章:大型结构
在一个大的系统中,如果因为缺少一种全局性的原则而使人们无法根据元素在模式(这些模式被应用于整个设计)中的角色来解释这些元素,那么开发人员就会陷入“只见树木,不见森林”的境地。
“大型结构”是一种语言,人们可以用它来从大局上讨论和理解系统。
设计一种应用于整个系统的规则(或角色和关系)模式,使人们可以通过它在一定程度上了解各个部分在整体中所处的位置(即使是在不知道各个部分的详细职责的情况下)。本章将探讨一些能成功构建这种设计结构的模式。
模式:EVOLVING ORDER(演化有序)
一个没有任何规则的随意设计会产生一些无法理解整体含义且很难维护的系统。但架构中早期的设计假设又会使项目变得束手无策,而且会极大地限制应用程序中某些部分的开发人员/设计人员的能力。很快,开发人员就会为适应结构而不得不在应用程序的开发上委曲求全,要么就是完全推翻架构而又回到没有协调的开发老路上来。
因此:让这种概念上的大型结构随着应用程序一起演变,甚至可以变成一种完全不同结构的风格。不要依次过分限制详细的设计和模型决策,这些决策和模型决策必须在掌握了详细之后才能确定。
于CONTEXT MAP不同的是,大型结构是可选的。当发现一种大型结构可以明显使系统变得更加清晰,而又没有对模型开发施加一些不自然的约束时,就应该采用这种结构。使用不合适的结构还不如不使用它,因此最好不要为了追求设计的完整性而勉强去使用一种结构,而应该找到尽可能精简的方式解决所出现问题。要记住宁缺毋滥的原则。
模式:SYSTEM METAPHOR(系统隐喻)
软件设计往往非常抽象且难于掌握。开发人员和用户都需要一些切实可行的方式来理解系统,并共享系统的一个整体视图。
因此:当系统的一个具体类比正好符合团队成员对系统的想象,并且能够引导他们向着一个有用的方向进行思考时,就应该把这个类比用作一种大型结构。围绕这个隐喻来组织设计,并把它吸收到UBIQUITOUS LANGUAGE中。SYSTEM METAPHOR应该既能促进系统的交流,又能指导系统的开发。它可以增加系统不同部分之间的一致性,甚至可以跨越不同的BOUNDED CONTEXT。但所有隐喻都不是完全精确的,因此应不断检查隐喻是否过度或不恰当,当发现它起到妨碍作用时,要随时准备放弃它。
SYSTEM METAPHOR并不适用于所有项目。从总体上讲,大型结构不是必须要用的。在极限编程的12个实践中,SYSTEM METAPHOR的角色可以由UBIQUITOUS LANGUAGE来承担。当项目中发现一种非常适合的SYSTEM METAPHOR或其他大型结构时,应该用它来补充UBIQUITOUS LANGUAGE。
模式:RESPONSIBILITY LAYER(职责分层)
如果每个对象的职责都是人为分配的,将没有统一的指导原则和一致性,也无法把领域作为一个整体来处理。为了保持大型模型的一致,有必要在职责分配上实施一定的结构化控制。
因此:注意观察模型中的概念依赖性,以及领域中不同部分的变化频率和变化的原因。如果在领域中发现了自然的层次结构,就把它们转换为宽泛的抽象职责。这些职责应该描述系统的高层目的和设计。对模型进行重构,使得每个领域对象,AGGREGATE和MODULE的职责都清晰地位于一个职责层当中。
想要找到一种适当的RESPONSIBILITY LAYER或大比例结构,需要理解问题领域并反复进行实验。如果遵循EVOLVING ORDER,那么最初的起点并不是十分重要,尽管差劲的选择确实会加大工作量。结构可能最后演变得面目全非。因此,下面将给出一些指导方针,无论是刚开始选择一种结构,还是对已有结构进行转换,这些指导方针都适用。
当对层进行删除、合并、拆分和重新定义等操作时,应寻找并保留一下一些有用的特征:
□ 场景描述。层应该能够表达出领域的基本实现或优先级选择一种大比例结构与其说是一种技术决策,不如说是一种业务建模决策。
□ 概念依赖性。“较高”层概念的意义应该依赖“较低”层,而低层概念的意义应该独立于较高层。
□ CONCEPTUAL CONTOUR。如果不同层的对象必须具有不同的变化频率或原因,那么层应该能够容许它们之间的变化。
□ 潜能层。我们能够做什么?潜能层不关心我们打算做什么,而关心能够做什么。如企业的资源(包括人力资源)以及这些资源的组织方式是潜能层的核心。
□ 作业层。我们正在做什么?我们利用这些潜能做了什么事情?像潜能层一样,这个层也应该反映出现实情况,而不是我们设想的状况。如我们希望在这个层中看到自己的工作和活动:我们正在销售什么,而不是能够销售什么。通常来说,作业层对象可以引用潜能层对象,它甚至可以由潜能层对象组成,但潜能层对象不应该引用作业层对象。
□ 决策支持层。应该采取什么行动或制定什么策略?这个层是用来作出分析和制定决策的。它根据来自较低层(如潜能层或作业层)的信息进行分析。决策支持软件可以利用历史信息来主动寻找适用于当前和未来作业的机会
□ 策略层。规则和目标是什么?规则和目标主要是被动的,但它们约束着其他层的行为。这些交互的设计是一个微妙的问题。有时策略会作为一个参数传递给较低层的方法。有时会使用STRATEGY模式。策略层与决策支持层能够进行很好的协作,决策支持层提供了用于搜索策略层所设定的目标的方式,这些目标又受到策略层设定的规则约束。
□ 承诺层。我们承诺了什么?这个层具有策略层的性质,因为他表述了一些指导未来运营的目标;但它也有作业层的性质,因为承诺是作为后续因为活动的一部分而出现和变化的。
虽然这5个层对很多企业系统都适用,但并不是所有领域的主要概念都涵盖在这5个层中。有些情况下,在设计中生硬地套用这种形式反而会起反作用,而使用一组更自然的RESPONSIBILITY LAYER会更有效。
我们需要对分层结构进行调整和实验,但一定要使分层系统保持简单,如果层数超过4或5,就比较难处理了。层数越多将无法有效地描述领域,而且本来要使用大比例结构解决的复杂性问题又会以一种新的方式出现。我们必须对大比例结构进行严格的精简。
如果一个领域与上述讨论毫无关系,所有的分层可能都必须从头开始。最后,我们必须根据直觉选择一个起点,然后通过EVOLVING ORDER来改进它。
模式:KNOWLEGE LEVEL(知识级别)
如果在一个应用程序中,ENTITY的角色和它们之间的关系在不同的情况下有很大变化,那么复杂性会显著增加。在这种情况下,无论是一般的模型还是高度定制的模型,都无法满足用户的需求。为了兼顾各种不同的情形,对象需要引用其他的类型,或者需要具备一些在不同情况下包括不同使用方式的属性。具有相同数据和行为的类可能会大量增加,而这些类的唯一作用只是为了满足不同的组装规则。
因此:创建一组不同的对象,用它们来描述和约束基本模型的结构和行为。把这些对象分为两个“级别”,一个是非常具体的级别,另一个级别则提供了一些可供用户或超级用户定制的规则和知识。
如果得到合理的运用,KNOWLEDGE LEVEL能够解决一些其他方式很难解决的问题。如果系统中某些部分的定制非常关键,而要是不提供定制能力就会破坏掉整个设计,这时就可以利用知识级别来解决这一问题。
像其他大比例结构一样,KNOWLEDGE LEVEL也不是必须要使用的。没有它,对象照样能工作,而且团队可能仍然能够认识到他们需要将Employee与Payroll分离。当项目进行到某个时刻,这种结构看起来已经没什么用了,那么就可以放弃它。
咋看上去,KNOWLEDGE LEVEL像是RESPONSIBILITY LAYER(特别是策略层)的一个特例,但它并不是。首先,KNOWLEDGE LEVEL两个级别之间的依赖是双向的,而RESPONSIBILITY LAYER在层次结构中,较低的层不依赖于较高的层。实际上,RESPONSIBILITY LAYER可以与其他大部分的大比例结构共存,它提供了另一种用来组织模型的维度。
模式:PLUGGABLE COMPONENT FRAMEWORK( 可插入式组件框架 )
在深入理解和反复精炼基础上得到的成熟模型中,会出现很多机会。通常只有在同一个领域中实现了多个应用程序之后,才有机会使用PLUGGABLE COMPONENT FRAMEWORK(可插入式组件框架)。
当很多应用程序需要进行相互操作时,如果应用程序都基于相同的一些抽象,但它们是独立设计的,那么在多个BOUNDED CONTEXT之间的转换会限制它们的集成。各个团队之间如果不能紧密地协作,就无法形成一个SHARED KERNEL。重复和分裂将会增加开发和安装的成本,而且互操作会变得很难实现。
因此:从接口和交互中提炼一个ABSTRACT CORE,并创建一个框架,这个框架要允许这些接口各种不同实现被自由替换。同样,无论是什么应用程序,只要它严格地通过ABSTRACT CORE的接口进行操作,那么就可以允许它使用这些组件。
PLUGGABLE COMPONENT FRAMEWORK也有几个缺点:
□ 一个缺点是它是一种非常难以使用的模式。它需要高度精确的接口设计和一个非常深入的模型,以便把一些必要的行为捕获到ABSTRACT CORE中。
□ 另一个很大的缺点是它只为应用程序提供了有限的选择。如果一个应用程序需要对CORE DOMAIN使用一种非常不同的方法,那么可插入式组件框架将起到妨碍作用。
总结:通过重构得到更适当的结构
1、最小化
2、沟通和自律
3、通过重构得到柔性设计
4、通过精炼可以减轻负担
【学习心得】:如果是在持续用心做事的话,每一次重构都是为了比原来的更好,以上的几种模式多少都会触碰得到,如果系统足够“大且运行良好的话。当然,这本书会给了我更加宽广的视野。
第17章:领域驱动设计的综合运用
1、把大型结构与BOUNDED CONTEXT结合起来使用
2、将大型结构与精炼结合起来使用
大型结构和精炼的概念也是互为补充的。大型结构可以帮助解释CORE DOMAIN内部的关系以及GENERIC SUBDOMIAN之间的关系。同时大型结构本身可能也是CORE DOMAIN的一个重要部分。
3、首先评估
当对一个项目进行战略设计时,首先需要清洗地评估现状。
(1)画出CONTEXT MAP。你能画出一个一致的图吗?有没有一些模棱两可的情况?
(2)注意项目上的语言使用。有没有UBIQUITOUS LANGUAGE?这种语言是否足够丰富,以便帮助开发?
(3)理解重点所在。CORE DOMAIN被识别出来了吗?有没有DOMAIN VISION STATEMENT?你能写一个吗?
(4)项目所采用的技术是遵循MODEL-DRIVEN DESIGN,还是与之相悖?
(5)开发团队是否具备必要的技能?
(6)开发人员是否了解领域知识?他们对领域是否感兴趣?
当然,我们不会发现完美的答案。我们现在对项目的了解永远不如将来的了解深入。但这些问题为我们提供了一个可靠的起点。
4、有谁定制策略
传统上,架构是在应用程序开发开始之前建立的,并且在这种组织中,负责建立架构的团队比应用开发团队拥有更大的权利。但我们并不一定的遵循这种传统的方式,因为它并不总是十分有效。
战略设计必须明确地应用于整个项目。项目有很多组织方式,这一点我并不想做过多的说明。但是,要想使决策制定过程更有效,需要注意一些基本问题。
1、从应用程序开发自动得出的结构
一个非常善于沟通、懂得自律的团队在没有核心领导的情况下照样能够很好地工作,他们能够遵循EVOLVING ORDER来达成一组共同遵守的原则,这样就能够有机地形成一种秩序,而不用靠命令来约束。
2、以客户为中心的架构团队
当几个团队共用同一种策略时,确实需要集中制定一些决策。架构师如果脱离实际开发工作,就可能会设计出失败的模型,但这是完全可以避免的。架构团队可以把自己放在与应用开发团队平等的位置上,帮助他们协调大型架构、BOUNDED CONTEXT边界和其他一些跨团队的技术问题。为了在这个过程中发挥作用,架构团队必须把思考重点放在应用程序的开发上。
5、制定战略设计决策的6个要点
1、决策必须传达整个团队;
2、决策过程必须收集反馈意见;
3、计划必须允许演变;
4、架构团队不必把所有最好、最聪明的人员都吸收进来;
5、战略设计需要遵守简约和谦逊的原则;
6、对象的职责要专一,而开发人员应该是多面手。
技术框架同样如此
技术框架提供了基础设施层,从而使应用程序不必自己去实现基础服务,而且技术框架还能帮助把领域与其他关注点隔离开,因此它能够极大地加速应用程序(包括领域层)的开发。但技术框架也有风险的,那就是它会影响领域模型实现的表达能力,并妨碍领域模型的自由改变。
不要编写“傻瓜式”的框架。
在划分团队时,如果认为一些开发人员不够聪明,无法胜任设计工作,而让他们去做开发工作,那么这种态度可能会导致失败,因为他们低估了应用程序开发的难度。
注意总体规划
由Christopher Alexander领导的一群建筑师(设计大楼的建筑师)在建筑和城市规划领域中提倡“聚少成多地成长”(piecemeal growth)。他们非常好地解释了总体规划失败的原因。
如果没有某种规划过程,那么俄勒冈州大学的校园永远不会像剑桥大学校园那样庞大、和谐而井井有条。
总体规划是解决这种难题的传统方法。它试图建立足够多的指导方针,来保持整体环境的一致性,同时仍然为每幢建筑保留自由度,并为适应局部需要预留下广阔的空间。
······将来这所大学的所有部分将构成一致的整体,因为它们只是被“插入”到总体设计的各个位置中。
······实际上总体规划会失败,因为它只是建立了一种极权主义的秩序,而不是一种有机的秩序。它们过于生硬,因此不容易根据自然变化和不可预料的社会生活变化来做出调整。当这些变化发生时······总体规划就过时了,而且不再被人们遵守。即使人们遵守总体规划······它们也没有足够详细地指定建筑物之前的联系,人口规模、功能均衡等这些用来帮助每幢建筑的局部行为和设计很好地符合整体环境的方面。
······试图驾驭这种总体规划过程非常类似于在小孩的填色本上填充颜色······这个过程最多也不过是得到一种极为平常的秩序。
······因此,通过总体规划是无法得到一种有机的秩序的,因为这个规划既过于精确,又不够细致。它在整体上过于精确了,而细节上又不够细致。
······总体规划的存在疏远了用户[因为,从根本上讲]大部分重要决策已经确定下来,因此社区成员对社区未来的建设几乎没有什么影响了。
——摘自Oregon Experiment,PP. 16-28 [Alexander et al. 1975]
Alexander和他的同事倡议由社区成员共同制定一组原则,并在“聚少成多地成长”的每次行动中多应用这些原则,这样就会形成一种“有机秩序”,并且能够根据环境变化做出调整。
【学习心得】:由于我阅读的集中力不足,所以无法很好地从一次阅读中获取系统性的认知。因此,我必须用抄写去深入我心。特别是一些重要且很重要的知识,我必须这么做。虽然费时费力,但用未来的眼光去看,当下是值得的,再用当下的眼光看未来,原来我现在做的都是对的。笨一点没关系,时间就这么用的。
来源:https://www.cnblogs.com/wcd144140/p/12289586.html