最近一段时间来,我都在忙于做一件“异想天开”的事情——寻找传说中的“银弹”。呵呵,听起来确实有些搞笑,但是我想我的确发现了什么。
引言
自Brooks预言“没有银弹”的数十年来,软件设计开发的理论和技术都有了相当大的改进,但也正如Brooks所言:没有银弹。
现在,我们拥有面向对象编程、UML建模、CMMI过程改进等各种理论和技术,然而所有这些充其量只能算得上铜质子弹,答案依然:没有银弹。
通用构件库(UCL)的基本构想
我所设计的是一个叫做“通用构件库(Universal Components Library)”的框架,设计灵感源自可视化构件库VCL、MVC三层架构以及Linux下一个基于Alsa的声音合成软件,而其理论基础则是分层数据流图。
这个框架贯穿整个软件的设计和开发过程,试图让设计和开发连成一气。虽然并非基于面向对象设计与编程,但也不排斥面向对象编程技术。除了大幅度提高代码重用性之外,这个框架也有助于提高软件的可维护性。在基于UCL的设计和开发过程中,还可以综合运用XP开发过程及CMMI过程以进一步改善整个过程。
UCL的目标和当初VB、Delphi的目标一样,让软件开发变得就像搭积木那么简单。使用类似VCL、MFC的东西的确可以提高开发效率,但无法从根本上实现这个目标。其原因可以归结为几点:
- 把“搭积木”理解为对整个系统进行可视化开发,而非对系统的各个模块进行分层搭建;
- 扩充性不足,自定义组件没有能获得和自带组件同等的地位,以至于不得不重复发明各种轮子;
- 独立于设计过程,无法将代码和设计进行对照。
UCL是一个抽象的库,只是一个框架而已,作用是把以VCL、MFC之类的具体组件组织起来,从而真正具备“搭建”的能力。由于本身就是个完全开放的框架,因此任何组件都是平等的,大家可以随心所欲地创建各种各样的专用UCL库。UCL框架使用UCL图作为设计和代码的中间媒介,数据流图可以直接转换为UCL图,代码可以由UCL图直接生成或“搭建”。
由于UCL组件和代码一一对应,因此UCL图是依赖于具体库的,例如使用VCL搭建的系统和使用MFC搭建的系统,即使其功能一致,UCL图也有可能不同。也就是说,想要应用UCL技术,就必须拥有要使用的具体库所对应的UCL库,例如UCL_VCL、UCL_MFC、UCL_Swing、UCL_QT等等,这也是为了实现真正的“搭建”所付出的代价。
UCL图是UCL设计思想的核心,其灵感来源于Linux下的一个声音合成软件。该软件提供了许多种类的声音处理模块,这些模块的外部接口都很简单,大都只有输入、输出接口而已。但是只需要将各种模块用音频流连接起来,理论上却可以合成出任意的声音。面向对象技术中的问题之一就是类的接口太多且没有标准,不便于掌握。如果可以每个组件只有几个接口,并且组件的分类比较科学,那么掌握起来就简单多了。
除了连接关系之外,为了适应自顶向下的分层设计的需要,UCL图还支持“组合”的关系。一个组件可以由一个或多个内部组件组合而成,嵌套层次不限。这样,一个复杂系统就可能最终分解为一系列仅具有一个输入接口和一个输出接口的简单组件。
UCL图除了能够充当设计和开发的中间媒介之外,还有另一个重要的作用。在分解为一系列UCL组件之后,整个系统的规模和工作量就可以快捷并准确地估计出来。
可见,UCL技术的优点在于:
- 使得软件的规模和工作量在前期设计就可以比较准确地估计;
- 使得设计和开发过程一气呵成;
- 提倡面向组件开发,在不增加代码复杂性的前提下,大大提高了可重用性;
- 使得软件更易于维护。
- 以下将分别讨论这些问题。
UCL解决的问题:前期设计
软件的前期设计,从来不是个简单的问题。我们在获得需求说明书之后,就需要将其转变为设计蓝图。过去的数据流、事务流、控制流分析方法更适用于模块内部的分析,而对于一个庞大的系统,则往往需要使用UML进行建模。
无论如何,建模过程总是繁杂的,而且最糟糕的是,和传统的工程设计不同,这种繁杂的软件设计往往跟最终的软件产品根本就是天壤之别。我们最后拥有的是一大堆跟最终代码几乎完全没关系的模型以及一大堆几乎丝毫看不出设计思想的代码。因此,对于一个稍微复杂的项目,我们对其规模和工作量都是难以准确估计的。
即使我们使用昂贵的工具来试图同步设计和产品代码,经常也只是徒劳而已,过高的设计成本和维护成本使得这种投资毫无意义。更多的公司选择了手工作坊式的开发方式,只是在开发过程中借助XP或CMMI来扳回一点面子。在项目完成后,再根据最终产品进行建模——当然,这已经不再是设计了,只能算是“文档化”罢了。
基于UCL的设计过程,并不是面向对象的设计过程,而是基于传统的分层数据流图,也就是说,整个设计过程是自顶向下的。本来,数据流图对于系统的架构描述并不清晰。但是,分层数据流图使得这样的架构描述至少可以达到“将就能用”的程度。更重要的是,由于最底层的数据流图和UCL图可以方便地对照,由UCL图作为设计和产品代码的中间媒介,从而将设计过程和开发过程连成一气。
实现UCL组件和数据流处理的一一对应,更重要的意义在于使得系统的规模和工作量将变得更直观、更准确,也更易于统计,而不像过去一样只能依靠项目负责人的经验判断。
而在购买或自行填充了UCL库之后,通过实现UCL组件和代码的一一对应,不仅可以实现建模和实现的互相参照,开发成本也可以大大降低。
以下是一个简单系统的功能需求说明、分层数据流图以及一个对应的UCL图,仅供参考:
UCL解决的问题:重用
重用,是件说起来容易做起来难的事情。利用面向对象技术,设计一个可重用构件的工作量大约是“一次性”构件的2~3倍。
这里的问题是通用性和专用构件的矛盾。人们在得到一个专用构件之后,在将其重用的过程中往往需要针对特定需求进行修改。不断的重用导致不断的修改,并催生了使这个构件日趋通用化的需求,最终专用构件变成了通用构件。但构件的通用性带来了高昂的维护成本,最终人们对其失去了兴趣,又开始重新开发专用构件。如是循环。
高昂的重用成本带来了昂贵的可重用构件,也就导致更多的人决定自行开发各种形态各异的轮子,最终这些轮子又陷入了成本高昂的重用泥沼中。
UCL组件和传统的构件很不相同,其特点如下:
- 关系简单,只有简单的“连接”和“组合”两种关系;
- 接口简单,只具备“输入”、“输出”等几个简单的接口;
- 处理简单,通常只执行很简单的处理,保证了低廉的开发成本和良好的可维护性,同时保证了可重用性。
通过两种简单的关系,我们就可以将各种简单的UCL组件“搭建”成具备复杂功能的复合组件,最终搭建成整个系统。也就是说,尽管UCL的设计过程是自顶向下的,但开发过程却是自底向上的,这就在保持低成本的同时兼顾了通用性,从而得以解决传统面向对象开发方法中的通用和专用的矛盾。
在具体的开发过程中,我们在得到UCL图之后,首先是实现图中的各个组件。这些组件的代码和组件一一对应,存放在代码库中,日后再次使用该组件时无需再次编写,由于组件的规模很小,可重用性就比传统的构件要高得多。
在所有的底层组件都有代码实现之后,借助专用的CASE工具,输入UCL图并连接代码库,即可直接生成完整的系统。
当然,以上是一个理想化的过程。其主要的一些应用问题如下(当然还有别的):
如何输入UCL图?
我的目标是设计出一个类似那个声音合成软件的画图软件,这样就能以可视化的方式来拖拉各种组件并创建连接。但是作为一种简单的替代方案,我也设计出了一种脚本语言,用文本的方式来表达UCL图中所包含的所有元素。以下是一个脚本示例:
cnitblog.addone.AID3Tagger.Mp3FileListEditor
AddFileButton:JButton,title="添加文件",proc>OpenFileDialog.show;
OpenFileDialog:JFilteredFileChooser,multiSelection="true"/filterString=".mp3|MP3音乐文件",show>Mp3FileList.add(fileList:File[]);
AddDirButton:JButton,title="添加文件夹",proc>OpenDirDialog.show;
OpenDirDialog:JFileChooser,selectionMode="directories",show>GetFilesInDirRecursively.proc(dir:File);
GetFilesInDirRecursively<static>,filterString=".mp3",proc>Mp3FileList.add(fileList:File[]);
RemoveFileButton:JButton,title="移除文件",proc>FileListBox.remove;
SourceEncodingBox:JComboBox,editable="false"/data="GBK|UTF-8|UTF-16-BE|UTF-16-LE",
proc>FileListBox.changeEncoding(encoding:String);
如何“真正地”实现UCL图到具体代码的转换?
这种转换的问题在于,对于UCL组件的属性应该如何转换?我目前的想法是,使用一个映射文件,将UCL组件和具体组件的属性映射关系保存在其中,例如:
version=0.1
type=java.swing
JButton.title=setText(String)
JFileChooser.multiSelection=setMultiSelectionEnabled(boolean)
JFileChooser.filterString=setFileFilter(Custom.FileFilter)
JFileChooser.selectionMode=setFileSelectionMode(Custom.FileSelectionMode)
_converter_.FileSelectionMode.source=String
_converter_.FileSelectionMode.target=int
_converter_.FileSelectionMode.rule=files|0|directories|1|files_and_directories|2
另外一个可供考虑的方法是,开发UCL组件来专门对属性进行处理。这样即可在目前的UCL框架中实现,无需增加新的特性。
如何使用UCL设计GUI界面?
不得不承认,这将是个很棘手的问题。对于java、.net来说,情况要好些,因为界面也是使用代码来构建的。但对于VB之类来说,由于界面是采用专用格式来保存,情况就麻烦得多了。而且,即使是java和.net,没有可视化的界面设计,也将是没有吸引力的解决方案。
我目前的想法有两个:一个是将具体的GUI组件包装为UCL组件,不过这样一来有可能会加大界面设计的工作量;一个是干脆完全将GUI部分从UCL中剔除出去,即这一部分使用传统的开发方法,但应该保证GUI组件和UCL组件能够彼此调用。
UCL解决的问题:维护
在传统的软件开发项目中,维护成本所占的比例是很大的。这里主要指的是由需求变更、BUG修改等引起的软件修改和更新工作。对于小的修补工作来说,每次修改的成本并不高,但是在多次修改之后,原有的代码往往就变得面目全非了。这时,如果再面对一个较大的需求变更,往往我们情愿重新开发,而不愿意再继续维护这个系统。
造成这种问题的原因有很多,一方面是因为无法从代码中看出设计,不得不求助于文档,这对于一些缺乏文档的项目来说是致命的;另一方面是,在修改之后没有及时地同步文档和模型,这种情况在发生若干次之后,这个项目离寿终正寝也不远了。
如前所述,UCL图是设计和实现之间的一座桥梁,对于解决这类维护问题应该是很有效的。修改工作和开发时一样,都只需对图进行操作,所有的设计都可一目了然。这种方式,有利于提高维护效率和降低维护成本。
总结和思考
由以上讨论可以看到,由于平滑连接了设计过程和实现过程,UCL技术具有许多独特的优点。作为一种全新的技术,UCL不仅仅是一个组件库的理论,更代表着全新的设计思想和开发方式。作为一个抽象的组件库,UCL更不是VCL、MFC等的替代品,而是这些具体库的包装者和使用者。
由这些理论可见,如果UCL技术能被证明可用于实际的商业开发中,目前的整个设计和开发方式都将发生巨大的改变。不过,由于理论是全新的,而且对开发语言具有依赖性,基于UCL的设计和开发过程更需要有一系列的CASE软件支持,这可能会带来一系列的问题,但更有可能形成一个巨大的商机。
不过很遗憾,尽管我已经在一些小软件中成功的应用了相关理论,但目前,各种应用问题还远没有得到完全解决。例如,问题1中所说的可视化设计器和脚本解析器,还都没有实现。而问题2、3的解决方法还需要进行深入讨论。这一切,都需要耗费大量的时间和精力。
好在,即使不能完全实现由UCL图向代码的转换,这个理论至少也能帮助我们在设计前期就对一个项目的规模和工作量进行更为准确的统计。
限于个人的时间、金钱和精力,我不得不暂时放弃UCL的相关思考。把东西扔在这里,只希望机缘巧合,居然能起抛砖引玉之效,那也不枉我连日的冥思苦想了。