可维护性和可复用性

110 阅读7分钟

可维护性

系统腐烂

导致一个软件设计的可维护性较低,也就是说会随着性能要求的变化而“腐烂”的真正原因有四个:过于僵硬、过于脆弱、复用率低、黏度过高。

过于僵硬

很难在一个软件系统里加入一个新的性能,哪怕是很小的都很难。这是因为加入一个新性能,不仅仅意味着建造一个独立的新模块,而且因为这个新性能会波及其它模块,最后变成跨越几个模块的改动。使得一个起初只需要几天的工作,最后演变成持续两个月的连续作战。

由于这种设计上的缺陷,使得项目经理不敢轻易向系统加入新功能。这就造成一个软件系统一旦做好,就不能增加新功能的僵硬化情况。

过于脆弱

与软件过于僵硬同时存在的,是软件系统在修改已有代码时过于脆弱。对一个地方的修改,往往会导致看上去没有什么关系的另一个地方发生故障。尽管在修改之前,设计师会竭尽所能预测可能的故障地点,但是在修改完成之前,系统的原始设计师甚至都无法确切预测到可能会波及到的地方。

这种一碰就碎的情况,造成软件系统过于脆弱。

复用率低

所谓复用,就是指一个软件的组成部分,可以在同一个项目的不同地方甚至另一个项目中重复使用。

每当程序员发现一段代码、函数、模块所做的事情是可以在新的模块或者新系统中使用的时候,他们总是发现,这些已有的代码依赖于一大堆其它的东西,以至于很难将它们分开。最后,他们发现最好的办法就是不去“碰”这些已有的东西,而是重新写自己的代码。他们可能会使用源代码剪贴的办法,以最原始的复用方式,节省一些时间。

这样的系统就有复用率低的问题。

黏度过高

有的时候,一个改动可以以保存原始设计意图和原始设计框架的方式进行,也可以以破坏原始意图和框架的方式进行。第一种办法无疑会对系统的未来有利,第二种办法是权宜之计,可以解决短期的问题,但是会牺牲中长期的利益。

如果第二种办法比第一种办法要容易得多的话,程序员就有可能牺牲中长期的利益,采取权宜之计;在模块中搭建一个短路桥,或者在一个通用的逻辑中制造一个特例,以便解决眼前的需要。

一个系统设计,如果总是使得第二种办法比第一种办法容易,就叫做黏度过高。一个黏度过高的系统会诱使维护它的程序员采取错误的维护方案,并惩罚采取正确维护方案的程序员。

设计目标

一个好的系统设计应该有如下的性质:可扩展性(Extensibility)、灵活性(Flexibility)、可插入性(Pluggability)。

可扩展性

新的性能可以很容易地加入到系统中去,就是可扩展性。相对应的是“过于僵硬”的属性。

灵活性

可以允许代码修改平稳地发生,而不会波及到很多其它的模块,这就是灵活性。相对应的是“过于脆弱”的属性。

可插入性

可以很容易地将一个类抽出去,同时将另一个有同样接口的类加入进来,这就是可插入性。相对应的是“黏度过高”的属性。

可复用性

软件的复用的好处有:第一,较高的生产效率;第二,较高的软件质量;第三,恰当使用复用可以改善系统的可维护性。

一个可以重复使用的软件成分可以为将来的使用节省费用。一个构件被复用的频率越高,构件的初始开发投资就相对越少。

传统的复用

代码的复用

源代码的剪贴往往作为程序员自发的代码复用形式出现。复用所能节省的初期投资十分有限。

算法的复用

各种算法比如排序算法得到了大量的研究。现在几乎不会有人在应用程序编程时试图建立自己的排序算法,通常的做法是在得到了很好的呀牛的各种算法中选择一个。

数据结构的复用

与算法的复用相对的,是数据结构的复用。如队、栈、队列、列表等数据结构得到了十分透彻的研究,所有的计算机教学都要详细描述这些数据结构。

两者的关系

传统的复用方案的一个致命缺陷就是复用常常是以破坏可维护性为代价的。比如两个模块 A 和 B 同时使用另一个模块 C 中的功能。那么当 A 需要 C 增加一个新的行为的时候,B 有可能不需要、甚至不允许 C 增加这个新行为。如果坚持使用复用,就不得不以系统的可维护性为代价;而如果从保持系统的可维护性出发,就只好放弃复用。

面对对象设计的复用

在一个像 Java 这样的面向对象的语言中,数据的抽象化、继承、封装和多态性是几项最重要的语言特性,这些特性使得一个系统可以在更高的层次上提供可复用性。数据的抽象化和继承关系使得概念和定义可以复用;多态性使得实现和应用可以复用;而抽象化和封装可以保持和促进系统的可维护性。这样一来,复用的焦点不再集中在函数和算法等具体实现细节上,而是集中在最重要的含有宏观商业逻辑的抽象层次上。

抽象层级是比实现细节更值得强调的复用焦点,因为它们是在提高复用性的同时保持和提高可维护性的关键。

既然抽象层次是一个应用系统做战略性判断和决定的地方,那么抽象层次就应当是较为稳定的,应当是复用的焦点。如果抽象层次的模块相对于具体层次的模块的话,那么具体层次内部的变化就不会影响到抽象层次的结构,所以抽象层次的模块的复用就会较为容易。

对可维护性的支持

首先,恰当地提高系统的可复用性,可以提供系统的可扩展性。允许一个具有同样接口的新的类代替旧的类,是对抽象接口的复用。客户端依赖于一个抽象的接口,而不是一个具体实现类,使得这个具体类可以被另一个具体类所取代,而不影响到客户端。

系统的可扩展性是由“开-闭”原则、里氏代换原则、依赖倒转原则和组合/聚合复用原则保证的。

其次,恰当地提高系统的可复用性,可以提高系统的灵活性。在一个设计得当的系统中,每一个模块都相对于其它模块独立存在,并只保持与其它模块的尽可能少的通信。这样一来,在其中某一个模块发生代码修改的时候,这个修改的压力不会传递到其它模块。

系统的灵活性是由“开-闭”原则、迪米特法则、接口隔离原则所保证的。

最后,恰当地提供系统的可复用性,可以提高系统的可插入性。在一个符合“开-闭”原则的系统中,抽象层封装了与商业逻辑有关的重要行为,这些行为的具体实现由实现层给出。当一个实现类不再满足需要,需要以另一个实现类取代的时候,系统的设计可以保证旧的类可以被“拔出(Unplug)”,新的类可以被“插入(Plug)”。

系统的可插入性是由“开-闭”原则、里氏代换原则、组合/聚合复用原则和依赖倒转原则保证的。