简单回顾
上一篇介绍了“精炼”,我们学习了可以用于精炼的几种模式,比如 CORE DOMAIN 模式, GENERIC SUBDOMAIN 模式, DOMAIN VISION STATEMENT 模式等等。 使用这些模式让开发者们能够把注意力集 中到核心元素上,并把其他元素表示为支持作用 。
但如果不贯彻某个主旨来应用一些系统级的设计元素和模式的话,关系仍然可能非常混乱。本章节我将概要介绍几种大型结构方法,然后详细讨论其中一种模式—— RESPONSIBILITY LAYER(职责层),通过这个示例来探索使用大型结构的含义。
复杂系统面临的真实问题
背景
硅谷一家小设计公司签了一份为卫星通信系统创建模拟器的合同。工作进展得很顺利,他们正在利用MODEL-DRIVEN DESIG来进行开发。为了澄清模型中的复杂关系,他们已经把设计分解为一些在规模上便于管理的内聚MODULE,于是现在便有了的很多MODULE。
问题
随着越来越多的MODULE被创建,现在系统开发面临着这样一些问题,开发人员要想查找某个功能,应该到哪个MODULE中去查呢?如果有了一个新类,应该把它放在哪里?这些小软件包的实际意义是什么?它们又是如何协同工作的呢?而且以后还要创建更多的MODULE。
开发人员互相之间仍然能够进行很好的沟通,而且也知道每天都要做什么工作,但项目领导者却不满足这种一知半解的状态。他们需要某种组织设计的方式,以便在项目进入到更复杂的阶段时能够理解和掌控它。
解决方案1.0
开发人员提出了不同的打包方案。有一些文档给出了系统的全貌,还有一些使用建模工具绘制的类图一一新视图可以用来指引开发人员找到正确的模块。但项目领导者对这些小花招并不满意。
缺点
1)方案1.0可以用模型把模拟器的工作流程简单地描述出来,也可以说清楚基础设施是如何序列化数据的,以及电信技术层怎样保证数据的完整性和路由选择。虽然模型包含了所有细节,但是却没有一条清楚的主线。领域的一些重要概念丢失了。这次丢失的不是对象模型中的一两个类,而是整个模型的结构。
解决方案2.0
经过一两周的仔细思考之后,开发人员有了思路。他们打算把设计放到一个结构中。整个模拟器将被看作由一系列层组成,这些层分别对应于通信系统的各个方面。最下面的层用来表示物理基础设施,它具有将数据位从一个节点传送到另一个节点的基本能力。它的上面是封包路由层,与数据流定向有关的问题都被集中到这一层中。其他的层则表示其他概念层次的问题。这些层共同描述了系统的大致情况。
按照新的结构来重构代码。为了不让MODULE跨越多个层,必须对它们重新定义。在一些情况下,还需要重构对象职责,以便明确地让每个对象只属于一个层。另一方面,藉由应用这些新思路的实际经验,概念层本身的定义也得到了精化。层、MODULE和对象一起演变,最后,整个设计都符合了这种分层结构的大体轮廓。
简单总结:将设计放到一个结构中。对整个系统重新分层,整个模拟器将被看作由一系列层组成,这些层分别对应于通信系统的各个方面。同时按照新的结构来重构代码。
优点:
1)分层结构描述了系统的大体轮廓,设计变得易于理解。人们基本上知道到哪里去寻找某个特定功能。分工不同的开发人员所做的设计决策
可以大体上互相保持一致。
问题结论
1)在一个大的系统中,如果因为缺少一种全局性的原则而使人们无法根据元素在模式(这些模式被应用于整个设计)中的角色来解释这些元素,那么开发人员就会陷入“只见树木,不见森林”的境地。
2)我们需要理解各个部分在整体中的角色,而不必去深究细节。
由此案例引出大型结构这个概念
“大型结构”是一种语言,人们可以用它来从大局上讨论和理解系统。它用一组高级概念或规则(或两者兼有)来为整个系统的设计建立一种模式。这种组织原则既能指导设计,又能帮助理解设计。另外,它还能够协调不同人员的工作,因为它提供了共享的整体视图,让人们知道各个部分在整体中的角色。
注意事项
大型结构可以被限制在一个BOUNDED CONTEXT中,但通常是跨多个BOUNDED CONTEXT中。当团队规模较小而且模型也不太复杂时,只需将模型分解为合理命名的MODULE,再进行一定程度的精炼,然后在开发人员之间进行非正式协调,以上这些就足以使模型保持良好的组织结构了。不要强行套用大型结构。虽然大型结构可以节省项目的开发费用,但不适当的结构会严重妨碍开发的进展。以下内容将探讨一些能成功构建这种设计结构的模式。
模式:EVOLVING ORDER (有序演化)
现状
很多开发人员都亲身经历过由于设计结构混乱而产生的代价。为了避免混乱,项目通过架构从各个方面对开发进行约束。无论架构是面向技术的,还是面向领域的,如果其限定了很多前期设计决策,那么随着需求的变更和理解的深入,这些架构会变得束手束脚。
为了规避混乱,不得不做很多限定,但是过多限定,可能会产生如下问题:
一个没有任何规则的随意设计会产生一些无法理解整体含义且很难维护的系统。但架构中早期的设计假设又会使项目变得束手束脚,而且会极大地限制应用程序中某些特定部分的开发人员/设计人员的能力。很快,开发人员就会为适应结构而不得不在应用程序的开发上委曲求全,要么就是完全推翻架构而又回到没有协调的开发老路上来。
问题并不在于指导规则本身应不应该存在,而在于这些规则的严格性和来源。如果这些用于控制设计的规则确实符合开发环境,那么它们不但不会阻碍开发,而且还会推动开发在健康的方向上前进,并且保持开发的一致性。
解决方案
让这种概念上的大型结构随着应用程序一起演变,甚至可以变成一种完全不同的结构风格。不要依此过分限制详细的设计和模型决策,这些决策和模型决策必须在掌握了详细知识之后才能确定。根据实际经验和领域知识来选择结构,并避免采用限制过多的结构,如此可以降低折中的难度。真正适合领域和需求的结构能够使细节的建模和设计变得更容易,因为它快速排除了很多选项。
结论
要想创建一种既为开发人员保留必要自由度同时又能保证开发工作不会陷入混乱的结构绝非易事。尽管人们已经在软件系统的技术架构上投入了大量工作,但有关领域层的结构化研究还很少见。一些方法会破坏面向对象的范式,如那些按应用任务或按用例对领域进行分解的方法。整个领域的研究还很贫瘠。
当发现一种大型结构可以明显使系统变得更清晰,而又没有对模型开发施加一些不自然的约束时,就应该采用这种结构。
以下会介绍4中较为通用的大型结构模式,可以解决现阶段大型系统开发过程中普遍存在的问题
模式:SYSTEM METAPHOR (系统隐喻)
隐喻思维在软件开发(特别是模型)中是很普遍的。但极限编程中的“隐喻”却具有另外一种含义,它用一种特殊的隐喻方式来使整个系统的开发井然有序。
系统隐喻
SYSTEM METAPHOR(系统隐喻)是一种松散的、易于理解的大型结构,它与对象范式是协调的。由于系统隐喻只是对领域的一种类比,因此不同模型可以用近似的方式来与它关联,这使得人们能够在多个BOUNDED CONTEXT中使用系统隐喻,从而有助于协调各个BOUNDED CONTEXT之间的工作。
作用
SYSTEM METAPHOR应该既能促进系统的交流,又能指导系统的开发。它可以增加系统不同部分之间的一致性,甚至可以跨越不同的BOUNDED CONTEXT。
注意事项
系统隐喻试一把双刃剑,并不适用于所有项目。且所有隐喻都不是完全精确的,因此应不断检查隐喻是否过度或不恰当,当发现它起到妨碍作用时,要随时准备放弃它,不要为了用而用。
结论
当系统的一个具体类比正好符合团队成员对系统的想象,并且能够引导他们向着一个有用的方向进行思考时,就应该把这个类比用作一种大型结构。“防火墙”就是一个类比,真实世界的防火墙可以防止火势从其他建筑蔓延到自身,而软件“防火墙”可以保护局域网免受来自外部网络的破坏
模式:RESPONSIBILITY LAYER (按照职责分层)(章节重点)
抛出观点
如果每个对象的职责都是人为分配的,将没有统一的指导原则和一致性,也无法把领域作为一个整体来处理。为了保持大模型的一致,有必要在职责分配上实施一定的结构化控制。
暗示:按职责分层,对应章节标题
什么是层?
所谓的层,就是对系统进行划分,每个层的元素都知道或能够使用在它“下面”的那些层的服务,但却不知道它“上面”的层,而且与它上面的层保持独立。
虽然这种自发的分层方式虽然使跟踪依赖性变得更容易,而且有时具有一定的直观意义,但它对模型的理解并没有多大的帮助,也不能指导建模决策。我们需要一种具有更明确目的的分层方式。
如何进行有意义的分层?
在一个具有自然层次结构的模型中,可以围绕主要职责进行概念上的分层,这样可以把分层和职责驱动的设计这两个强有力的原则结合起来使用。这些职责必须比分配给单个对象的职责广泛得多才行。
注意观察模型中的概念依赖性,以及领域中不同部分的变化频率和变化的原因。如果在领域中发现了自然的层次结构,就把它们转换为宽泛的抽象职责。这些职责应该描述系统的高层目的和设计。对模型进行重构,使得每个领域对象、AGGREGATE和MODULE的职责都清晰地位于一个职责层当中。
为什么要分层?
这种明确的职责分组可以提高模块化系统的可理解性,因为MODULE的职责会变得更易于解释。而高层次的职责与分层的结合为我们(开发人员/领域专家)提供了一种系统的组织原则。
分层模式有一种变体最适合按职责来分层,我们把这种变体称为RELAXED LAYERED SYSTEM(松散分层系统)[Buschmann etal.1996,p.45],如果采用这种分层模式,某一层中的组件可以访问任何比它低的层,而不限于只能访问直接与它相邻的下一层。
上述内容太过抽象,我们可以通过示例来深入理解
案例
以运输系统为例,以下是运输系统模型的一部分。此时开发团队已经提炼出一个CORE DOMAIN,他们正在寻找一个能体现整个系统的主题并且让大家一致认同的大型结构。
下图展示了在预订期间如何使用模型来制定一个货运线路。
团队成员观察到了一些自然的概念层次结构,比如讨论运输时间表时,不需要涉及到货物。而讨论货物的跟踪时,如果不知道它的运输信息,就很难进行跟踪。这里有很清晰的概念依赖性。团队分出了两个层:作业层和支持这些作业的基础层(也称为能力层)。
公司的活动都被放到作业层中,最明显的作业对象是 Cargo ,它是公司大部分日常活动的焦点。Route Specification 是 Cargo 不可或缺的一部分,它规定了运输需求,而 Itinerary 是运输计划,这些对象都以 Cargo 为聚合根。
能力层反应了公司在执行作业时所能利用的资源。为货轮制定航程时间表时,需要考虑货轮的货运能力,因此 Transport Leg 是一个典型的例子。
把 Customer 对象放在哪里是一个稍微复杂一点决策。这家运输公司需要与客户保持长期关系,因为大部分业务来自回头客。所以 Customer 应该属于能力层。如果是快递公司,客户可能只是临时对象,在包裹投递完成后就被遗忘了,此时客户仅与作业相关,可以放在作业层。
虽然作业层与能力层的区别是这张图看上去很清楚了,但次序仍需要进一步细化。经过几个星期的试验后,团队发现 Router 并不是作业(计划)的一部分,它是用来帮助修改计划的,因此定义了一个新的层,用来支持决策(Decision Support)。
决策支持层可以为用户提供用于制定计划和决策的工具。Router 是一个 SERVICE,能帮助选择运送货物的最佳路线,因此属于决策支持层。原先在 Transport Leg 中的 is preferred 属性是因为公司希望优先使用自己的货轮,当 Router 选择最佳路线时会用到这个属性,因此它与能力层毫无关系,所以最终被重构到了 Route Bias Policy 中。
在多次重构之后这个模型更加符合大型结构了。
从案例中可得知,在选择合适的 RESPONSIBILITY LAYER 时,可从以下几个方面考量。
1)场景描述
层应该能够表达出领域的基本现实或优先级。选择一种大比例结构与其说是一种技术决策,不如说是一种业务建模决策。层应该显示出业务的优先级。
2)概念依赖性
较高层概念的意义应该依赖较低层,而底层概念应该独立于较高的层。
3)CONCEPTUAL CONTOUR(概念轮廓)
如果不同层的对象必须具有不同的变 化频率或原因,那么层应该能够容许它们之间的变化。
以下是常见的几种分层
1)能力层
我们能够做什么?能力层不关心我们打算做什么,而关心能够做什么。也称为潜能层。
2)作业层
我们正在做什么?我们利用这些潜能做了什么事情?像潜能层(能力层)一样,这个层也应该反映出现实状况,而不是我们设想的状况。我们希望在这个层中看到自己的工作和活动:我们正在销售什么,而不是能够销售什么。
3)决策支持层
应该采用什么行动或指定什么策略?这个层是用来作出分析和制定决策的。它根据来自较低层(比如潜能层或作业层)的信息进行分析。决策支持软件可以利用历史信息来主动寻找适用于当前和未来作业的机会。 很多项目利用数据仓库技术实现决策支持层,此时决策支持层实际上变成了一个独特的BOUNDED CONTEXT,并且与作业层软件具有一种CUSTOMER/SUPPLIER关系。
4)策略层
规则和目标是什么?策略层可以和其他层使用同一种语言来编写,但他们有时候是使用规则引擎来实现的。所以也可以考虑放到单独的 BOUNDED CONTEXT 中。
在金融服务或保险业中,潜能(能力)在很大程度上是由当前的运营状况决定的。 例如金融服务、保险等行业; 一家保险公司在考虑签保单承担理赔责任时,要根据当前业务的多样性来 判断是否有能力承担它所带来的风险。潜能(能力)层有可能会被合并到作业层 中,这样就会演变出一种不同的分层结构。
这些情况下经常出现的一个层是对客户所做出的承诺。
5)承诺层
我们承诺了什么?这个层具有策略层的性质,因为它表述了一些指导未来运营的目标;但它也有作业层的性质,因为承诺是作为后续业务活动的一部分而出现和变化的。
上述5个层虽然对很多企业系统都适用,但并不是所有领域的概念都涵盖在这5个层中。我们需要根据实际情况决定分层,层数过多将无法有效描述领域。
结论
在模型构建过程中,注意观察模型中的概念依赖性,以及领域中不同部分的变化频率和变化的原因。如果在领域中发现了自然的层次结构,就把它们转换为宽泛的抽象职责。这些职责应该描述系统的高层目的和设计。对模型进行重构,使得每个领域对象、AGGREGATE和MODULE的职责都清晰地位于一个职责层当中。这样重构后模型就会更加符合大型结构。
模式:KNOWLEDGE LEVEL (知识层级)
KNOWLEDGE LEVEL 是一组描述了另一组对象应该有哪些行为的现象。它并不像其他分析模式那样对领域进行建模,而是用来构造模型的。
章节开头,大图说明什么是KNOWLEDGE LEVEL
场景描述
当我们需要让用户对模型的一部分有所控制,而模型又必须满足更 大的一组规则时,可以利用KNOWLEDGE LEVEL(知识级别)来处理 这种情况。它可以使软件具有可配置的行为,其中实体中的角色和关系 必须在安装时(甚至在运行时)进行修改。
当对系统进行修改或替换时,开发人员会发现,有一些功能的真实含义并不想他们看上去的那样。它们在不同情况下具有完全不同的含义。如果不破坏这些互相叠加的含义,修改任何东西都是非常困难的。想要把数据迁移到一个“更合适”的系统中,必须要理解这些奇怪的部分,并对其进行编码。
看不懂/(ㄒoㄒ)/~~,通过案例深入理解
案例
以员工工资和退休金管理系统为例,员工分为小时工和受薪员工,小时工的退休金计划为固定代扣制(Defined Contribution),而受薪员工为固定受益制(Defined Benefit)。
在这个模型中,每个员工随便加入哪一种退休计划都可以,因此每位办公室行政人员都可以改变退休计划。管理层最后放弃了这个模型,因为它没有反映出公司的策略。
办公室行政人员按小时付薪酬,且采用固定受益退休计划。这个策略暗示出job title(工作头衔)字段现在表示了一个重要的领域概念。开发人员可以重构模型,用Employee Type(员工类型)把这个概念明确显示出来,如图16-19和图16-20所示。
只有超级管理员才能编辑 Employee Type 对象,而且只有公司策略变更时,他才能修改此对象。这些受限制的对象让开发人员想到了 KNOWLEDGE LEVEL 模式,他尝试按照这种思想,重新组织了一下模型:
受限制的对象都在 KNOWLEDGE LEVEL 中,可以自由编辑的对象都在 Operational Level 中,区分得非常清楚。由于模型变得更清晰了,开发人员发现 Employee Type 可以被指定为 Retirement Plan 中的任何一种, 也可以被指定为两种工资类型中的任何一种。这里实际上隐含了一个 Payroll 的概念,它和 Employee Type 混在了一起。于是开发人员重构了模型:
通过案例得出结论
如果在一个应用程序中,ENTITY的角色和它们之间的关系在不同的情况下有很大变化,那么复杂性会显著增加。这种情况下,无论是一般的模型还是高度定制的模型,都无法满足用户的需求。为了兼顾各种不同的情形,对象需要引用其他的类型,或者需要具备一些属性,用于在不同情况下采用不同使用方式。相当于在我们模型中嵌入了另一个模型,作用是用来描述我们的模型。KNOWLEDGE LEVEL分离了模型的自我定义的功能,并清晰地表达了它的限制。
KNOWLEDGE LEVEL 模式下,需要创建一组不同的对象,用他们来描述和约束基本模型的结构和行为。把这些对象分为两个“级别”,一个是非常具体的级别,另一个级别则提供了一些可供用户或者超级用户定制的规则和知识。
相似和区别
KNOWLEDGE LEVEL 是 REFLECTION(反射)模式在领域层中的一种应用。Java中有一些基本的反射机制,用于查询一个类的方法和属性等信息。
KNOWLEDGE LEVEL 看上去像是 RESPONSIBILITY LAYER 的一个特例,但事实并非如此。KNOWLEDGE LEVEL 中两个级别之间的依赖性是双向的,而在层次结构中,依赖是单向的。
KNOWLEDGE LEVEL具有两个很有用的特性。
1)首先,它关注的是应用领域,这一点与人们所熟悉的REFLECTION模式的应用正好相反。
2)其次,它并不追求完全的通用性。KNOWLEDGE LEVEL显得更简单,而且可以传达设计者的特别意图。
创建一组不同的对象,用它们来描述和约束基本模型的结构和行为。把这些对象分为两个“级别”,一个是非常具体的级别,另一个级别则提供了一些可供用户或超级用户定制的规则和知识。
模式:PLUGGABLE COMPONENT FRAMEWORK (可插入式组件框架)
背景
当很多应用程序需要进行互操作时,如果所有应用程序都基于相同的一些抽象,但它们是独立设计的,那么在多个BOUNDEDCONTEXT之间的转换会限制它们的集成。各个团队之间如果不能紧密地协作,就无法形成一个SHARED KERNEL。重复和分裂将会增加开发和安装的成本,而且互操作会变得很难实现。
解决方案
将它们的设计分解为组件,每个组件负责提供某些 类别的功能。通常所有组件都插入到一个中央hub上,这个hub支持组件 所需的所有协议,并且知道如何与它们所提供的接口进行对话。还有其 他一些将组件连在一起的可行模式。对这些接口以及用于连接它们的 hub的设计必须要协调,而组件内部的设计则可以更独立一些。
可插入式组件框架的白话文解释
使用场景和要求
从接口和交互中提炼出一个 ABSTRACT CORE,并创建一个框架,这个框架需要允许这些接口的各种不同实现被自由替换。通用,无论是什么应用程序,只要它严格的通过 ABSTRACT CORE 的接口进行操作,那么就可以允许它使用这些组件。
缺点
1) 一个缺 点是它是一种非常难以使用的模式。它需要高精度的接口设计和一个非 常深入的模型,以便把一些必要的行为捕获到ABSTRACT CORE中。
2) 它只为应用程序提供了有限的选择。如果一个应用程 序需要对CORE DOMAIN使用一种非常不同的方 法,那么可插入式组 件框架将起到妨碍作用。
结构应该有一种什么样的约束
从非常宽松的SYSTEM METAPHOR到严格的PLUGGABLE COMPONENT FRAMEWORK。当然,还有很多其他结构,而且,甚至在一个通用的结构模式中,在制定规则上也可以选择多种不同的严格程度。
我们可以为每种不同的情况设计不同的事件机制,也可以让特殊层 中的对象在交互时遵守一种一致的模式。结构越严格,一致性就越高, 设计也越容易理解。如果结构适当的话,规则将推动开发人员得出好的设计。不同的部分之间会更协调。
另一方面,约束也会限制开发人员所需的灵活性。在异构系统中, 特别是当系统使用了不同的实现技术时,可能无法跨越不同的 BOUNDED CONTEXT来使用非常特殊的通信路径。
因此一定要克制,不要滥用框架和死板地实现大比例结构。大比例 结构的最重要的贡献在于它具有概念上的一致性,并帮助我们更深入地 理解领域。每条结构规则都应该使开发变得更容易实现。
通过重构得到更合适的结构
控制成本的一个关键是保持一种简单、轻量级的结构。不要试图使结构面面俱到。只需解决最主要的问题即可,其他问题可以留到后面一个一个地解决。开始最好选择一种松散的结构,如SYSTEM METAPHOR或几个RESPONSIBILITY LAYER。不管怎样,一种最小化的松散结构可以起到轻量级的指导作用,它有助于避免混乱。
整个团队在新的开发和重构中必须遵守结构。要做到这一点,整个团队必须理解这种结构。必须把术语和关系纳入到UBIQUITOUS LANGUAGE中。
对模型施加的另一项关键工作是持续精炼。这可以从各个方面减小修改结构的难度。
总结
本章,重点阐述了大型结构系统中可能存在的一些真实问题,并分别给出了几个指导原则进行加以改进。
模式:EVOLVING ORDER (有序演化):大型系统中的一些复杂规则和约束,在初期不要限制的太严格,因为随着需求的不断变化,这些严格的规则和约束往往会限制住开发人员的手脚,在初期可以稍微宽松点,对规则和约束逐步演进。
模式:SYSTEM METAPHOR (系统隐喻):有利于理解大型系统中的一些构件,需要把隐喻加到通用领域语言内。
模式:RESPONSIBILITY LAYER (按照职责分层):对于复杂系统,需要能通过职责划分出不同的层。分层的艺术可以重点看下示例。
模式:KNOWLEDGE LEVEL (知识等级):用来描述另一组对象该有哪些行为,主要作用用来构造对象。构造的艺术需要重点消化下文中的示例
以下是各种大型结构和模型的关系、