两位研究面向对象软件工程的美国学者 (Stave Halladay和Michael Wiebel) 曾这样说:“一般的面向对象编程(OOP)思路不过是一批乌合之众,把灵机一动、随机应变的技巧用于他们绞尽脑汁抽象出来的‘对象’而已。即使是最优秀的 OOP 程序员,他们所能对付的极限也莫过于中等规模的开发项目。倘若程序员经验不足,系统规模又很大,那么采用 OOP 只能把你引入漫无边际的泥沼之中。” 一方面是几乎没有一位软件工程学者认为 OOP 是完美无缺的,另一方面是 OOP 势如破竹,近乎每一种最新推出的程序开发工具或语言都采用了 OOP 思路;一方面是越来越多的“乌合之众”在毫无章法、随心所欲地处理着“对象”,另一方面是经过近 30 年的积累已经拥有了最大多数用户的结构化软件方法的日渐萎缩……面对这一现实,研究软件工程方法学的专家们纷纷指出:“当前摆在软件开发方法学面前的一个重要课题是:从理论上理解 OOP 具有强大生命力的天然合理性,并完善面向对象软件工程方法学体系。”
一年来我们通过国内外一些实用系统的开发实践,对面向对象的软件工程方法进行了较为深入的学习和探讨,特别是在北京市公路局计算机系统的一期工程实践中,借鉴国外软件设计经验,较系统地采用了面向对象软件工程方法,受益匪浅。
一、 是“设计主导”还是“程序主导”
在一个系统开发过程中是只采用 OOP 还是采用了OOSE(面向对象软件工程)方法,关键看整个开发过程是“设计主导”还是“程序主导”。
近年来,大量先进程序开发工具进入我国,这对提高软件开发效率无疑具有很大的作用。然而,它们又往往使程序主导型软件开发人员在“以程序代系统”、“以算法代设计”的误区里越陷越深。
一般的软件开发人员(包括那些只见程序不见系统的程序员)主观上都认为:软件开发不应“系统设计主导”而应“程序算法主导”。但是用下面几个问题考察一下,结果往往相反。
问题1 在进行软件设计和选择软件开发工具之前,是否进行开发方法学的选择?
所谓方法学是指组织软件生产过程的一系列方法、技术和规范。方法学是软件开发者长年失败和成功经验的理论性总结,从软件重用的思路来说,方法学重用的价值远非某些程序组件重用可比。
以北京市公路局系统为例。首先,在系统调查阶段我们了解到:这个系统要分期 (递增式) 开发。由于处于机构改革时期,系统生存期内的用户需求和系统结构变因很多。这表明目标系统应该具有较强的可维护性,即每期开发成果应在后续工程中具有较高的可重用率。其次,一期工程的工作量相当大(最后成果包括 124 个模块、72 类报表、119个数据库表、439 个窗口、912 个数据窗口),而开发者对公路局业务不了解,多为经验不足的大学生,理解需求的能力较低。这表明采用的开发方法学必须能最大限度地减少重复劳动,实现开发过程中的成果共享和重用;必须能支持消除需求理解误差的调整工序,使下游成品阶段的设计变更比较容易进行。
在开发此系统之前,我们承接了一个国外软件的下游开发任务。由于它采用了面向对象的软件设计,使我们深刻认识到国内外软件开发方法学和技术上的差距,颇受启发。
参照我们承接的国外软件开发工作量计算方法,即仅下游120个模块 (含报表) 的编码和测试为41人月,那么公路局系统从上游设计开始近200个模块和报表、100多个数据库表的开发工作量至少也应在120人月以上。由于采用了面向对象的软件工程方法,尽管开发人员大多经验不足,但是第一期工程总工时最终仍控制在 80 人月以内,降低成本1/3左右。同时在系统可维护性、重用度及其他功能和性能指标上,均超过了我们以往采用结构化方法开发的系统。
对停留在程序主导级开发的软件开发人员来说,他们选择 OOP 的原因也往往是被动的。其实,在程序主导开发者的辞典中是找不到“方法学”这一词的,或者把“方法学”与“程序算法”混为一谈。至于把 OOP 看成是 OOSE 的全部就更不足为怪了。
问题2 对象抽象的出发点是现实世界的问题描述,还是可执行的实例对象?
在现实世界早期抽象阶段,面向对象方法与其他方法区别并不大,都要从现实世界的问题描述出发,即从用户接口、问题领域的知识和经验出发,构筑现实世界的问题模型,也就是确定目标系统是“做什么的”。面向对象的问题分析模型从3个侧面进行描述,即对象模型 (对象的静态结构)、动态模型(对象相互作用的顺序)和功能模型(数据变换及功能依存关系)。软件工程的抽象原则、层次原则和分割原则同样适用于面向对象方法,即对象抽象与功能抽象原则是一样的,也是从高级到低级、从逻辑到物理,逐级细分。每一级抽象都重复对象建模 (对象识别)→动态建模(事件识别)→功能建模(操作识别)的过程,直到每一个对象实例在物理(程序编码)上全部实现。
对象抽象是从逻辑级还是物理级出发,与开发前是否进行方法学选择一样,也是区分OOSE 与 OOP 的试金石。由于许多工具或语言(如PB、C++、Motif) 都支持OOP,使一些程序级系统开发人员可以很方便地不经过逻辑抽象就直接开发物理对象,在早期阶段意识不到从物理层即实例对象出发进行系统开发的祸患,孰不知正是这种随心所欲的 OOP 不仅无法发挥面向对象方法应有的优越性,而且还会给开发后期带来大量返工作业。
和以往采用结构化方法一样,我们在系统设计阶段也引入了原型化方法,以便用系统样品即原型与用户对话,求得对需求理解的勾通,避免或减少后期返工。大多OOP工具都为开发原型提供便利,问题在于原型与最终产品间的关系,即原型是逻辑对象还是物理对象的样品。若是后者,那就等同于最终产品。在木已成舟时再让用户评审,若发现问题,要么推倒重来,要么强迫用户削足适履。事实上,我们为设计评审而基于逻辑对象开发的原型,相当部分被用户否决。但由于尚未进行对象实例即物理级开发,而是使用超类对象原型统一模拟对象事件和操作,所以无论是对象模型、动态模型还是功能模型,修改起来都不困难。
问题3 设计阶段是否先设计超类,是否在实例对象设计开始之前完成超类对象的实现?
面向对象方法开发出的软件具有较强的可重用性,这种重用包括开发项目内部的重用和外部的重用。重用依存于超类设计,没有超类的对象系统好比“把洗衣机当米缸”,不能物尽其用。超类设计的好与不好,首先看其内部重用率的高低,内部重用率高,必然外部重用率也高。
由于系统开发工期紧、工作量大,而我们的开发队伍年轻,经验和人力都不足,内部重用率高的超类开发无疑是我们的救星。它可以减少重复劳动,易于统一规格,对复杂问题统一攻关、统一解决,便于统一维护。
对超类的抽象即实例对象的泛化原则,我们是从下面几个方面考虑的:
(1)寻找大多数实例对象的共同行为。
例如“打印报表”、“查询静态代码表”、“录入数据库表数据”等。
(2)超类的多态性设计要保证使用超类继承关系可以满足各子类的操作要求。
例如,继承同一个“数据录入”祖先窗口,可以完成不同结构数据库表的数据录入。
(3)利于信息的隐蔽性,不会破坏数据的完整性,利于将复杂问题简单化。
例如,对具有复杂关系、结构及相关存取操作的数据库表集的维护。如果不使用一个泛化类将数据结构及其相关操作封装起来,下层程序员要想操作有关库表就必须对库表设计有深入的了解,并且确保程序算法设计不得破坏数据的相关一致性,这将大大增加程序设计和测试的难度,要求程序员有较丰富的经验。而采用这种泛化类 (公用函数、公用存储过程) 后,程序员所要做的只是发“消息”和取“输出信息”了。
(4)有利于推行开发规范,统一界面风格。
我们在开发国外软件中受到的最大磨练是:国外对用户界面 (报表、屏幕) 一丝不荀的严格要求。所有屏幕按钮的高、宽、起始位置都用精确到小数点后 3 位的 X、Y 座标进行规定。这样出来的产品使人看上去就有赏心悦目之感。但是如果人人都做界面窗口、按钮的精细调整,工作量势必成倍增长。采用屏幕界面模版超类的继承关系,结合特化处理,问题便可迎刃而解。
显然,超类的设计和实现必须在程序员普遍进行实例对象开发之前完成。也就是说,OOSE 的上游系统设计人员必须文武 (设计与编程) 双全,能够担负起超类对象的程序实现与测试任务,这与结构化方法的上层系统设计人员基本可以不编程有所不同。同时,超类对象在下游开发过程中必须经常吸收特化过程中的反馈(包括来自用户的反馈),进行相应的调整修改。所以OOSE担任超类对象设计与实现的设计人员很难像结构化方法那样进入编程阶段后就可以稍事轻松,他们往往始终离不开编程现场。
如果设计阶段不预先设计和开发出超类对象,在同一项目的多数开发者之间没有可以共同继承的祖先对象,甚至在各个开发人员自己的作用范围内都不使用继承关系,那么这不仅不是OOSE,就连称之为OOP都很勉强。
问题4 如何处理对象模型面向对象关系数据库模式的映射?
面向对象的数据库设计方法可以用于各种数据库,如层次型、网络型、关系型,当然也包括面向对象型。OOSE 中的数据库设计无疑必须采用面向对象的数据库设计方法。
数据库设计也称数据库模式,基本上由3个层次的模式构成:从特定DB应用角度来看待DB设计的外部模式;从组织或企业角度出发进行的DB设计即概念模式;处理对应特定 DBMS 特征与局限性的DB设计即内部模式。具体而言,内部模式是数据库的SQL定义,逻辑模式是表集合的逻辑定义,外部模式是从特定应用角度看的局部DB。外部模式与逻辑模式之间的接口是视图、存储过程或其他驻在服务器端的DB处理程序。
如果在抽象出的对象模型中,各个应用分别是一个或多个超类对象的子对象,那么,选择适当细分层次的对象模型将其映射到概念模型,是数据库库表对象设计的关键。外部模式与概念模式之间的接口越少、越简单越好,这样的程序设计简单,数据库和程序都易于维护。也就是说,局部化是个重要的设计原则。
OOP多是数据库的后端处理,是基于既存数据库的。因此无论是否进行过问题世界的对象建模,以及是否将对象模型合理地映射到数据库逻辑模式 (面向对象数据库设计),OOP 都可以工作。
问题5 编程时是否先调查有无可重用 (继承) 对象,是否参与下层对象对上层对象、超类对象的反馈?
埋头于自己分担的程序对结构化方法或许是必须的,但在面向对象方法中担任程序设计的开发人员,应该先去调查对象数据辞典中有无其他开发人员已经完成、自己稍加特化就可重用的对象。从总体上说,对象的共享、重用应该由上层设计人员统一管理,以便保证对象风格的一致性,避免冲突。但是,对象的独立性、封装性和多态性都很便于重用,这是结构化系统所不能比拟的,而重用是软件开发方法学的最重要思想之一。上层设计人员往往不可能面面俱到,懂得软件设计理论的开发人员,即使只开发下层程序也应采用最省力、最有效率的编程方法,即大量使用重用对象。
在继承超类对象和重用他人对象时,若发现有设计不合理的地方,应该及时反映给对象开发的承担者。
对上层设计人员来说,一方面应该鼓励程序实现人员重用既存对象,另一方面应通过开发人员共享对象数据辞典,使个别的对象重用能够立即反映到整体对象模型中,以保证设计变更时的一致性。
二、面向对象方法与结构化方法比较
分析是问题抽象 (做什么),设计是问题求解 (怎么做),实现是问题的解 (结果)。任何方法学对客观世界的抽象和求解过程都是如此。在问题抽象阶段,结构化方法面向过程,按照数据变换的过程寻找问题的结点,对问题进行分解。因此,与面向对象方法强调的对象模型不同,描述数据变换的功能模型是结构化方法的重点。如果问题世界的功能比数据更复杂或者更重要,那么结构化方法仍然应是首选的方法学。如果数据结构复杂且变换并不多,那么如以过程主导分析和设计,一旦有系统变更就会给下游开发带来极大混乱。
由于对过程的理解不同,面向过程的功能细分所分割出的功能模块有时会因人而异。而面向对象的对象细分,从同一问题领域的对象出发,不同人得出相同结论的比率较高。
在设计上,结构化方法学产生自顶向下、结构清晰的系统结构。每个模块有可能保持较强的独立性,但它往往与数据库结构相独立,功能模块与数据库逻辑模式间没有映射关系,程序与数据结构很难封装在一起。如果数据结构复杂,模块独立性很难保证。面向对象方法抽象的系统结构往往并不比结构化方法产生的系统结构简单,但它能映射到数据库结构中,很容易实现程序与数据结构的封装。
在软件工程基本原则中有一条“形式化原则”,即对问题世界的抽象结论应该以形式化语言 (图形语言、伪码语言等) 表述出来。结构化方法可以用数据流图、系统结构图、数据辞典、状态转移图、实体关系图来进行系统逻辑模型的描述;而面向对象方法可以使用对象模型图、数据辞典、动态模型图、功能模型图。其中对象模型图近似系统结构图与实体关系图的结合,动态模型图类似状态迁移图,功能模型图类似数据流图。
公路局系统有 100 多个数据库表,但数据的加工 (变换) 很单纯,如果当初选择结构化方法学,情况会怎么样?
在问题抽象的最初阶段不会有太大差异。由于数据变换少,可以把对象和对象的操作看成一一对应,即最初问题描述的对象模型与功能模型基本一致。以其中计划管理处子系统为例,对象是计划管理员、规划管理员、概预算管理员、统计管理员,功能 (操作) 是计划、规划、概预算、统计。
问题存在于下层抽象里。
首先,许多公共超类对象设计与结构化方法相悖,因为它破坏了过程的连续性及系统结构的逻辑层次性,把一些下层模块及在过程分析中没有语义的对象,放在系统结构的上层。因此如果采用结构化方法,须将继承关系改为下层模块调用关系。但是事实上,祖先对象的一些状态 (属性值) 是从主控模块直接得到指示而确定的;从控制角度说,它的确处于系统的上层地位。如果采用结构化方法,结果将是要么把系统结构变成网络状,失去结构化特征,要么放弃这种统一完成重复性劳动的设计方案。
其次,应用对象模型向数据库概念模式的映射设计也是该系统采用面向对象方法的一个标志。如果使用结构化方法,数据库模式可能映射客观世界的数据结构。由于公路、养路单位、管理单位、路况、桥梁、隧道及道路上的绿化情况等各实体间客观存在着复杂的多重关系,其结果可能定义出一个像蜘蛛网似的关系库结构,因而大大加重了数据库前端应用编程和数据库维护的负担。
总之,该系统若使用结构化方法,系统结构和数据库结构都可能成为网状结构,且互相无关。而目前采用的面向对象方法,系统结构和数据库结构都是多重继承结构,相互存在映射关系。显然前者较后者复杂性高、可维护性差、内部重用难度大、重用率低。
其实,无论是用什么方法学开发软件,交给用户的都应该是满足用户当前需求的软件。用户在短期内不会发现开发者使用先进方法学给他们带来的益处,倒是开发者本身由于大大减轻了开发负担而最先受益。但是随着时间的推移,获得最大收益的还是用户,因为软件的长期质量(包括维护成本低和生存周期长)给用户带来的好处才是根本的。
三、方法学是思路不是定律
对于方法学,我们是这样理解的:
(1)方法学的目的是:使后人分享前人的成功,避开前人的失败,把注意力集中在尚未开拓领域的创造性劳动上。所以方法学与开发人员的创造性是绝不冲突的。它既不能像法律那样靠权威来界定是非边界,也不能像定律那样通过证明和推理给出普遍结论。如果一定要做比喻的话,它好比人的世界观。
(2)没有放之四海而皆准的方法学,任何方法学都有其局限性,所以软件开发人员大可不必拘泥于某种特定的方法学。
例如,面向对象方法的对象模型图,这种形式化语言远不如结构化方法的结构图和数据流图简单明了,倘若把公路局系统全部用对象模型图表述出来,至少也要几十页。由于最上层功能模型与对象模型是一致的,所以我们采用的是结构化方法的系统结构图。
(3)事实表明,由 OOP 带动的 OOSE 方法确实比结构化方法更能自然地抽象现实世界,而且一些 OOP 工具确实已相当成熟。相反,结构化方法及开放平台下的结构化程序开发工具,虽然不能说止步不前,但其近年来的进步是有限的。
(4)根据我们的体会,对实践 OOSE 有以下一些建议:
1 最好在选定方法学后,对全体开发人员进行一次关于面向对象方法学的培训。
2 由于有超类对象的提前开发工作,OOSE 的上游设计工作量比结构化方法的上游工作负担重,时间和人力应该更充足一些。否则到下游开发后再追加或多次修改变更超类对象,容易造成混乱和无效劳动。
3 由于系统越大对象类越多,为了便于内部重用和共享,应该建立电子化的对象数据辞典,以便对对象进行统一归类管理。
4 应该有严格的命名规则,如果可能,应将命名规则集成到数据辞典中。
5 下层开发铺开后,如果发现应该对某些实例对象泛化成新的超类对象,必须尽快进行新超类追加的设计,变更越快越好。
6 子对象继承超类对象后,发现超类设计的缺陷是常有的事。开发队伍内部应有很畅通的反馈渠道,使超类得到及时的修正。子对象切不可轻易将超类对象封杀掉,使系统失去统一控制。遵从系统设计中定义的继承关系进行实例对象开发应该成为全体开发人员的理念。
7 面向对象设计的好处越到后来越显著,特别是在系统维护和扩充方面。