本节书摘来自华章计算机《基于模型的软件开发》一书中的第2章,第2.2节,作者:[美]H. S.莱曼(H. S. Lahman)著, 更多章节内容可以访问云栖社区“华章计算机”公众号查看。
2.2 广度优先处理(又称对等协作)
第1章讨论了SD的深度优先,分层的功能分解以及与此相关的问题。这次同样是对象的问题。由于抽象、封装和逻辑不可分性,对象具有自包含实现,因此它们允许广度优先通信。这是因为你让对象做什么它就做什么,并能够完成,这完全依赖于它的逻辑不可分性和自包含性。
从客户角度看它是原子的,一个对象就是单个可标识的实体。因此,应用中重要的控制流是按照对象的交互(OO中的协作)进行描述的。这在一定程度上大大提高了控制流的抽象层次。事实上,在本书后面的章节中你将看到,当定义协作时,哪个对象职责牵扯其中并不重要,消息也被认为是对象之间的协作而不是职责之间的协作(即,接收对象利用其现有的内部状态数据为消息选择恰当的行为)。
广度优先范式的价值体现在应用维护上,特别是在调试程序解决问题的时候。除非你试过,否则很难想象这种方法的强大。根据笔者的经验,在软件开发时,大约三分之二的bug通过审查就能够诊断出来,仅需几分钟而已;在等效的程序代码中,很可能是五分之一的bug。
在第一个试点项目中,作者曾经使用OO范式。那时硬件人员都把软件当做一种诊断工具,用它指出硬件该如何工作。所以硬件是基于软件的逐渐改进而改进的。在硬件人员完成电路板的制作之前,我们通常在软件中有一个硬件变更准确实现。
当我们完成试点项目后,市场改变了性能需求。为满足新的需求我们不得不对任务间通信进行彻底返工,从共享文档到共享内存,还有类似硬件初始化这样的基础性变更。我们在一周内完成了这些变更。这震惊了每一个熟悉这个项目的人,因为如果到那时候应用还像其他驱动程序一样以过程化方式书写,我们将花费数月的时间才能完成这些变更。(市场人员已经“亮出了他们的剑”并且半公开地向用户宣布,同样的变更如果以过去的方法执行将会延迟数月才能完成。)
描述广度优先控制流的另一种说法是对等协作。在解决方案中的每一个点,下一步操作由一个消息触发,该消息直接从引起触发条件的对象传递到具有响应的对象。没有任何间接或者中介参与协作。抽象与逻辑不可分性相结合的结果就是保证每一个行为都是自包含的。
2.2.1 细化与转化
细化被刻画为应用开发的一连串阶段(OOA→OOD→OOP),开发人员在每个阶段直接增加值。OOA是对与计算环境相关的战略细节的逐渐细化,直到形成完整的OOD。至此OOA不复存在。然后OOD逐步细化战术细节直到提供OOP解决方案。这是20世纪80年代后期OO范式实现的典型方式。
与此相反,转化论支持者在OOA和OOD中间划一条明显的界线。他们将OOA作为一种独立实现的、针对用户问题的抽象解决方案。然后他们使用一个完整的代码生成器(称为转换引擎)将OOA转化成一个代码模型(3GL或程序集)。转换引擎基本上实现了OOD和OOP的自动化。
细化方式的主要优点是每个人都知道应该怎样做。缺点是有陷入设计无能的趋势,因为OOA和OOD完成时间没有明确的标准。另外一个缺点是当进行计算环境移植的时候,不能很好地将用户问题解决方案从实现细节中分离出来。此外,在OOP中还需要花费工作量实施依赖管理重构,以保证3GL程序的可维护性。为方便以后的维护工作,需要对OOA/D模型进行副本保存,维护变更时会面临多个版本的问题。但是,最大的代价其实是为应用重复提供实现的基础设施。在一个典型的3GL程序中,至少有60%的代码将直接或者间接地投入到某种特定技术(例如,XML)和优化技术(例如,缓存)中,主要用来满足非功能需求或者3GL的战术解决方案。基本上这些代码在每个应用中都会重复。
转化的优点如下:
转化可以将问题域解决方案逻辑从计算环境的实现问题中分离出来,即允许分别关注这两类问题。在实践中这意味着功能需求(OOA)可以独立于非功能需求(OOD和OOP)进行解决。这促进了应用问题解决和实现优化之间的专业化。
转化允许同一OOA解决方案移植到不同的计算环境中使用且不需要进行变更,可以保证问题解决方案的逻辑保持有效。
转化允许在一个既定的计算环境中一次完成转换引擎开发。这个转换引擎能够重用,对所有应用OOA进行转化,使其运行于这个环境。实际上对于给定的计算环境,拥有大量的跨应用OOD/P设计。
OOA完成的退出标准是清晰的,因为OOA模型是可执行的。当功能需求满足时OOA就算结束了。
当需求变更时,只需要维护OOA模型,于是无需重构依赖管理,而物理耦合完全是3GL问题。
OOA解决方案大约比3GL解决方案要简洁一个量级,它将重点放在解决方案的基本要素上,在很大程度上更易于交流和理解。
转化的缺点如下:
计算域可能是十分确定的,但同时也是庞大且复杂的。因此提供合适的转换引擎优化十分不易,自制转换引擎又不可行。因此,当今的商业转换引擎相当昂贵。从20世纪80年代早期到20世纪90年代后期,优化技术一直在发展,试图让转化代码的性能与精化代码的性能具有可比性。这需要更有多的投资。
OOA模型必须非常严格。代码生成器只能实现明确的指示,而不能猜度指示者的意图,因此软件开发人员必须使用良好的分析和设计方法。当然,选择不是唯一的,软件开发人员需要学习更多的东西,例如状态机等,因为它们也能够提供必要的帮助。
从传统的细化到转化对于开发软件的方式来说是巨大的变化。从配置管理到测试开发都会被这种变化所影响,因为开发阶段发生了本质上的变化。
如引言所述,MDB是转化方法学的一种。对于20世纪60年代,作者认为从程序集到3GL,转化技术的应用不可避免。20世纪50年代,计算域的自动化不可阻挡。作者的第一个程序在插件板上完成,那时FOTRAN和COBOL语言还是学术界的新奇玩意儿,链接器与加载器刚刚出现,BAL即将成为解决软件危机的银弹。从那时开始我们就注定要在这条路上走很久。在利基市场中,转化技术已经制度化了,例如GRUD/USER处理中的HPVEE和RAD IDE。从3GL到通用4GL的升级也是不可避免的。
然而,本书所有方法学的设计原则在细化或转化中的使用都是相同的。将OOA模型和MBD相结合,会开发出一个可维护性更好的应用,同时对于细化的深化阶段,它也是一个完整的、精确的、明确的规格说明。换句话说,细化开发人员应当提供MBD OOA模型,尽管他们很少去做这件事。
2.2.2 消息范式
想要隐藏知识和行为的实现,我们需要一个盾,实现可以隐藏其后。因此,每一个对象或者子系统都有一个接口隐藏这些实现。接口的概念并不新鲜。OO范式将接口用于对象细粒度的层次上是非常有利的,但不是非常重要的。另一方面,OO范式与其他软件构造方法最重要的区别之一就是接口的实现方式。
接口可以定义为一个关于实体响应消息的集合。
OOA/D的开发人员提出这个概念来应对各种各样的离散问题。这么做,可以使OO软件构造从根本上唯一,并且彻底颠覆开发人员对于构造软件的思考。
对象通过互发消息进行沟通,接收对象会选择行为来响应接收到的消息。这恰好可以将消息发送方与接收方去耦合,因为发送方不需要了解接收方是如何响应的。这是一种非常强大而灵活的方法。首先,在OOA/D中,消息可以十分抽象,或是表现为硬件中断的类似功能,或是定时器的暂停,或是单击一个GUI控件的动作。其次,接收方允许在相当大的范围内进行响应。接收方可以根据其内部状态用不同的行为响应同一个消息,在某些情况下无视消息。基本上,这是一个很好的主意,即消息发送方对于消息会如何响应没抱任何期望。
消息不等同于方法。消息是类接口而方法是类实现。
消息和方法的这种分离正是接口的OO视图导致一种完全不同于以往软件构造方式的原因。传统上,在SD的功能分解中,高层次功能调用低层次功能,低层次功能是高层次功能职责的扩展,所以高层次功能能够预测即将发生什么。子节点功能的规格说明是高层次功能规格说明的子集,高层次功能不仅可预测低层次功能做什么,还依赖于低层次功能。因此,正如我们在第1章中所见,导航功能分解树的基本方法就是“做
这些”。
但是,在OO范式中,基本范式是逻辑不可分的实体和行为之间的对等协作,因此我们变更解决方案时,只需知道消息去哪里,而不涉及方法的实现。现在,消息成为发送方的声明,即我做完了。这些声明与DbC是吻合的,因此一个消息可以简单声明:一些特定的条件在解决方案的状态中随处可见。
在本书第三部分中我们将会看到,DbC用于严格定义正确的控制流,这是通过将生成消息的方法的后置条件与整个解决方案控制流中下一个执行方法的前置条件相匹配来实现的。实际上我们有一个形式化的方法来确保控制流是正确的。
另外,我们很好地进行了去耦合,因为所有消息发送方仅仅需要了解做了什么。因此,如果所有消息发送方都对外声明了它做了什么,它就不需要了解任何关于响应方的信息。同样,因为接收方只是简单地对消息进行响应,所以它不需要对消息的发送方再进行任何了解。这大大减少了两者之间的耦合,因为发送、接收双方唯一共享的就是消息及相关数据包。
OO范式中方法的设计源于问题域抽象的方法学技术。这些技术确保了内聚、逻辑不可分和职责自包含。但是这些技术都假设以控制流中“我已经做完”的概念替代结构化开发中“做这些”的概念。也就是说,OO消息范式将一切连接在一起,如果没有了消息范式,这些技术效用有限。问题域抽象可能是OO范式区别于其他方法的首要特征,而消息范式仅次于它。
在结束消息范式的讨论之前我们要指出的是,在MBD中对消息接口的限制比其他OO方法都要多。MBD中,对象层次上几乎所有消息都是简单的知识请求或者状态机事件。另一层含义是,这些接口都是纯粹的值传递数据接口。这意味着MBD明确禁止更多极坏形式的耦合,例如向行为方法传递对象引用。
2.2.3 对象特征
这个话题涵盖对OO范式“基石”的概述。“对象”这个词含义太多。《Dictionary of Object Technology》一书中其定义多达17种,讨论篇幅长达14页。在本书中为了避免表述混乱,始终采取一个一般性的定义,该定义足以避免很多争议。
类:拥有相同职责的、可唯一标识的对象集合。类枚举全体成员都具有的一组共享职责。
对象:对象是对某些问题域中真实的、可识别的实体的抽象。潜在实体可能是具体的也可能是概念性的。对象抽象通过一组职责进行刻画,拥有一组相同职责的对象属于同一个类。
实例:在问题解决方案执行过程中,对象通过内存中的软件,或者在持久数据存储中间接地实例化。
职责:为了(了解掌握)知识或者(实现)行为而抽象问题域实体的特质。在OO范式中没有直接的方式表达类似目的的概念。这些问题域的特质必须通过知识或者行为职责来进行抽象。这种限制潜在的原因是提供一个到集合、图或者其他运算的明确映射,这些集合、图或者其他数学运算用于定义计算域。例如,知识职责可以方便地映射到状态变量的概念,行为职责可由过程来进行表达。
这些要素之间的关系如图2-2所示。用户域实体,例如Dick和Jane,拥有独特的特质,我们可以在概念设计域中将其抽象为对象。这些对象直接与Dick和Jane相对应。由于这些对象拥有一些相同的抽象属性,我们可以将其归于同一个类。这个类可以从用户域中捕获“人”的定义。最后我们将在软件运行时将这些对象实例化为实例。这些实例在内存地址中拥有具体的表示,其属性为二进制值。
当然,其中争议最大的定义是就是实例。很多作者将对象和实例作为同义词来使用。在MBD中我们发现区分两者非常有用,无论应用执行与否,对象是存在于设计中的概念设计抽象,实例是运行时抽象的内存映像。通常,对象的动态实例化是以解决方案的即时上下文为基础的,在解决方案中实例对于事件什么时候发生是非常重要的。
属性
属性定义了一个类中每个对象所承担职责的知识。这里的关键词是职责。属性描述的是知识,该知识是类中任何对象被期望能够提供或者管理的内容。一个普遍的错误理解是,属性是用来表示物理数据存储的。通常,属性可以表示物理数据存储,但是这只是一种实现上的巧合,因为在计算域知识通常映射到数据。为了我们的目的,属性仅仅描述实例必须了解些什么,而不描述它们如何去了解。这与一个特定对象了解的值不同。同一类集合中的单个对象对于给定属性可以具有不同的值,它们可能恰巧对于相同属性拥有相同的值。属性值仅取决于拥有对象的标识。
方法
一个方法实现了一个特定的行为职责,这一职责是一个类中所有的实例必须提供的。类中的每一个方法都是对特定行为的特定业务规则和策略的封装。传统上,方法的名称简要描述行为,方法体隐藏行为如何执行。
OOA/D仅仅描述什么是行为职责应该做的,而没有描述如何做。
一个行为职责可能相当复杂,即使它在拥有对象的抽象层次上是逻辑不可分的。为了描述一个行为方法应该做什么,我们需要特定的语言,该语言能够以抽象的形式进行描述,且与其他的模型保持一致。因此,我们需要抽象行为语言(Abstract Action Language,AAL),它设计成为一种规格说明语言,而不仅仅是一种实现语言。虽然大部分的AAL语法有意地设计为与3GL类似,但它们还是有很大不同,我们将在第18章进行详述。
图2-2 实体、对象、类和实例之间的关系。实体拥有的特质被抽象为对象的属性。拥有同样属性的对象被划分为类。在计算机内存中,不同对象被不同的具体值实例化