架构师面向对象分析系列文章(开篇)
架构师面向对象分析系列(一)-OOA定义用例,看明白需求
架构师面向对象分析系列(二):OOA定义领域模型,快速熟悉

架构师面向对象分析系列(三):OOD定义交互图,理清职责和协作
架构师面向对象分析系列(四):OOD定义设计类图,构建软件世界
架构师面向对象分析系列(五):OOP选择语言,进行代码实现
架构师面向对象分析系列(六):Grasp、Solid和GOF那些事
丰富多彩的软件世界里面,交织着五彩斑斓的类,简单来看,对象主要有:
来源于领域对象:领域对象更侧重于业务领域的抽象和描述,强调问题领域中的概念、关系和行为,它是从业务需求出发的。来源于数据对象:更关注数据的组织和结构,描述数据在计算机系统中的存储方式,它是从数据管理和系统实现出发的。比如数据库存储对象,缓存对象、搜索doc对象等等。行为和辅助对象: 为了实现特定的功能,满足低耦合高内聚而创建的一些功能性辅助类。与领域模型表示的是真实世界的类不一样的,设计类图表示的软件世界的类,真实世界类并不一定等价于软件世界的类,但能启发软件设计类的设计,比如其中的某些类名和内容还是非常相似的。从这一方面讲,oo设计和语言能够缩小软件架构和我们所设想的领域模型之间的差异,即实现低表示差距(lower representational gap)
1. 基于UML表示类除了在交互图中显示对象协作的动态视图外,还可以用设计类图来有效地表示类定义的静态视图,这样可以描述类的属性和方法。
当我们绘制交互图时,在此动态对象建模的创造性设计过程中会产生一组类及其方法,他们的对应关系如下图。
这也就是为什么定义交互图是那么重要。
因此,类图的定义能够从交互图中产生,虽然这表明一种线性的顺序,即先绘制交互图,再绘制类图。但是在实践中,尤其是应用了并行建模的敏捷建模实践后,这些互补的动态视图和静态视图是并行创建的。
下面是UML绘制类图场景的使用方式,更多可以参考
2. 认识职责与职责驱动
在 RDD 中,我们认为“软件对象具有职责”,这个定义很符合人在社会群体中分工协作的方式,软件也是人编写的,所以根据职责思考设计的软件系统符合人的行为习惯,同时更易于理解和管理。看亚当史密斯提出的劳动分工的重要性,软件对象亦是如此。
理解职责是顺利进行面向对象设计的关键,决定方法归属于哪个对象和对象之间如何交互,其意义重大,应谨慎从事。掌握OOD涉及一套柔性原则,自由度很大,这正是OOD的复杂所在。但OOD并不是魔术,其模式是可以命名(重要!
)、解释和应用的。例子可以帮助我们,实践可以帮助我们。
Uml与设计原则,由于UML只是一种标准的、可视化建模语言,了解它的细节并不能教会你如何用对象思想来思考,而对象思想正是最重要的主题。UML有时候被描述成一种“设计工具”,但是这并不完全正确……最关键的软件开发工具是受过良好设计原则训练的思维,而不是UML或任何其他技术。
在绘图(和编写代码)活动当中,我们要运用各种OO设计原则,如GRASP和GoF(四人帮)设计模式。OO设计建模总的来说,基于职责驱动设计(RDD)所代表的内在含义是考虑怎样给协作中的对象分配职责。
在RDD中,我们认为软件对象具有职责,即对其所作所为的抽象。UML把职责定义为“类元的契约或义务”。就对象的角色而言,职责与对象的义务和行为相关。职责分为以下两种类型:行为和认知。
对于软件领域对象来说,由于领域模型描述了领域对象的属性和关联,因此其通常产生与“认知”相关的职责。
职责与方法并非同一事物,职责是一种抽象,而方法实现了职责。
RDD也包括了协作的思想,职责借助于方法来实现,该方法既可以单独动作,也可以与其他方法和对象协作。
例如,Sale类可以定义一个或多个方法来获取其总额,比如命名为getTotal方法。为了完成该职责,Sale可能与其他对象协作,例如向每个SalesLineItem对象发送getSubtotal消息以获取其小计金额。
RDD是思考OO软件设计的一般性隐喻。把软件对象想象成具有某种职责的人,他要与其他人协作以完成工作。RDD使我们把OO设计看作是有职责对象进行协作的共同体。
GRASP对一些基本的职责分配原则进行了命名和描述,因此掌握这些原则有助于支持RDD。
3. GRASP:给对象分配职责掌握基本对象设计和职责分配需要详细的原则和推理,对这些原则和推理进行命名和解释是可能的。GRASP原则或模式是一种学习工具,它能帮助你理解基本对象设计,并且以一种系统的、合理的、可以解释的方式来运用设计推理。对这种设计原则进行理解和使用的基础是分配职责的模式。一旦理解了这些模式,我们就拥有了讨论设计的丰富、共享的词汇。因为模式的名称能简洁地表达出复杂的设计概念,一个简短的句子也能表达出许多设计信息。例如以下这个句子:“我建议从抽象工厂模式生成策略模式来支持防止变异模式和关于<X>的低耦合”
设计对象交互和职责分配是对象设计的核心。这些设计决策对对象软件系统是否清晰、是否具有扩展性和可维护性具有重大影响,同时也对构件复用的程度和质量具有影响。职责分配可以遵循一定的原则,GRASP模式总结了面向对象设计最常用的原则。
GRASP是通用职责分配软件模式(General Responsibility Assignment Software Patterns)的缩写。之所以选择这个名称,是为了表明掌握(grasping)这些原则对于成功设计面向对象软件的重要性。 GRASP 原则是对其他原则和设计模式的归纳,设计模式有成百上千种,光是记住 GoF 23 种设计模式就已经很困难了,更别提还要记住每种模式的细节,因此需要对设计模式进行有效的归类,GRASP 中的原则描述了模式的本质。这样除了能够有助于加速对详细设计模式的学习之外,而且对发现其根本的基本主题(防止变异、多态、间接性等)更为有效,它能够帮助我们透过大量细节发现应用设计技术的本质。
什么是模式,模式 = 原则 + 惯用方案。经验的OO开发者(以及其他的软件开发者)建立了既有通用原则又有惯用方案的指令系统来指导他们编制软件。如果以结构化形式对这些问题、解决方案和命名进行描述使其系统化,那么这些原则和习惯用法就可以称为模式。简单地讲,好的模式是成对的问题/解决方案,并且具有广为人知的名称,它能用于新的语境中,同时对新情况下的应用、权衡、实现、变化等给出了建议。
“模式”的真实含义是长期重复的事物。设计模式的要点并不是要表达新的设计思想。恰恰相反,优秀模式的意图是将已有的经过验证的知识、惯用法和原则汇编起来;磨砺的越多、越悠久、使用得越广泛,这样的模式就越优秀。
因此,GRASP模式陈述的并不是新思想,它们只是将为广泛使用的基本原则命名并其汇总起来。对于OO设计专家而言,GRASP模式(其思想而非名称)应作为其基础和熟练掌握的原则。这是最关键的!
“模式”具有确切的名称,软件开发是一个年轻领域。年轻领域中的原则缺乏大家广泛认可的名称,这为沟通和培训带来了困难。模式具有名称,例如信息专家和抽象工厂。对模式、设计思想或原则命名具有以下好处:
它支持将概念条理化地组织为我们的理解和记忆。它便于沟通。模式被命名并且广泛发布后(我们都同意使用这个名字),我们就可以在讨论复杂设计思想时使用简语(或简图),这可以发挥抽象的优势职责、GRASP和UML图之间的联系,你可以想一想,在编写代码或建模时,如何给对象分配职责。在UML之中,绘制交互图是考虑这些职责(实现为方法)的时机。
因此,当我们在绘制UML交互图时,就是在决定职责的分配。GRASP中的基本原则,以指导在分配职责时可做的选择。这样,当绘制UML交互图以及编写代码时,你就可以运用GRASP原则了
3.1 创建者(Creator)
在OO设计中,你必须考虑的首要问题之一是:由谁创建对象X?这是行为职责
创建者模式指导我们分配那些与创建对象有关的职责,这是很常见的任务。创建者模式的基本意图是寻找在任何情况下都与被创建对象具有连接的创建者。如此选择是为了保持低耦合。
组合聚集部分,容器容纳内容,记录者进行记录,所有这些都是类图中类之间极为常见的关系。创建者模式建议,封装的容器或记录器类是创建其所容纳或记录的事物的很好的候选者。当然,这只是一个准则。注意,我们在考虑创建者模式时提到了组合(composition)的概念,组合对象是创建其组成部分的良好候选者。
有时,可以通过寻找具有初始化数据的类来确定创建者,这些数据将在创建过程中传递给被创建者。这实际上就是专家模式的例子。初始化数据在创建期间是通过某种初始化方法(如带有参数的Java构造器)来传递的。例如,假定在创建Payment实例时,需要使用Sale的总额对其进行初始化。因为Sale知道其总额,所以它是Payment的候选创建者。
对象的创建常常具有相当的复杂性,例如为了性能而使用回收的实例,基于某些外部特性值有条件地创建一个或一族类的实例,等等。在这些情况下,最好的方法是把创建职责委派给称为具体工厂(Concrete Factory)或抽象工厂(Abstract Factory)的辅助类,而不是使用创建者模式所建议的类。
优点,支持低耦合,这意味着它具有较低的维护依赖性和较高的复用性。这种方法可能不会增加耦合性,因为其所创建的类对于创建者类而言已经是可见的,正是由于存在已有的关联,因此它成为创建者。
相关模式或原则:低耦合,具体工厂和抽象工厂。
3.2 信息专家(Information Expert)
一个设计模型也许要定义数百或数千个软件类,一个应用程序也许需要实现数百或数千个职责。在对象设计中,当定义好对象之间的交互后,我们就可以对软件类的职责分配做出选择。如果选择得好,系统就会易于理解、维护和扩展,而我们的选择也能为未来的应用提供更多复用构件的机会。
如下例子:确定总额要哪些信息?其实就是销售的所有SalesLineItem实例及其小计之和。从领域模型,我们知道Sale实例包含了上述信息,按照信息专家建议的准则,Sale是适合这一职责的对象类,它是适合这项工作的信息专家。
更进一步,为了确定商品的小计,我们需要哪些信息呢?答案是SalesLineItem.quantity、和ProductDescription.price。SalesLineItem知道其数量和与其关联的ProductDescription。因此,根据专家模式,应该由SalesLineItem确定小计,它就是信息专家。为了实现获知并回答小计的职责,SalesLineItem必须知道产品价格。ProductDescription是回答价格的信息专家,因此SaleLineItem向它发送询问产品
信息专家经常用于职责分配,这是对象设计中不断使用的基本指导原则。专家并不意味着模糊或奇特的想法,它表达了一种“直觉”,即对象完成与其所具有的信息相关的职责。
注意,完成职责往往需要分布在不同对象类中的信息。这意味着许多“局部”的信息专家要通过协作来完成任务。例如,销售总额问题最终需要三个对象类协作完成。只要信息分布在不同对象上,对象就需要通过消息进行交互来共同完成工作。
信息专家模式(与对象技术中的其他事物一样)是对真实世界的模拟。我们一般把职责分配给那些具有完成任务所必需的信息的个体。例如在企业中,谁应当作出盈利或亏损的结论?答案是有权使用所有必要信息的人,或许是首席财务官。正如因为信息分布在不同的对象上,软件对象之间要互相协作一样,首席财务官也要和其他人协作。公司的首席财务官也许会要求会计师生成关于借贷的报告。
信息专家模式也不一定全是对真实世界的模拟,专家模式通常导致这样一种设计,软件对象所做的操作通常是作用于它们在真实世界中所代表的非生命体的那些操作;Peter Coad称之为“DIY”(Do It Myself)策略。例如,在真实世界中,不借助电子装置的帮助,销售本身无法告诉你它的总额,销售是一种非生命体,销售的总额是由某人计算出来的。但是在面向对象的软件领域,所有软件对象都是“活的”或“有生命的”,并且它们可以承担职责,完成任务。从根本上说,它们只完成那些与它们所知信息有关的事情。我称其为对象设计的“有生命”原则;
注意,在某些情况下,专家模式建议的方案也许并不合适,通常这是由于耦合与内聚问题所产生的。谁应当负责把Sale存入数据库呢?的确,大多数要保存的信息位于Sale对象中,于是专家会建议将此职责分配给Sale类。那么按照这一决定进行逻辑推理,每个类都应当有能把自身保存到数据库中的服务。但这样会导致内聚、耦合及冗余方面的问题。例如,Sale类现在必须包含与数据库处理相关的逻辑,如与SQL和JDBC(Java数据库连接)相关的处理逻辑。因此,Sale类不仅仅关注“作为销售”的纯应用逻辑。现在由于存在其他职责而降低了它的内聚。这个类必须与其他子系统的数据库服务进行耦合,如和JDBC服务耦合,而不只是与软件对象在领域层的其他对象耦合,所以使耦合度上升。这样也会导致在大量持久性类中重复出现类似的数据库逻辑。
所有这些问题都表明这种做法违反了基本架构原则,即设计要分离主要的系统关注。将应用逻辑置于一处(如领域软件对象),数据库逻辑置于另一处(如单独的持久性服务子系统)等,而不是在同一构件中把不同的系统关注混合起来。支持主要关注的分离可以改善设计中的耦合和内聚。因此,即使按照专家模式,把数据库业务的职责分配给Sale类是合理的,但是却由于其他原因(通常是内聚和耦合),会使我们最终得出不佳的设计。
优点,因为对象使用自身信息来完成任务,所以信息的封装性得以维持。这样就支持了低耦合,进而形成更为健壮的、可维护的系统。低耦合也是一种GRASP模式,将在下一节讨论。行为分布在那些具有所需信息的类之间,因此提倡定义内聚性更强的“轻量级”的类,这样易于理解和维护。该模式通常支持高内聚。
“把职责与数据置于一处”,“知其事,行其责”,“DIY”,“把服务与其属性置于一处”。
3.3 低耦合(Low Coupling)
专家指导我们,由于Board了解所有Square,所以将获知特定Square(具有唯一的名称)的职责分配给Board对象(它拥有这些信息,因此它是信息专家)。但是为什么专家模式给出这样的建议?可在低耦合原则中找到这个问题的答案。
简要地说,耦合(coupling)是元素与其他元素的连接、感知及依赖的程度的度量。如果存在耦合或依赖,那么当被依赖的元素发生变化时,则依赖者也会受到影响。子类与超类是强耦合的。调用对象B的操作的对象A与对象B的服务之间具有耦合作用。
低耦合原则适用于软件开发的许多方面,它实际上是构建软件最重要的目标之一。下面是低耦合模式的定义,从解决方案可知低耦并不是一个明确且可直接使用的方法,他只是强调如果有多个方案的时候,可以他用于进行评估。
耦合(Coupling)是对某元素与其他元素之间的连接、感知和依赖程度的度量。具有低(或弱)耦合的元素不会过度依赖于其他元素;“过度”是与语境相关的,但我们必须对此进行检查。这些元素包括类、子系统、系统等。具有高(或强)耦合的类依赖于许多其他的类,这样的类或许不是我们所需要的。有些类会遇到以下问题:由于相关类的变化而导致本体的被迫变化。难以单独地理解。由于使用高耦合类时需要它所依赖的类,因此很难重用。
假设,我们需要创建Payment实例并使它与Sale关联。哪个类应负责此事呢?因为在真实世界领域中,Register记录了Payment,所以创建者模式建议将Register作为创建Payment的候选者。Register实例会把addPayment消息发送给Sale,并把新的Payment作为参数传递给它
这种职责分配使Register类和Payment类之间产生了耦合,即Register类要知道Payment类。
根据职责分配,哪个设计支持低耦合?在这两个例子中,我们都假设Sale最终都必须耦合于Payment。在第一个设计方案中,Register创建Payment,在Register和Payment之间增加耦合;在第2个设计方案中,Sale负责创建Payment,其中没有增加耦合。如果单独地从耦合的角度来看,第2个设计方案是首选,因为保持了总体上的的低耦合。这个例子说明两个不同模式(低耦合和创建者)为何会导致不同方案。
在C++、Java和C#这样的面向对象语言中,从TypeX到TypeY耦合的常见形式包括:
TypeX具有引用TypeY的实例或TypeY自身的属性(数据成员或实例变量)。TypeX对象调用TypeY对象的服务。TypeX具有以任何形式引用TypeY的实例或TypeY自身的方法。通常包括类型TypeY的参数或局部变量,或者由消息返回的对象是TypeY的实例。TypeX是TypeY的直接或间接子类。TypeY是接口,而TypeX是此接口的实现。没有绝对的度量标准来衡量耦合程度的高低。重要的是能够估测当前耦合的程度,并估计增加耦合是否会导致问题,要极力避免产生具有负面影响的高耦合。
低耦合的极端例子是类之间没有耦合。这个例子违反了对象技术的核心隐喻:系统由相互连接的对象构成,对象之间通过消息通信。耦合度过低会产生不良设计,其中会使用一些缺乏内聚性、膨胀、复杂的主动对象来完成所有工作,并且存在大量被动、零耦合的对象来充当简单的数据知识库。对象之间的适度耦合,对于创建面向对象系统是来说是正常和必要的,其中的任务是通过被连接的对象之间的协作来完成的。
高耦合对于稳定和普遍使用的元素而言并不是问题。例如,J2EE应用能安全地将自己与Java库(java.util等)耦合,因为Java库是稳定、普遍使用的。
高耦合本身并不是问题所在,问题是与某些方面不稳定的元素之间的高耦合,这些方面包括接口、实现等。
低耦合的优点: 不受其他构件变化的影响,易于单独理解,便于复用。
在更高的目标层次上考虑,为什么期望低耦合呢?其实就是为了我们要减少变化产生的影响,因为低耦合往往能够减少修改软件所需的时间、工作量和缺陷。这只是个简要的回答,但是它对于构建和维护软件而言具有重大意义。
3.4 控制器(Controller)
在SSD分析期间,要首先探讨系统操作,这些是我们系统的主要输入事件,那什么是处理这些事件的第一个对象呢?
控制器(Controller)是UI层之上的第一个对象,它负责接收和处理系统操作消息。求助于控制器模式的指导,可以得到一般情况下可接受的、合适的选择。
控制器设计中的常见缺陷是分配的职责过多。这时,控制器会具有不良(低)内聚,从而违反了高内聚原则。
正常情况下,控制器应当把需要完成的工作委派给其他的对象,是一种外观控制器。控制器只是协调或控制这些活动,本身并不完成大量工作。当把职责分配给外观控制器会导致低内聚或高耦合的设计时,通常是当外观控制器的职责过多而变得“臃肿”时,就需要考虑使用用例控制器,那么对于每个用例,应用使用不同的控制器。对于同一用例场景的所有系统事件使用相同的控制器类,在消息处理系统中,可以用命令对象来表示和处理每个消息
3.5 高内聚 (High Cohesion)注意,在左侧的方案中,MonopolyGame对象自己完成全部工作,而在右侧方案中,它为playGame请求对工作进行了委派和协调。
内聚是软件设计中的一种基本品质,内聚可以非正式地用于度量软件元素操作在功能上的相关程度,也用于度量软件元素完成的工作量。比如,有100个方法和2000行源代码(SLOC)的Big对象,要比只有10个方法和200行源代码的Small对象所完成的任务多很多。如果Big对象的100个方法覆盖了众多不同的职责领域(如数据库访问和随机数产生),那么Big对象比Small对象的功能内聚性更低。概括地讲,代码的数量及其相关性都是对象内聚程度的指示器。很明显,不良内聚(低内聚)不只是意味着对象仅依靠本身工作;实际上,具有2000行源代码的低内聚对象或许需要和大量其他对象进行协作。所有的交互也都会趋于产生不良(高)耦合。不良内聚和不良耦合通常是齐头并进的。
高内聚模式的定义如下
内聚性较低的类,它们会导致以下问题:难以理解、难以复用、难以维护,且非常脆弱,经常会受到变化的影响
内聚性低的类通常表示大粒度的抽象,或承担了本应委托给其他对象的职责。
假设我们要创建一个(现金)Payment实例,并使其与Sale关联。哪个类可以负责这项工作呢?看2种直观的方案:
这种职责分配方式给Register赋予了支付的职责,Register承担完成makePayment系统操作的部分职责。在这个例子中,这是可以接受的。但是如果我们继续让Register类负责越来越多的、与系统操作有关的某些或大部分工作,它的任务负荷不断增加,而成为非内聚的类。这并不是说,创建单个Payment的任务导致Register成为非内聚的,而是说,从整体职责分配的全局出发,它可能具有低内聚的倾向。
这种设计既支持高内聚,又支持低耦合,所以它是我们需要的设计。
以下是描述不同功能性内聚程度的一些场景:
非常低的内聚,由一个类单独负责完全不同的功能领域中的大量事物。假设存在一个称为RDB-RPC-Interface的类,它负责与关系数据库交互的所有工作,同时负责处理远程过程的调用。这是两个完全不同的功能领域,并且每个领域都需要大量代码来支持。因此,应该将这些职责分为一组与RDB访问相关的类和一组与RPC支持相关的类。低内聚,由一个类单独负责一个功能性领域内的复杂任务。假设存在一个称为RDBInterface的类,它负责与关系数据库的交互的所有工作。这个类的所有方法之间是相互关联的,但是数量过多,并有需要大量的代码来支持,其中或许有成百上千个方法。这个类应当分为一组能分担RDB访问工作的、轻量级的类。高内聚,由一个类负责某个功能领域中的相应职责,并与其他类协作完成任务。假设存在称为RDBInterface的类,它只负责与关系数据库的交互的部分。为了检索和存储对象,它要和许多与RDB访问有关的类交互。适度内聚。由一个类负责几个不同领域中的轻量级和单独的职责,这些领域在逻辑上与类的概念相关,但彼此之间并不相关。假设存在称为Company的类,它负责了解所有雇员信息和财务信息的所有工作。这两个领域虽然在逻辑上都与公司的概念相关,但彼此之间并没有太紧密的关联。另外,其公共的方法并不多,其代码的总量也不多。根据经验,高内聚的类的方法数目较少、功能性有较强的关联,而且不需要做太多的工作。如果任务规模较大的话,它就与其他对象协作,共同完成这项任务。高内聚的类优势明显,因为它易于维护、理解和复用。高度相关的功能性与少量的操作相结合,也可以简化维护和改进的工作。细粒度的、高度相关的功能性也可以提高复用的潜力。
高内聚模式(与对象技术中的许多事物一样)是对真实世界的类比。显而易见,如果一个人承担了过多不相关的工作,特别是本应委派给别人的工作,那么此人一定没有很高的工作效率。从某些还没有学会如何分派任务的经理身上可以发现这种情况,因此正承受着低内聚所带来的困难,并且变得“分身乏术”。
内聚和耦合,阴和阳。不良内聚通常会导致不良耦合,反之亦然。我把内聚和耦合称为软件工程中的阴和阳,因为它们是互相依赖的。例如,考虑一个GUI窗口小部件类,它代表并绘制窗口小部件,把数据存入数据库,并调用远程对象服务。这样,它不但完全不是内聚的,而且还耦合了大量(全异)元素。
在少数情况下,可以接受较低内聚。一种情况是,将一组职责或代码放入一个类或构件中,以使某个人能方便地对其进行维护(尽管这种分组可能也会使维护工作变得很糟糕。但是假设应用中含有嵌入式SQL语句,而根据其他良好的设计原则,这些语句应该被分布到10个类中,如10个“数据库映射器”类。现在,通常仅有1~2个SQL专家知道如何才能最佳地定义和维护这些SQL语句。即使许多面向对象的程序员为此项目工作,但可能只有很少的面向对象程序员会有较强的SQL技能。假设这个SQL专家并不是合格的OO程序员。基于以上情况,软件架构师可以决定,把所有的SQL语句分组到一个类中,即RDBOperations类中,这样可以便于SQL专家在一个位置对所有SQL进行工作。
另一种情况是具有分布式服务器对象的低内聚构件。由于系统开销和性能与远程对象及远程通信相关,因此有时候需要创建数量较少并且规模较大的低内聚服务器对象,以便为大量操作提供接口。这种方法也与称为粗粒度远程接口的模式相关。在这种模式中,远程操作的粒度更粗,以便在远程操作调用中,可以完成或请求更多的工作,这样能够减轻网络的远程调用对于性能的不良影响。举个简单例子,使用一个接收一组数据的远程操作setData来代替具有三个细颗粒操作(setName、setSalary和setHireDate)的远程对象,可以减少远程调用从而获得较好的性能。
高内聚,能够更加轻松、清楚地理解设计。简化了维护和改进工作。通常支持低耦合。由于内聚的类可以用于某个特定的目的,因此细粒度、相关性强的功能的重用性增强。
3.6 多态(Polymorphism)
基于类型的选择——条件变化是程序的一个基本主题。如果使用if-then-else或case语句的条件逻辑来设计程序,那么当出现新的变化时,则需要修改这些case逻辑——通常遍布各处。这种方法很难方便地扩展有新变化的程序,因为可能需要修改程序的多个地方——任何存在条件逻辑的地方。
不要测试对象的类型,也不要使用条件逻辑来执行基于类型的不同选择。
多态是一个基本的设计原则,用于设计系统如何组织以处理类似的变化。基于多态分配职责的设计能够被简便地扩展以处理新的变化。例如,增加新的具有getTaxes多态方法的计算器适配器将会对现有系统产生很小的影响。
何时使用接口进行设计?多态意味着在大部分OO语言中要使用抽象超类或接口。何时应该考虑使用接口呢?普遍的答案是,当你想要支持多态但是又不想约束于特定的类层次结构时,可以使用接口。如果使用了抽象超类AC而不是接口,那么任何新的多态方案都必须是AC的子类,这对于诸如Java和C#的单根继承语言来说将十分局限。经验的做法是,如果有一个具有抽象超类C1的类层次结构,可以考虑对应于C1中的公共方法特征标记来定义接口I1,然后声明C1来实现接口I1。这样,对于新的多态方案,即使没有避免使用C1子类的直接动机,也能够获得灵活的进化点,用来应对将来未知的情况。
有时,开发者会针对某些未知的可能性变化进行“未来验证”的推测,由此而使用接口和多态来设计系统。如果这种变化点是基于立即或十分可能变化的原因而明确存在的,那么通过多态来增加灵活性一定是合理的。但是有些时候使用多态设计形成的变化点在实际中不一定发生或从未实际发生过,这种不必要的付出很常见。在投入灵活性的改进前,要现实地对待可变性的真实可能性。
多态的优点:易于增加新变化所需的扩展,无需影响客户便能够引入新的实现。
相关模式:防止变异大量流行的GoF设计模式,基于多态讨论其中的适配器(Adapter)、命令(Commond)、组合(Composite)、代理(Proxy)、状态(State)和策略(Strategy)模式。
选择消息,不要询问“什么类型”。
3.7 纯虚构(Pure Fabrication)面向对象设计有时会被描述为:实现软件类,使其表示真实世界问题领域的概念,以降低表示差异;例如Sale和Customer类。然而,在很多情况下,只对领域层对象分配职责会导致不良内聚或耦合,或者会降低复用潜力。
这种类是凭空虚构的,理想状况下,分配给这种虚构物的职责要支持高内聚和低耦合,使这种虚构物清晰或纯粹——因此称为纯虚构。
例如,假设需要在关系数据库中保存Sale的实例。根据信息专家模式,存在一些理由可以将此职责分配给Sale类自身,因为Sale持有需要保存的数据。但是考虑如下含义:这一任务需要相对大量的支持面向数据库的操作,而与实际的销售概念无关,这样会使Sale类变得非内聚。Sale类必须与关系数据库接口(如Java技术中的JDBC)耦合,这样便增加了耦合。而且这种耦合甚至不是针对其他领域对象的,而是针对特定种类的数据库接口。在关系数据库中保存对象是十分普遍的任务,许多类都需要支持这一任务。将此职责置于Sale类中表明了难以复用,或是在其他类中存在大量完成此项工作的冗余。因此,即便就信息专家而言,Sale是将其保存于数据库中的合理候选者,但是这样会导致低内聚、高耦合和难以复用的设计——正是此类困境使得我们需要虚构某些事物。合理的方案是创建一个新类,使其独自负责在某种持久性存储介质(例如关系数据库)中保存对象;对该类命名为PersistentStorage。[2]该类是纯虚构的臆想之物。
PersistentStorage,虽然这个概念是可以被理解的,但是我们却无法在领域模型中找到这个名称或“持久性存储”的概念。并且如果开发者询问商店里的业务人员:“你使用持久性存储对象吗”,他们不会理解。他们能够理解诸如“销售”和“支付”的概念。而PersistentStorage并不是领域概念,它只是为了便于软件开发而凭空捏造或虚构的概念。
纯虚构解决了如下设计问题:
使Sale保持高内聚和低耦合的良好设计。PersistentStorage类本身是相对内聚的,具有唯一目标,即在持久性存储介质中存储或插入对象。PersistentStorage类是十分普遍和可复用的对象。在本例中创建纯虚构正是由环境所驱使的——消除了基于专家模式的不良设计,改善了内聚和耦合,增加了潜在的复用性。与所有GRASP模式一样,纯虚构模式强调的是职责应该置于何处。在本例中,职责由Sale类(源于专家)转移给了纯虚构。
对象的设计可以被广泛地分为两组:
诸如Sale等软件类的创建是根据表示解析得来的;这种软件类项涉及或代表领域中的事物。表示解析是对象设计中的常见策略,并支持低表示差异的目标。但是有时,我们需要通过对行为分组或通过算法来分配职责,而无需创建任何名称或目的与现实世界领域概念相关的类。
换言之,纯虚构通常基于相关的功能性进行划分,因此这是一种以功能为中心的或行为的对象。大量现有的面向对象设计模式都是纯虚构的例子:适配器(Adapter)、策略(Strategy)、命令(Command)等。
纯虚构的优点:支持高内聚,因为职责被解析为细粒度的类,这种类只着重于极为特定的一组相关任务。增加了潜在的复用性,因为细粒度纯虚构类的职责可适用于其他应用。
有些对象设计初学者和更熟悉以功能组织和分解软件的人有时会滥用行为解析及纯虚构对象。夸张的是,功能正好变成了对象。创建“功能”或“算法”对象本来并没有错,但是这需要平衡于表示解析设计的能力(例如应用信息专家的能力),这样便能够使诸如Sale等表示类同样具有职责。信息专家所支持的目标是,将职责与这些职责所需信息结合起来赋予同一个对象,以实现对低耦合的支持。如果滥用纯虚构,会导致大量行为对象,其职责与执行职责所需的信息没有结合起来,这样会对耦合产生不良影响。其通常征兆是,对象内的大部分数据被传递给其他对象用以处理。
相关模式和原则:低耦合、高内聚、纯虚构通常会接纳本来是基于专家模式所分配给领域类的职责。所有GoF设计模式例如适配器(Adapter)、命令(Command)、策略(Strategy)等)都是纯虚构。事实上,所有其他设计模式也都是纯虚构。
3.8 间歇性(Indirection)
如果耦合不可避免,那我们可以更优雅的面对:
让耦合发生在非核心重要对象之间(一般领域对象低表示差异的对象是核心对象)让耦合发生在一个相对稳定的接口或者协议,将职责分配个一个相对稳定的中介对象TaxCalculatorAdapter这些对象对于外部税金计算器来说充当了中介的角色。通过多态,它们为内部对象提供了一致的接口,并且隐藏了外部API的变化。通过增加一层中间性和多态,适配器对象保护了内部设计,使其不受外部接口变化的影响
“计算机科学中的大多数问题都可以通过增加一层间接性来解决”,这一格言特别适用于面向对象设计。
如同大量现有的设计模式是纯虚构的特例一样,许多设计模式也同样是间接性的特例。适配器(Adapter)、外观(Facade)和观察者(Observer)就是这样的例。此外,许多纯虚构是因为间接性而产生的。间接性的动机通常是为了低耦合,即在其他构件或服务之间加入中介以进行解耦。
间歇性优点,实现了构件之间的低耦合。
相关模式和原则:防止变异低耦合大量GoF模式,诸如适配器(Adapter)、桥(Bridge)、外观(Facade)、观察者(Observer)和中介(Mediator)[GHJV95]。大量间接性中介都是纯虚构。
3.9 防止变异 (Protected Variation)
注意:这里使用的“接口”指的是广泛意义上的访问视图,而不仅仅是诸如Java接口等字面含义。前述的外部税金计算器问题及其使用多态的解决方案能够描述防止变异。
其中的不稳定或变化之处是外部税金计算器所具有的不同接口或API。POS系统需要能与大量现有税金计算器系统进行集成,并且还能与现在还不存在但将来可能出现的第三方计算器进行集成。通过增加一层间接性,即接口,并且使用具有不同ITaxCalculatorAdapter实现的多态,这样便实现了对内部系统的保护而避免了外部API的变化所产生的影响。内部对象只与稳定的接口协作,各种适配器实现隐藏了外部系统的变化。
防止变异,这是非常重要和基本的软件设计原则,几乎所有的软件或架构设计技巧都是防止变异的特例,例如数据封装、多态、数据驱动设计、接口、虚拟机、配置文件、操作系统等。
就某种程度而言,从以下方面能够发现开发者或架构师的成熟度:不断地增长更多实现PV机制的知识、选择值得解决的适宜的PV问题、选择恰当的PV解决方案的能力等。早期,人们学习数据封装、接口和多态等核心机制用来实现PV。后来,人们学习的技术包括基于规则的语言、规则解释器、反射和元数据设计、虚拟机等,这些技术都能够用于防止某些变化。
防止变异的核心机制:数据封装、接口、多态、间接性和标准都是源于PV的。注意:诸如虚拟机和操作系统等构件是实现PV的间接性的复杂例子。
4. 对象可见性设计可见性(visibility)是对象“看到”或引用其他对象的能力。更广义地说,可见性与范围问题有关:某一资源(例如实例)是否在另一资源的范围之内?考虑可见性的动机是:为了使对象A能够向对象B发送消息,对于A而言,B必须是可见的。实现对象A到对象B的可见性通常有四种方式:
属性可见性(Attribute Visibility)——B是A的属性参数可见性(Parameter visibility)——B是A中方法的参数局部可见性(Local visibility)——B是A中方法的局部对象(不是参数)全局可见性(Global visibility)——B具有某种方式的全局可见性属性可见性,当B作为A的属性时,则存在由A到B的属性可见性(attribute visibility)。这是一种相对持久的可见性,因为只要A和B存在,这种可见性就会保持。这也是面向对象系统中可见性的常见形式
参数可见性,当B作为参数传递给A的方法时,存在由A到B的参数可见性(parameter visibility)。这种可见性是相对暂时的,因为它只在方法的范围内存在。
局部可见性,当B被声明为A的方法内的局部对象时,存在由A到B的局部可见性(local visibility)。这种可见性是相对临时的,因为其仅存在于方法的范围之内。局部可见性是参数可见性之后的面向对象系统中第三种常见的可见性形式。实现局部可见性的两种常见方式是:创建新的局部实例并将其分配给局部变量。将方法调用返回的对象分配给局部变量。和参数可见性一样,将本地声明的可见性转换为属性可见性是很常见的。
全局可见性,当B对于A是全局时,存在由A到B的全局可见性(global visibility)。这是一种相对持久的可见性,因为只要A和B的存在,这种可见性就会存在。这是在面向对象系统中最不常见的可见性形式
实现全局可见性的一种方式是将实例分配给全局变量,这在某些语言(如C++)中是可能的,但是有些语言(如Java)不予支持。实现全局可见性的首选方法是使用单实例类(Singleton)模式
在上述各个可见性中,需要仔细审视是否必要,尽量减少耦合提高内聚。
5. 更多对象设计数据对象,缓存对象、document对象、其他的功能/辅助对象等都不在这里进行体系化展开,后续有机会再展开。