架构设计之拥抱着变化而设计
防止软件退化最重要的工作就是要有良好的设计,那么什么是好的设计?所谓“合适的就是最好的”,那么什么是“合适”?合适,就需要对客户需求作更深入的分析,通过挖掘潜在的需求,找到设计中的关键点,特别是对于面向对象的分析和设计(OOAD)来说尤其重要。
一、面向对象分析与设计的本质
1,结构化设计思维及其缺陷
软件设计思维的早期是以结构化为核心的。结构化思维最本质的东西就是功能分解:这是一种处理复杂问题的自然方法,其原因在于解决更小的问题,比解决整个问题更简单,它通常形成的结构,是让一个“主”程序(模块)负责控制子程序(模块),但这样一来,就造成了如下缺点:
l 主模块所承受的责任太多、太集中,承受的压力相当大;
l 经常会产生非常复杂的代码,而且互相间的耦合很严重。
正是由于这样的特征,结构化应对变化的能力很差。但是在现实世界中,变化总是无法避免的,例如,要为已有的主题增加新的变体,需要新的业务方法等。如果将实现业务各步骤的所有逻辑代码,按照自然方式放在一个模块中的话,那么这些业务任何实质性的变化,都会造成对这些模块进行大面积修改。
2,需求总是会发生变化
1)需求变更不可避免
问问软件开发人员,对于从用户那里获取的需求,他们认为有哪些说法正确的?我相信经常得到的回答是这样的:
l 需求是不完整的。
l 需求经常是错误的。
l 需求(和用户)容易让人误解。
l 需求并不会告诉你全部情况。
只有一种问答是听不到的:“我们的需求不仅完整、清晰、易于理解,而且还说明了我们今后五年需要的所有功能!”即使前期我们确实耗费了极大的努力来收集需求,这样的需求还是会发生变化。
2)需求变化的原因
需求变更的根源,除了需求获取和分析的能力之外,关键之处还有如下几个简单原因:
l 用户对自己需求的看法,会因为后期获取信息越来越多,特别是在开发过程中与开发人员的讨论,以及看到软件新的可能性而发生变化。
l 开发人员自己对用户问题领域的看法,会在开发这个领域的软件过程中,因为对它更加熟悉而发生变化。
l 最重要的原因:我们是人,只要有可能,我们总是希望做到最好,或者得到更好的东西。
在多年编写软件的经历中,我认为应该教会我们最主要的一点就是:需求总在变化。大多数开发人员都认为需求变化是一件坏事。但是很少有人去认真设计可以很好处理需求变更的结构,这就使问题变得越来越严重。
现实情况确实是需求总是在变化,但这并不意味着我们可以不去收集好的需求。良好的需求可以保证不至于由于当初需求的缺失而发生不应有的变化。但是,我们也不应该不正视那些自然而然会发生的事情。与其抱怨需求总是变化,还不如适应这种变化,这就是“拥抱着变化而设计”。
3,赋予职责:易于应对变化的结构
从设计的角度,能适应需求变化的设计原理是什么呢?
1)需要判断需求会在哪里变化
关键之处在于:我可能无法知道什么将会变化,但是我能够猜到在哪里会变化。
如果我们在分析中就能注意到这一点,就可以利用面向对象设计的巨大优点,封装这些变化区域,从而更容易地将代码与变化产生的影响隔离开来。
2)分离职责就是分离变化
为了理解这一点,我们可以从其它的领域来获取思想。当我们在管理一个项目,要为团队分配任务的时候可以有两种做法:
l 第一种方法:你是全权负责的领导,直接给每个人都提供详细的指示。在这个情况下,你必须密切关注大量细节,团队所有人必须按你的指示行事,而且团队除你之外没有其他人负责。在项目非常庞大并存在变化的时候,你会不会疯掉?这就是垂直管理模型,也是结构化的特征。
l 第二种方法:你只给出通用的提示,给每个人分配职责,然后期待每个人会自己弄清怎样完成任务。你只是负责各个职责之间的协调,团队中每个人都知道这个职责的目标,并按照他们自己熟悉的方式行事。在项目非常庞大并存在变化的时候,你是什么样的感觉?这就是水平管理模型,也是面向对象的特征。
这其中最大的区别就是责任的转移。在第一种情况下,你要对一切负责;而在第二种情况下,人们对自己的行为负责。这两种情况下,要实现的目的相同,但组织方式差异很大。
当情况发生变化的时候,在第一种方法中,大量的指示修改可能会耗用你大量的精力而望而却步。在第二种方法中,你首先定义好相互间的接口,然后与担负相关职责的人交流,请他们再通过互相交流来自主找到解决方案,这往往更能应对变化。
3)面向对象的三个视角
既然软件是一种思维,那么我们的软件设计思维中又从中吸取到什么营养呢?面向对象的思想正是在这种对于实践的思考中发展起来的,它的核心就是赋予职责,我们可以站在三个视角来看待职责问题:
l “概念”的视角:呈现所研究领域中的各种概念,回答软件要负责什么的问题。
l “规约”的视角:关注软件的接口,而不是实现,回答怎么使用软件的问题。
l “实现”的视角:构建合理的结构,回答软件怎样履行自己职责的问题。
由于“实现”被隐蔽在“规约”之后,当履行职责的方式发生变化时,并不会对其他部分造成影响。正是设计上关注的这三个视角,决定了面向对象的设计思想与结构化有根本的区别,并且具有极大的适应变化的能力。
4,面向对象分析与设计的新要求
1)面向对象是个思维方式
现在已经没有人怀疑面向对象分析与设计(OOAD)的巨大威力了,人们可以如数家珍的说出面向对象的特点是:抽象性、封装性、继承性、多态性。但是我看到的情况是,尽管人们使用着面向对象的工具(UML、C++、Java、C#),但思维方式还是局限于传统的结构化思想,并没有真正体会到面向对象的核心思维,因此设计出的软件也不具面向对象的特征。
面向对象最精髓的思想是:软件是个活物,它不是一栋建筑,而是一个可培育可成长的东西。因此,用面向对象的思想开发出来的软件已开始可能不是那么完美,但是它可以不断成长壮大。换句话说,用面向对象的思想开发出来的软件必须适应变化。
2)如何在需求变化中减少工作量?
一个可培育和发展的软件中,需求总是会发生变化,而且可能变化量还可能会比较大。这就需要把变化当成重要的设计主题来看待,需要拥抱着变化而设计。问题在于,这种基于变化来设计会影响开发效率吗?
在传统的设计中,需求变更无疑会大幅度增加工作量。但是,有经验的开发人员都知道,在现有系统中增加新功能的时候,主要的成本往往不是在编写新的代码,而是要处理原有系统中的各个组成部分紧密耦合的关系。在代码的某个地方修改了一个部分,却对代码的其他地方造成了意想不到的影响。在应对需求变化、维护、调试中所耗费的大多数时间,并不是花在修改错误上,而是花在寻找和弄清如何避免在修改代码时导致不良副作用上了。
3)保持关注点分析
这样一来,就形成了一个关键的解决方案:保持关注点分离。
软件中总是会发生变化。无论何种变化都或者会影响对象、服务的用户,或者影响实例化的对象。保持关注点分离,就可以减少这种相互间的不良影响,从而从根本上提高开发效率。问题是关注点又是什么?我们需要分离什么?怎样正确的分离?这就对面向对象分析(OOA)提出了新的要求,除了传统的功能分析以外,还需要对共性和变化性进行分析,没有这种良好的分析谈何正确的设计?
二、关注特征:共性和可变性分析
1,除了功能之外,我们还需要考虑什么?
1)关注共性和变化性
如何正确分配职责呢?在面向对象的理念中,除了功能之外,对职责的正确分配还来自于对现实世界的问题的正确理解和分类。因此,我们需要一种方式,首先弄清有些什么东西,然后再尝试找到它们之间的关系。在自然科学中,人们在长期的总结中,形成了一套行之有效的分类方法,例如在生物学中,人们抓住了两个问题:
l 共性(是一种抽象,概念):它们是动物,特征与植物不同;
l 变化性(是一种具象,行为):每种动物都有自己区别于其他动物的特征。老鹰(会飞翔的肉食动物)、狮子(会行走的肉食动物)、麻雀(会飞翔的食草动物)和牛(会行走的食草动物)。
在科学领域,正是依靠这种对共性和变化性的理解,来对千变万化的自然界分类的,一般来说共性分析与(问题领域的)概念是互相关联的,而可变性分析与(特定情况的)实现是互相关联的。面向对象的分析与设计,正是归纳了人们对于自然科学的理解而发展起来的。
2)共性和变化性分析
共性和变化性分析(Commonality and Variability Analysis , CVA)是面向对象的分析最重要的内容之一。这种分析告诉了我们如何在问题领域中找到不同变化,以及如何找到不同领域中的共同点。首先需要通过共性分析找到“变化的地点”,然后再通过变化性分析找出“如何变化”。这样我们就可以正确按照接口编程,并使用聚集封装变化,获得灵活和易于测试的设计得以实现。
在这个背景下面向对象的方法建议:
l 首先:使用 CVA 找到问题域中存在的各种概念(共性)和具体的实现(可变性)。这时我们最感兴趣的是找到其中的概念。
l 其次:这一过程中也会发现许多可变性。问题域中任何没有包含在这些概念中实体(比如可能有一些属于“某种”对象的对象)也应该找出来。
l 最后:在所需功能的概念都找到之后,继续为封装这些概念的抽象制定接口。接着,考虑你将如何使用从这个抽象派生出具体实现。
这基本上就遵循了基于背景进行设计的方法。通过定义这些接口,还确定了哪个对象使用哪些对象,从而非常自然地完成了设计的规格说明。
2,共性和变化性分析
1)传统思维与现代思维
传统上的功能分析可能也会关注哪些是会导致重新设计的原因,但基本的目标还是一个稳定的需求和设计。但是在面向对象的设计理念中,一开始就把变化作为重要的设计要素来考虑,这是软件分析与设计哲学上的一个很大的不同。
首先,在领域概念分析中,我们通过发现问题域中的术语(名词),创建概念模型来表示它们。然后找到与这些名词相关的动词,并通过在对象中添加方法来实现这些动词。这只是一个初步的分析。再进一步更深入的分析中,我们就需要以变化为主题,通过强调共性和可变性分析,使分析与设计逐步走向深入。
2)站在不同的视角观察变化
下面这张图反映了这种基本的设计思路,它显示从分析到设计的自然过渡:
l 从共性和可变性分析入手;
l 关注概念视角、规约视角和实现视角三大问题;
l 定义抽象类及其接口和抽象类的派生类来完成设计。
从上图可以看到,共性分析与问题领域的概念视角是互相关联的,而可变性分析与(特定情况的)实现是互相关联的。规约视角则位于其中,共性和可变性都要涉及这个视角。规约描述了如何与一组概念上相似的对象通信。这些对象每一个都代表了公共概念的一种变化。在实现层次上这个规约将成为抽象类或者接口。
3)抽象类的意义
当我们这样来看待设计的时候,就可以描述出在面向对象理念中抽象类的意义。
使用抽象类进行共性分析的好处 |
|
与抽象类的对应关系 |
描述 |
抽象类与核心概念 |
抽象类代表了将所有派生类联系起来的核心概念,正是这个核心概念定义了派生类的共性。 |
共性与要使用的抽象类 |
共性定义了需要使用的抽象类。 |
可变性与抽象类的派生类 |
在共性中辨别出的可变性将成为抽象类的派生类。 |
规约与抽象类的接口 |
这些类的接口对应于规约层次 |
这样一来,类的设计过程就简化成了两个步骤,如下表所示。
设计的两步法 |
|
定义 …… 时 |
必须问自己 …… |
抽象类(共性) |
需要用什么接口来处理这个类的所有责任? |
派生类(可变性) |
对于这个给定的特定实现(这个变化),应该怎样根据给定的规约来实现它? |
l 规约视角和概念视角之间的关系在于:规约标识了用来处理此概念所有情况(即概念视角所定义的共性)所需的接口。
l 规约视角和实现视角之间的关系在于:对于给定的规约,怎样实现这个特定情况(这个变化)?
4)基于接口进行设计
有趣的是,遵循“按接口设计”的做法,找出变化之处,代码封装在定义明确的接口之后,从而使代码高度内聚(同样的代码不会出现在两个地方)而且避免了耦合,这正是消除冗余代码所需要的。强内聚而且“按意图编程”,又使代码具有更好的可读性,这也是优秀代码的一种必需品质。
重要的是:必须考虑你的设计中哪些地方可能变化,不是考虑什么会迫使你的设计改变,而是考虑怎样才能够在不重新设计的情况下进行改变。这就形成了一种“封装变化”的设计理念,换句话说,面向对象的设计就是:“发现变化并将其封装起来。”
三、分析矩阵:与客户一起讨论变化
多年来与客户打交道的经验使我们懂得了以下几点:
l 他们通常非常了解他们的问题域(大多数我永远也赶不上);
l 一般情况下,他们不会像我们那样在概念层次表达事情。相反,他们会谈得非常具体。
l 他们经常用“总是”表达“通常”。
l 他们经常用“从不”表达“很少”。
总之,对于非常具体的问题,客户详细的回答一般是可信的,但是他们一般性的回答却不可信。在与客户沟通的时候也不可能循着一个问题表一步步的表达。除了那些最简单的问题,大多数的交流都是散乱而无规律的。因此,我们必须使用某种模型来与客户来沟通变化的特征,这种模型必须是容易理解而且问题集中的。更重要的,这种模型还需要能够激起客户对这个问题的兴趣,采用分析矩阵来与客户一起分析共性与变化性问题,是一个比较好的选择。
1,案例的背景
面向对象的分析并不是指的使用某种面向对象的模型,更重要的是要用面向对象的思想表述问题,下面我们通过一个案例来说明这种方法的应用,这个案例的背景如下。
一个美国某国际电子商务公司的订单处理系统。假设系统必须能够处理来自不同的国家(地区)的销售订单。
最开始要求很简单:处理美国和加拿大的订单。
系统的需求清单如下:
l 要为加拿大和美国构建一个销售订单系统。
l 根据所在国家计算运费。
l 运费还应该以所在国家(地区)的货币支付。
l 在美国,税额应按当地计算。
l 使用美国邮政规则验证地址。
l 在加拿大,使用联邦快递发货,同时缴纳联邦政府销售税(GST)和地方销售税(PST)。
尽管这个需求已经很清楚,但我们必须通过分析使这个需求变得清晰,分析:
1)共性:
这是一个销售订单系统,具备国际上任何销售系统所具备的共同特征。
2)变化性:
这些需求又可以分成两种情况:
l 顾客在美国;
l 顾客在加拿大。
这个问题中存在的变化并不太复杂。光凭观察,就可以显而易见得到下面的表。
因顾客所在地不同的两种情况 |
|
情况 |
过程 |
美国 |
根据 UPS 费用计算运费 使用美国邮政规则验证地址 按当地计算销售额和/或服务的税额 用美元处理金额 |
加拿大 |
用加拿大元处理金额 使用加拿大邮政规则验证地址 通过联邦快递发货到加拿大 按加拿大省的税收规则计算销售额和/或服务的税额(使用GST和PST) |
这是一个简单的问题,但是我们希望用这个简单的背景来说明分析矩阵的分析过程,而不希望把精力过多地放在案例的背景上。
2,在填写分析矩阵中进行分析
如果情况很复杂,例如有成百上千种情况,几十种变化。我们就可以发现自己当时甚至无法与项目的客户和其它相关人员交谈,因为信息实在是太多了。这就必须提出一种组织海量数据的新方式。使用分析矩阵不仅帮助我们理解问题域,而且有助于实现它。
让我们从观察一种情况开始。首先观察必须实现的每个功能,并标记它所表示的概念。每个功能将分行写出,这一行的左边写上它所表示的概念(共性),然后描述相应的实现(变化性),据此一步一步地给出整个分析剧矩阵。如下表所示。
填写分析矩阵:完成第一种情况(美国销售) |
|
概念 |
美国销售 |
计算运费 |
按照 UPS 费率 |
验证地址 |
使用美国邮政规则 |
计算税额 |
使用美国和当地的税收规则 |
金额 |
美元 |
3,继续处理其他情况,按需要扩展这个矩阵
现在转到下一种情况和其他情况,按需求所提供的顺序进行。每种情况一列,根据已有的所有信息完成每个单元格。但是需要把新情况与正在处理的概念进行比较,无需对各种情况进行比较。记住这一点很重要,需要考察的是这些新情况,对最左一列中己经找到的概念是如何进行处理。还应该记住,构建矩阵时首先应该考察从 CVA 得到的关系,这时并不需要寻找其他关系。
我们来填表,首先添加了针对加拿大情况的一列,如下表所示。
下一种情况的分析矩阵:(加拿大销售) |
||
概念 |
美国销售 |
加拿大销售 |
计算运费 |
按照 UPS 费率 |
|
验证地址 |
使用美国邮政规则 |
|
计算税额 |
使用美国和当地的税收规则 |
|
金额 |
美元 |
|
第一条需求是:用加拿大元处理金额。应该放在哪一行呢?
我认识到计算运费时需要使用加拿大元,但是加拿大元并不是“计算运费”的一种,因此不应该放在这一行。加拿大元和“验证地址”之间看不出任何关系,所以再看下一行。“计算税额”与“计算运费”的情况相同,有关系,但不是 CVA 关系。最后一行是“金额”,加拿大元正是共性“金额”的一种变化,就是这里了。如下表所示的结果。
下一种情况的分析矩阵:(加拿大销售) |
||
概念 |
美国销售 |
加拿大销售 |
计算运费 |
按照 UPS 费率 |
|
验证地址 |
使用美国邮政规则 |
|
计算税额 |
使用美国和当地的税收规则 |
|
金额 |
美元 |
加拿大元 |
重复这一过程,直到填完如下表所示的表格。
下一种情况的分析矩阵:(加拿大销售) |
||
概念 |
美国销售 |
加拿大销售 |
计算运费 |
按照 UPS 费率 |
按照联邦快递费率 |
验证地址 |
使用美国邮政规则 |
使用加拿大邮政规 |
计算税额 |
使用美国和当地的税收规则 |
使用GST和PST |
金额 |
美元 |
加拿大元 |
当然,事情并非总是如此顺利。新情况往往会带来新功能。但是这是好事情。这样我们就能够有机会检验分析的完整性。在多年与客户交流的经验都表明:他们通常无法提供完整的需求,因为他们总是按正常情况考虑问题,忽视异常情况(而我们却是必须要处理的)。
4,在分析过程中寻找不完整和不一致
在构建矩阵的过程中,我可能会发现需求中的遗漏。我将使用这些信息扩展分析。这些不一致说明客户提供的信息不完整。也就是说,在某种情况下某个顾客可能提出某种特殊需求,而另一个顾客则不会。
例如,在来自美国市场的需求中,没有提到最大重量的问题,而加拿大市场可能有最重 31.5 千克的限制。通过比较这些需求,我们有必要找到美国客户的联系人,专门询问她重量限制的问题(实际上可能并不存在此限制),然后补上这一漏洞。
随着时间流逝,我们总是需要处理新的情况(例如业务扩展到了德国)。当你发现了某种情况中存在一个新概念时,就扩展分析矩阵,在矩阵中增加一行,即使它并不适用于其他情况。如下表所示。
扩展分析矩阵 |
|||
概念 |
美国销售 |
加拿大销售 |
德国销售 |
计算运费 |
按照 UPS 费率 |
按照联邦快递费率 |
按照德国货运公司费率 |
验证地址 |
使用美国邮政规则 |
使用加拿大邮政规 |
使用德国邮政规则 |
计算税额 |
使用美国和当地的税收规则 |
使用GST和PST |
使用德国增值税 |
金额 |
美元 |
加拿大元 |
欧元 |
日期表示 |
mm/dd/yyyy |
mm/dd/yyyy |
dd/mm/yyyy |
最大重量 |
|
|
30千克 |
美国和加拿大有最大重量吗?可能没有,也可能有,但是客户忘了提到这一点。现在,有一个很好的具体问题要问了。我的客户对于回答具体问题是非常擅长的,而对于“还有别的什么吗?这样的问题,他们一般并不擅长。
5,用行来发现规则
现在概念以及其实现上的变化性已经找到,对这些己知信息应该怎么处理呢?
观察上面表中的矩阵。第一行标记为“计算运费”,包括“按照 UPS 费率”、“按照联邦快递费率”和“按照德国货运公司费率”。这一行表示:
l 实现“计算运费费率”的一般规则。
l 必须实现的具体规则集:也就是在不同国家(地区)使用的货运公司。
实际上,每一行都表示实现一个一般规则的特定方式。其中有两行(金额和日期)可以在对象层次上处理。例如,金额可以用包含货币对象的对象来处理。日期可以利用计算机语言库中的不同日期格式。
首先确定处理每一行概念的基本方式,把这些设计策略记录下来,如下表所示。
扩展分析矩阵 |
||||
概念 |
美国销售 |
加拿大销售 |
德国销售 |
基本方式 |
计算运费 |
按照 UPS 费率 |
按照联邦快递费率 |
按照德国货运公司费率 |
计算运费费率的各种方式的具体实现 |
验证地址 |
使用美国邮政规则 |
使用加拿大邮政规 |
使用德国邮政规则 |
验证地址的各种方式的具体实现 |
计算税额 |
使用美国和当地的税收规则 |
使用GST和PST |
使用德国增值税 |
计算应付税额的各种方式的具体实现 |
金额 |
美元 |
加拿大元 |
欧元 |
使用包含Currency字段和Amount字段的Money对象,可以自动兑换货币 |
日期表示 |
mm/dd/yyyy |
mm/dd/yyyy |
dd/mm/yyyy |
使用包含display方法的Data对象,可以根据顾客所在国家(地区)的要求显示日期 |
最大重量 |
|
|
30千克 |
最大重量的各种方式的具体实现 |
6,用列发现实现上的区别
那么列又表示什么呢?它们是针对所表示的情况的特定实现。如下表所示
扩展分析矩阵 |
||||
概念 |
美国销售 |
加拿大销售 |
德国销售 |
基本方式(行) |
计算运费 |
按照 UPS 费率 |
按照联邦快递费率 |
按照德国货运公司费率 |
计算运费费率的各种方式的具体实现 |
验证地址 |
使用美国邮政规则 |
使用加拿大邮政规 |
使用德国邮政规则 |
验证地址的各种方式的具体实现 |
计算税额 |
使用美国和当地的税收规则 |
使用GST和PST |
使用德国增值税 |
计算应付税额的各种方式的具体实现 |
金额 |
美元 |
加拿大元 |
欧元 |
使用包含Currency字段和Amount字段的Money对象,可以自动兑换货币 |
日期表示 |
mm/dd/yyyy |
mm/dd/yyyy |
dd/mm/yyyy |
使用包含display方法的Data对象,可以根据顾客所在国家(地区)的要求显示日期 |
最大重量 |
|
|
30千克 |
最大重量的各种方式的具体实现 |
具体实现规则:列 |
当我们有美国顾客时,使用这些实现 |
当我们有加拿大顾客时,使用这些实现 |
当我们有德国顾客时,使用这些实现 |
|
例如,第一列说明了用于处理美国销售订单的具体实现。
然后我们再通过观察行,分析和确定设计策略:
例如,第一列说明了用于处理美国销售订单的具体实现。应该怎样把这些深入认识转化成设计策略呢?
对上表进行观察(重点是观察行),分析相关概念的特点,每一行都表示实现最左列中概念的特定方式。例如:
l 在“计算运费”行中,“按照 UPS 费率”、“按照联邦快递费率”两项实际上表示“应该怎样计算运费”。所封装的算法是“运费费率计算”。具体的规则将是不同的“UPS费率”、“加拿大费率”和“德国费率”的计算方法。
l 下两行也是由不同规则及其相关具体实现组成的。
l “金额”和“日期”两行表示可能在整个应用程序中保持一致的类,算法上是可以共享的,但是需要根据国家(地区)的不同表现也不同。
因此,除“金额”和日期”两行之外的行可以考虑把业务按照不同的变化整体上封装起来。例如,第一行的对象可以实现为分别封装“计算运费”规则的策略。如下表所示。
实现策略 |
|||
概念 |
美国销售 |
加拿大销售 |
德国销售 |
计算运费 |
这一行的对象可以用分别封装“计算运费”规则的策略 |
||
验证地址 |
这一行的对象可以用分别封装“验证地址”规则的策略 |
||
计算税额 |
这一行的对象可以用分别封装“计算税额”规则的策略 |
||
金额 |
使用统一的包含Currency字段和Amount字段的Money对象,可以自动兑换货币 |
||
日期表示 |
使用统一的包含display方法的Data对象,可以根据顾客所在国家(地区)的要求显示日期 |
||
最大重量 |
这一行的对象可以用分别封装“计算最大重量”规则的策略 |
以类似的方式,再来观察列。弄清楚每一列描述了哪种情况用哪一个规则?这些项表示该情况需要的对象系列。每一行代表一个概念,其接口应该是相同的,但具体实现可以各不相同。
7,子矩阵
有时候一个简单的矩阵往往是不够的,即使在这个简单案例中,也很容易想像,如果一个国家(地区)中有超过一种货运方式会变成什么样子。这就可以在主矩阵中再加入子矩阵,下图给出了这个例子。
内嵌的子矩阵使关系变得复杂,实际上只是在无法直接得到“共性与可变性表”的时候,才使用矩阵中的矩阵。上图中的内嵌矩阵对应的表如下表所示。
内嵌矩阵定义的共性与可变性 |
|
计算运费共性 |
取货附加费共性 |
UPS 费率 |
无 |
USPS 费率 |
5 美元 |
联邦快递费率 |
4 美元 |
验证地址共性 |
最大重量共性 |
美国邮政规则 |
50 磅 |
美国邮政规则(无邮箱时) |
70 磅无 |
|
无最大限制 |
8,得到高层设计
1)封装创建一组相关的对象
还要注意到另外一种变化:由于美国、加拿大、德国这些地域的变化,会成组的使用不同对象。这就需要确定在什么情况下使用哪一组对象是相同的。从封装变化的角度来说,设计需要将“使用哪些对象”的规则与“如何使用这些对象”的逻辑分离开来。
具体的是提供一个接口,来创建一组相关或相互依赖的对象,但无需指定它们的具体类是什么,这种设计策略可以给它起个名字:Abstract Factory(抽象工厂,AF)。
2)把设计策略变成结构
这样一来,就得到了一个初始的高层设计,如下图所示。
3)表现类之间的消息传递
各个类之间更详细的信息流动关系,可以用序列图来表达,美国关系的序列图如下图所示。
如果这种解决问题的设计方案被证明是有效的,就可以把它记录下来,并赋予一个唯一的名称,这就成为模式,成为组织的智力资产。
4)可扩展性的问题
这个分析矩阵的共性和变化性的分析,使设计结果具有很强的可伸缩和可扩展性,而且可以应对变化。例如,如果在我们的电子商务系统中加入了打印销售票据的需求,并发现如下变化:
l 美国销售票据需要表头。
l 加拿大销售票据需要表头和页脚。
l 德国销售票据需要两个不同的页脚。
我可以再加上一行(增加了一个概念)包含这些信息,其中每列与一种销售票据的格式相关。这一行的需求将使用保持核心业务不变,但封装了前后业务方法来实现,这样就能比较容易地改变所用的页脚和表头(以及它们的顺序)。
9,问题越大,分析矩阵越有用
尽管分析矩阵并不能解决软件分析上的所有问题,但它至少可以用于大多数问题域,特别是在特殊情况太多,我的脑子无法得到总体视图时最有用。
分析师与客户交流的时候,当需求不能以非常协调的形式陈述给分析师或开发人员,人们对所涉及的所有东西无法在概念上很好地把握,他们只是谈论一般规则和例外情况,其中显而易见的存在大量遗漏。这时我们该怎么办?是怨天尤人?还是一味的责怪客户?
现在我们可以利用分析矩阵把大问题变成小问题。在这种情况下,对于一个特性,查看最左一列,看这个特性是哪个概念的一种变化。如果找到了这个概念,就将这个概念放入那一行中。如果找不到这样的概念,就说明必须创建新的一行。以此逐步的使混沌逐步走向清晰,这也是领域建模的一种方式。
概念中的变化可能是分析和设计所面临的最大挑战之一,这也称为特征分析。通过分析矩阵这种工具,有助于我们发现和弄清这些变化的意义,为良好的设计打下基础。虽然这种工具对封装变化很有用处,但它只是众多方法中的一种,并不能靠它捕获设计的所有特性。从方法论来说和设计一样:“合适的往往就是最好的”。
四、在行为分析中发现共性和变化性
行为分析主要考虑在一个事件触发下的业务流程,而流程分析可能是软件领域最古老、最传统的方法了。但是如果在行为分析的时候,对行为适当的抽象,针对行为的共性和变化性进行深入分析,这就使分析思维上升到了面向对象分析(OOA)的角度,形成了现代领域特征分析方法。几乎任何功能分析都需要进行行为分析,但是领域特征分析主要关注在一个领域中的行为共性和变化性
1)领域行为分析的关注点
领域特征分析主要工作是识别功能具有的行为特点。这个阶段建模首先是发现事件,再考虑在事件触发下业务流程(活动)的描述,以可视化的方式把业务的行为展现出来,这也称之为用例分析,具体包括:
l 分析功能执行的前期行为特征,如功能执行的前置条件、前期准备工作等;
l 分析功能主体行为的特点,发现其具有的显著特点和可能的变化性;
l 分析功能的后期行为特点,如功能执行的后置条件、善后处理工作、功能执行完毕后的控制权转移等。
l 分析与寻找变化点,分离共性的行为和变化的行为,而确定变化行为的位置,并且建立这两种行为之间的约束,成为发现“特征”的关键。
需要为每一种行为特点建立领域内一致的名称和说明,并将其放入特征模型的行为特点层,建立与功能层特征的整体部分关系。
2)对行为的共性进行抽象
在进行领域行为进行共性和变化性分析的时候,我们首先应该注意在整个领域中寻找业务流程的共性。很多业务流程一旦抽取掉了行业特征,就可以发现其中的共性。举个例子:
假定目前的工作是关于一个图书馆的系统,在上下文范围中几乎肯定有这样的业务事件:图书馆用户希望续借图书。下图展示了对该事件的系统响应流程,当一个图书馆用户提交了一个续借请求的时候,系统的响应要么是拒绝续借,要么是批准续借。
在图书馆这个行业特征上的工作导致了一份详细的需求规格说明,可用于创建一个特定的产品。这项工作的一项副产品是识别了某些有用的业务流程,这些都可以在图书馆行业的其它项目中复用。但是,复用的范围能不能扩大?
现在想象目前的工作是“卫星广播”这个极不同的行业特征,例如。这个上下文范围中有一个业务事件是“卫星广播希望对许可证续约”。当卫星广播者提交了广播许可证请求之后,系统的响应要么是拒绝该请求,要么是提供新的许可证。
当你站在更高的角度审视这两个不同的行业项目,就会发现在为卫星广播项目与图书馆项目相似的业务流程,有可能在这些相似项目中复用。
第一眼看上去,图书馆续借和卫星广播许可证续约对事件的响应很不一样,它们的不同之处是它们来自于不同的行业。但是再仔细研究它们的响应,就会发现它们之间有比较大的相似之处,如果发现了相似之处,我们就有机会导出更抽象的业务模型,就可以在很多不同行业软件中复用。
“续借”和“广播许可证”都是要续约东西,但其中有很大的相似之处:每件要续约的东西都有唯一的标示符、标准的续约周期以及续约费用。下图是我们对这两个业务事件的处理策略进行抽象所得到的结果。我们使用抽象来确定共同的特征,这意味着透过事物的表象来发现有用的相似之处或者分类。它也意味着可以忽略一些特征以发现共同之处。
忽略实际物品和主题行业,我们就可以把注意力集中在两个不同系统所执行的底层操作上,我们这样就可以发现相似之处,也就是一种共性,以便于加以利用。
发现和确定共性需要分析师具有一些独特的能力:
l 从不同的抽象层次来看待工作的能力;
l 按不同的方式进行分类的能力;
l 发现望远镜与注满水的玻璃半球都是放大镜的能力;
l 指出显然不同的事情之间相似之处的能力;
l 以抽象的方式来看问题的能力。
3)对行为的个性进行变化分析
进一步想,事实上图书馆借书和卫星广播还是不同的,它们不同的点到底在哪里呢?在这些点上我们如何实现这种变化呢?这些变化部分如何与其共性清晰分离呢?走到这一步,一个特定领域分析已经开始了走向深入了。
通过这个案例,我们进行了领域行为共性与应用个性(变化)的分析,这种分析的翔实准确,使得共性和变化性可以清晰的分离,这就为开发可复用可插拔的软件架构提供了坚实的基础。
关注共性和变化性,这本身也是个重要的工作方法。这个世界没有什么是完全不同的,总结了共性,就可以减少很多重复劳动,提高理解问题的效率。这个世界也没有什么是完全相同的,总结了个性或者变化性,就可以使我们的产品有特点,理解更深入,更能够解决具体问题。
五、对变化建模
在上述关于领域行为共性和变化性的分析中,我们从概念的视角发现了业务流程的领域共性,又从规约的视角发现了变化点。但是从规约和实现的视角来看变化,这个分析还是很粗糙的。变化性对于设计如此重要,以至于提出了对于变化性本身进行建模分析的要求。一旦发现了变化点,变化性建模的一个目标就是知晓变化性,并且有意识的去解决它。下面我们将讨论与此相关的问题。为了正确地建模,必须定义与模型相关的基本概念,再从基本概念引申到建立基础模型。
1,变化主题和变化对象
1)可变性管理
在传统的产品设计中,处理可变性往往是一项辅助性的工作,但在现代软件设计思想中,研究可变性问题成为一项主要和关键性工作。正是这样一个背景,我们的需求分析、项目管理以及设计思想就需要在一个大思维框架下统一起来。我们把分析中识别和描述可变性的活动称为“识别可变性”,而在不同阶段是用“可变性管理”来进行支持的,它包括:
l 支持与定义可变性活动。
l 管理可变性交付物。
l 为解决可变性相关的活动提供支持。
l 收集、存储和管理完成这些任务所必需的追踪信息。
在整个产品设计过程中,都需要导入可变性问题,因此必须以一致的方法保证这些变量被正确的构建。在现实中可变性解决的时刻被称为可变性绑定时间,但绑定时间并不是可变性建模的关注点。为了增加灵活性,也可能会把可变性绑定时间推迟到实现的后期阶段。
2)需要思考的三个基本问题
所谓可变性,指的是变化的能力和变化的趋势。换句话说,我们关注的可变性并不是偶尔发生的,而是有目的地产生的。通过仔细思考下面三个基本问题,有助于我们定义产品线可变性问题:
l 第一个问题是“什么在变”:回答这个问题就可以准确地识别现实世界中的变化项和属性,这就是变化的主题。
l 第二个问题是“为什么变”:主题的变化原因很多,例如利益相关者不同的需求、不同国家的法律以及技术原因,而且当变化项存在依赖关系的时候,一个项的变化很可能是由于另外一个项变化引起的。
l 第三个问题是“怎么变”:这体现了变化的主题的不同形态,从而可以定义变化对象。也就是说,变化对象是变化主题的一个实例。
通过考虑这三个问题,就可以改变人们对可变性的思考方式。
2,变化点与变量
为了正确的进行可变性建模,我们首先需要对模型中的元素进行定义。
1)变化点
变化点代表了领域交付物内部的变化主题(什么在变?),也代表了现实世界中所有可能的变化主题的一个子集。变化点同时也包含了上下文信息,例如:引入变化点的理由等。
2)变量
变量代表了领域交付物内部的变化对象(怎么变?)。
一个变量对应着变化点的一个候选项,并且可以与其它交付物建立联系,这就表明了这个交付物与对应的特定选项有关。
3)定义变化点和变量
为了定义变化点和变量,我们可以通过下面三个步骤来系统性的识别变化:
l 第一步:识别现实世界中的变化项:例如系统组件中的几种不同的通讯方式,例如有线LAN、无线LAN、蓝牙等,这就产生了一个变化主题“网络类型”。
l 第二步:在产品上下文环境中定义变化点:现实世界中的可变性(变化主题)与产品中的可变性(变化点)是不同的。例如,一个变化点意味着有多种需求可供选择,也意味着可能产生不同的应用。例如第一步定义的“网络类型”变化主题,由此形成的变化点是“自定义系统网络”,它表明产品必须适应不同网络类型,但并没有指明究竟是其中哪一种网络类型。
l 第三步:定义各种变量:为此,需要从已经识别的变化主题中选择一些变化对象,把它定义为变化点的变量。例如对于“自定义系统网络”变化点,把如何用统一的方式处理有线LAN、无线LAN、蓝牙等方式,这样一来工作效率要高的多。
2,时间、空间、内部与外部可变性
对可变性的进一步研究,还需要考虑可变性的各种类型。
1)时间可变性
时间可变性指的是一个交付物在不同的时间的不同版本。这是因为软件产品随时间的进化所造成,在软件产品中,可以使用预测与调整把变化点标出一些预先定义的位置,在这些位置上可以相对容易的引入变化。
例如,一个“门锁识别机制”,当时的技术是“磁卡”,但我们预测还会有“指纹扫描”技术出现,这可以把它定义为一个变量,一旦出现这种技术,可以很容易地加入它。
2)空间可变性
空间可变性指的是一个交付物在同一个时间以不同的形式存在。例如,某个系统的变化点“系统访问方式”,可以分成三个变量“浏览器”、“移动电话(短信)”与“固定电话(语音)”,他们需要用一致的方式集成到系统。
3)外部可变性
外部可变性指的是领域架构对于直接用户可见的可变性。由于是对用户可见的,他们可以直接或者间接的决定每个变量是要还是不要,也可以由产品经理通过选择变量来定义不同的应用,客户可以从这些应用中选择。
4)内部可见性
内部可见性是领域架构中隐藏的可变性。这种变量的选择与客户无关,而是由应用开发来决定如何处理可变性问题。例如,通讯系统不同通信协议的转换。客户通常对如此细粒度层次的需求并不感兴趣,也没有必要让客户了解不同的实现方法。
3,变化性建模的模型骨架
对变化建模分析可以通过面向对象分析(OOA)来达到,最常用的是使用概念模型,也称之为变化分析的元模型。在具体变化问题分析中,可以进一步细化某些内容(比如具体的属性)。
1)模型的骨架
作为这个模型的骨架,所关注的就是:变化点、变量和可变依赖性这三个大问题。首先必须定位变化点与变量之间的关系,也就是建立这两个概念高层的基础模型,被称之为可变性元模型。
2)第一个概念,“变化点(什么在变?)”:
这是一个抽象类,其属性描述了具体的变化点及其它细节。
它被具体化为“内部变化点”和“外部变化点”两个类。这两种“变化点”是互斥的,也就是说对于任何一个具体的变化点,要不就是“内部变化点”,要不就是“外部变化点”。两种变化点具有不同的语义、内涵。内部变化点的变量对于用户不可见,而外部变化点对于用户和应用开发人员都是可见的。
3)第二个概念,“变量(怎么变?)”:
它代表了领域交付物内部的变化对象,其属性描述了变化对象的各种细节。
4)第三个概念,变化点与变量之间依赖:
由于“变化点”与“变量(怎么变?)”之间是多对多关系,这就必须引进一个关联类,称为“变化依赖”,以保证这种关系得以实现。
关联类中至少需要两个属性来建立这种联系,每个属性需要与“变化点”与“变量”变量之间的一个属性相容。这种关联的多样性必须满足下列条件:
l 每个变化点至少与一个变量相关联。
l 每个变量至少与一个变化点相关联。
l 一个变化点可以提供多个变量。
l 一个变量可以与不多个不同的变化点相关联。
5)“必选与可选”:
“变化依赖”定义成了一个抽象类,其具体化为一个“必选”关系和一个“可选”关系,这些具体化是完整而且互斥的。
“可选”的变化依赖指的是一个与变化点相关的变量,是某个具体产品的特定部分,但不是必须的部分。例如:“键盘”、“磁卡”、“指纹扫描”是用户可选的应用,但也可以不选。
“强制”的变化依赖指的是一个与变化点相关的变量,这个变量是这个变化点所必需选择的,但并不意味着这个变量必须包含进产品线所有的应用中去。例如:对远程通讯加密,128位加密是强制变量。同时,也定义了256位、512位、1024位的可选变量。在用户不做选择的时候,自然认为选择了128位加密。但用户选择512位加密时,并不等于一定要128位,它只是远程加密应用的一部分。
这个模型只是考虑了“变化点”与“变量:之间的一般关系,具体问题还需要仔细考虑每个类的具体属性,以这个模型为基础,就可以分析很多关于变化的具体问题了。
我们必须注意到:一个事情要成功,关键是要有两个作用力,一个是思考力,另一个是执行力。对于架构师来说思考力是最重要的,因为如果思考是错误的,那么执行越坚决,最后错误就越大。设计源自于分析,分析的头脑关键在于在纷乱复杂的世界中迅速抓住本质,迅速理出线条,迅速捕获重点。无论从任何一方面来说,一个优秀的人才首先应该是有思考力的人才。