5.8 Spaghetti Code(面条代码)
|
反模式名称:Spaghetti Code 最常见规模:应用层 重构方案名称:Software Refactoring(软件重构)、Code Cleanup(代码清理) 重构方案类型:软件 根源:无知、懒惰 不平衡的力量:复杂性管理、变化管理 轶事证据:“啊!真乱哪!”“你确实知道这种语言支持不只一个函数,不是吗?”“重写这段代码比试着修改它更容易。”“软件工程师不写Spaghetti Code。”“你的软件结构的质量是为将来的修改和扩展进行的投资。” |
5.8.1 背景
Spaghetti Code反模式是经典的,也是最著名的反模式。从编程语言的发明之日起,它就以这样或那样的形式出现了。非面向对象语言似乎更容易受该反模式的影响,但在那些尚未完全掌握面向对象所蕴含的高级概念的开发人员当中,这个反模式也相当常见。
5.8.2 一般形式
Spaghetti Code就是看上去几乎没有软件结构的一段程序或一个系统。编码和逐步的扩展严重破坏了软件的结构,以至于即使对它的原始开发人员来说,如果他离开该软件一段时间,也会弄不清系统的结构。如果是使用面向对象语言进行开发,该软件可能会包含少量对象,它们的方法具有非常庞大的实现,调用一个单独的、多阶段的处理流程。而且,对对象方法调用的可预测性很高,系统中对象之间的动态交互少到可以忽略的程度。很难维护和扩展该系统,也没有机会复用其他相似系统中的对象和模块。
5.8.3 症状和后果
● 在代码挖掘后,可以发现只有部分对象和方法看起来适于复用。挖掘Spaghetti Code的回报相对于投入常常相当可怜。在做出进行挖掘的决策之前就要考虑到这一点。
● 方法是面向处理过程的;实际上,对象常常被按照处理过程命名。
● 执行流由对象的实现控制,而不是由对象的客户控制。
● 对象间只有最小限度的关系。
● 很多对象方法没有参数,使用类变量或全局变量进行处理。
● 对象的使用模式具有很强的可预测性。
● 代码难以复用,进行复用的方式往往是通过复制代码。很多时候,根本就没有考虑过代码复用。
● 难以留住那些具有面向对象才能的人。
● 失去了面向对象带来的益处。没有使用继承来扩展系统;没有使用多态机制。
● 后续维护工作会加剧这个问题。
● 软件迅速达到回报渐小点;维护现有代码集的成本比从头开始开发新方案的成本更高。
“先做重要的事,不重要的就不要做。”
——Shirley Conran(英国女作家)
5.8.4 典型原因
● 对面向对象设计技术缺乏经验。
● 没有适当的指导;失效的代码评审。
● 实现前未进行设计。
● 通常是开发人员孤立工作的产物。
5.8.5 已知例外
在接口是连贯的而只有实现是面条形式时,Spaghetti Code反模式在一定程度上是可以接受的。这就像封装一段非面向对象的代码。如果该构件的寿命很短,而且清晰地与系统其他部分隔开,那么可以忍受一定量的低质代码。
软件行业的现实就是对软件质量的考虑通常要让位于商业考虑,而某些时候,商业上的成功取决于尽快交付某个软件产品。如果软件架构师和开发人员不熟悉要处理的领域,可能较好的做法是先开发(质量一般的)产品来获得对领域知识的了解,目的是晚些时候再采用改进的架构来设计产品[Foote 1997]。
5.8.6 重构方案
软件重构(或代码清理)是软件开发中的重要部分[Opdyke 1992]。超过70%的软件成本都是由扩展造成的,所以维持一个支持扩展的连贯软件结构非常关键。当为了支持预料之外的需求而破坏了结构时,代码支持扩展的能力就受到了限制,直到最终不复存在。不过,“代码清理”这个术语对那些高层管理者没有吸引力,所以最好换个类似“软件投资”之类的词来讨论这个问题。毕竟,从非常实际的角度来说,代码清理是对软件投资的维护。结构良好的代码会有更长的生命周期,可以更好地支持业务领域和内在技术的变化。
理想情况下,代码清理应该是开发过程的自然组成部分。在代码中每增加一个功能(或一组功能),就应该接着进行代码清理来恢复或改进代码结构。根据增加新功能的频率,代码清理可以按照小时或天来进行。
代码清理还支持对性能的改善。性能优化一般遵循90/10规则,也就是要达到最优性能的90%,只需要对10%的代码进行修改。对单子系统编程或应用编程,性能优化常常会造成在代码结构上的折中。第一步的目标是获得令人满意的结构;然后是通过测量确定对性能很关键的代码的存在位置;最后是谨慎地引进必要的结构折中来改善性能。有时必须为了重要的系统扩展而取消软件中为了改善性能而做出的变化。为了给将来的版本保留这些软件结构,对这些地方需要进行额外的文档说明。
解决Spaghetti Code反模式的最佳方法是进行预防;也就是说先思考,然后在编写代码前建立一个行动计划。但是,如果代码集已经退化到无法维护的地步,而且软件重新设计也不可行,还是可以采取一些其他步骤来避免问题的恶化。首先,在维护过程中向Spaghetti Code代码集中加入新功能时,不要用类似于刚好满足新需求的风格来修改代码,而是总是花一些时间来把已有系统重构到更可维护的形式。软件重构包括对已有代码进行下列操作:
(1)使用访问函数来获得对类中成员变量的抽象访问。编写新的和重构的代码时使用这些访问函数。
(2)把一段代码转换成可以在将来的维护和重构工作中复用的函数。抵制Cut-and-Paste反模式(接下来将讨论)的诱惑至关重要。应该使用Cut-and-Paste的重构方案来修补以前实现的Cut-and-Paste反模式。
(3)重新排列函数的参数,从而在整个代码集中获得更高的一致性。即使是不好的但是一致的Spaghetti Code,也比不一致的Spaghetti Code好维护。
(4)移除将会或已经无法访问的代码。反复地未能发现和移除过时代码是造成Lava Flow反模式的主要原因。
(5)重命名类、函数或数据类型来遵守企业或行业标准,符合可维护实体的要求。大部分软件工具都支持全局重命名。
简而言之,一旦需要修改代码集,就在资源许可范围内致力于积极地重构和改进Spaghetti Code。单元测试和系统测试工具及应用软件的使用,对保证重构没有给代码集立即产生任何新缺陷非常有效。经验表明,软件重构带来的效益远远超过了额外的修改可能产生新缺陷的风险。
如果可以选择预防Spaghetti Code,或者如果你可以完全重新设计一个Spaghetti Code应用,可以采取下面这些预防性手段:
(1)不管对领域的理解程度如何,坚持采用正确的面向对象分析过程来建立领域模型。对任何中等规模或大规模的项目来说,建立一个领域模型作为设计和开发的基础都是至关重要的。如果对领域理解得非常充分,让人觉得不需要领域模型了,可以用“如果是那样的话,建立一个模型的时间也就是可以忽略的”来反驳。如果这个时间确实可以忽略,就礼貌地承认你开始时错了。否则的话,花掉的时间就足以证明它的重要性了。
(2)在建立了领域模型来解释系统需求及要解决的可变性范围之后,建立一个隔离的设计模型。虽然使用领域模型作为设计的起点也是有效的,但必须把领域模型保持原样以保留那些有用的信息。否则,如果允许它直接发展成设计模型,就会失去那些信息。设计模型的用途是抽取领域对象之间的共同性,通过抽象来阐明系统中必需的对象和关系。如果正确进行,它会建立软件实现的边界。实现只应用于满足系统需求,这些需求或者是由领域模型明确指出的,或者是系统架构师或高级开发人员所预期的。
(3)建立设计模型时,重要的是保证把对象都分解到可以被开发人员完全理解的层次。要让开发人员而不是设计人员相信软件模块易于实现。
(4)一旦对领域模型和设计模型都进行了第一遍设计,就可以根据设计建立的计划开始实现。设计不必是完整的,其目的只是在于总是应该根据某个预定计划来进行软件构件的实现。开始开发以后,继续以增量的方式检验领域模型的其他部分,并设计系统的其他部分。随着时间的流逝,可以精化领域模型和设计模型,以接纳需求收集中的发现和设计决策,解决实现方面的问题。再次强调,如果有整体软件开发过程,在实现前说明需求和设计而不是让它们同时发生,那么出现Spaghetti Code的可能性可以小得多。
5.8.7 示例
这是新接触面向对象软件开发的人会表现出的一个常见问题,他们把系统需求直接映射成函数,使用对象作为组合相关函数的地方。每个函数都包含完整实现特定任务的整个处理流。例如,下面的代码段包含类似initMenus()、getConnection()和executeQuery()的函数,它们完整地执行了所说明的操作。每个对象方法都包含了独立的处理流,按照完成该任务所需的顺序执行所有的步骤。对象在连续调用之间只保留很少的状态信息,或者根本没有。类变量只是用于临时保存单个处理流的中间结果。
代码清单5-1
public class Showcase extends Applet
implements EventOutObserver {
//Globals
String
homeUrl="http://www.webserver.com
/images/"
;
int caseState;
String url="jdbc:odbc:WebApp";
Driver theDriver;
Connection con=null;
ResultSet rs,counter;
int theLevel;
int count=0;
String tino;
int [] clickx;
int [] clicky;
String [] actions;
String [] images;
String [] spectra;
String showcaseQuery=null;
TextArea output=null;
Browser browser=null;
Node material=null;
EventInSFColor diffuseColor=null;
EventOutSFColor outputColor=null;
EventOutSFTime touchTime=null;
boolean error=false;
EventInMFNode addChildren;
Node mainGroup=null;
EventOutSFVec2f coord=null;
EventInSFVec3f translation=null;
EventOutSFTime theClick=null;
Image test;
int rx,ry;
float arx,ary;
int b=0;
Graphics gg=null;
//Initialize applet
public void init() {
super.init(); setLayout(null);
initMenus();
output=new TextArea(5, 40);
add(output);
browser=(Browser)
Browser.getBrowser((Applet)this);
addNotify(); resize(920,800);
initUndoStack();
caseState=0; theLevel=0;
setClock(0);
try { theDriver=new postgresql
.Driver(); }
catch(Exception e) {};
try { con=DriverManager
.getConnection(
"jdbc:postgresql://www.webserver
.com/WebApps",
"postgres","");
Statement stmt=con
.createStatement();
showcaseQuery="SELECT sid,
case,
button,text , name, actions FROM
WebApp
WHERE case="+caseState+" and
level="+theLevel+";";
rs=stmt.executeQuery
(showcaseQuery);
count=0; while (rs.next())
count++;
System.out.println("Count=
"+count+"\n");
rs=stmt.executeQuery
(showcaseQuery);
}
catch(Exception e) {System.out
.println(
"Error connecting and running:
"+e);};
nextButton=new
symantec.itools.awt.ImageButton();
lastButton=new
symantec.itools.awt.ImageButton();
try {
nextButton.setImageURL(new
java.net.URL(
"http://www.webserver.com:8080/
images/next.jpg"));
if (count<7) nextButton
.setVisible(false);
else nextButton.setVisible
(true);
lastButton.setImageURL(new
java.net.URL(
"http://www.webserver.com:8080/
images/last.jpg"));
}
catch(Exception e) {};
imageButtons=new
symantec.itools.awt.ImageButton[6];
11=new
symantec.itools.awt.shape.Horizontal
Line();
12=new
symantec.itools.awt.shape.Horizontal
Line();
v1=new
symantec.itools.awt.shape.Vertical
Line();
v2=new
symantec.itools.awt.shape.Vertical
Line();
bigspectralabel=new
java.awt.Label("Spectra");
gtruthlabel=new
java.awt.Label("GroundTruth");
clickx=new int[6];
clicky=new int[6];
actions=new String[6];
images=new String[6];
spectra=new String[6];
imageLabels =new java.awt.Label[6];
for (int I=0; i<6 ; i++) {
imageButtons[i]=new
symantec.itools.awt.ImageButton();
imageLabels[i]=new java.awt
.Label();
actions[i]=new String();
images[i] =new String();
spectra[i] =new String();
};
for (int i=0; i<6 ; i++) {
try{
rs.next();
tino=rs.getString(4);
System.out.println(tino+"\n");
actions[i]=rs.getString(6);}
catch(Exception e) {System.out
.println("SQL
Error :"+e);}
try{
System.out.print(tino+ln"\n");
int len=tino.length();
if (tino.startsWith
("INVISIBLE")) {
imageButtons[i]
.setVisible(false);
imageLabels[i]
.setVisible(false);}
else {
imageButtons[i].setImageURL(
new java.net.URL
(homeUrl+tino));
imageButtons[i].setVisible
(true);
imageLabels[i].setText
(rs.getString(5));
imageLabels[i].setVisible
(true);
}
} catch (Exception e) { System.out
.println(
"Died in accessor statement:
"+e);
}
}
11.reshape(0,6,775,1);add(11);
12.reshape(0,120,775,1);add(12);
v1.reshape(0,6,1,114);add(v1);
v2.reshape(775,6,1,114);add(v2);
bigspectralabel.reshape
(460,122,200,16);
bigspectralabel.setVisible(false);
gtruthlabel.reshape(124,122,200,16);
gtruthlabel.setVisible(false);
add(bigspectralabel);add
(gtruthlabel);
nextButton.reshape(2,12,84,40);
add(nextButton);
lastButton.reshape(2,56,84,40);
add(lastButton);
imageLabels[0].reshape(124,12,84,16);
add(imageLabels[0]);
imageButtons[0].reshape(124,30,84,84);
add(imageButtons[0]);
imageLabels[1].reshape
(236,12,84,16);
add(imageLabels[1]);
imageButtons[1].reshape
(236,30,84,84);
add(imageButtons[1]);
imageLabels[2].reshape
(348,12,84,16);
add(imageLabels[2]);
imageButtons[2].reshape
(348,30 ,84,84);
add(imageButtons[2]);
imageLabels[3].reshape
(460,12,84,16);
add(imageLabels[3]);
imageButtons[3].reshape
(460,30 ,84,84);
add(imageButtons[3]);
imageLabels[4].reshape
(572,12,84,16);
add(imageLabels[4]);
imageButtons[4].reshape
(572,30 ,84,84);
add(imageButtons[4]);
imageLabels[5].reshape
(684,12,84,16);
add(imageLabels[5]);
imageButtons[5].reshape
(684,30 ,84,84);
add(imageButtons[5]);
// Take out this line if you don’t
use
// symantec.itools.net.RelativeURL
symantec.itools.lang.Context
.setDocumentBase(
getDocumentBase());
//{{INIT_CONTROLS
//}}
}
5.8.8 相关解决方案
● Analysis Paralysis。该反模式是把Spaghetti Code的解决方案带到了逻辑极限的结果。它不是在没有设计来指引代码的整体结构时即兴开发代码,而是产生一个详细的设计,却没有地方可以着手开始实现。
● Lava Flow。该反模式常常包含多个Spaghetti Code的样本,阻碍对已有代码集的重构。在Lava Flow中,代码集在生命周期中的某些时候具有特定的逻辑目的,但是其中的某些部分虽然已经过时,却仍然保留在代码集中。
小型反模式:Input Kludge(输入拼凑)
反模式问题
未能通过直接行为测试的软件可能就是Input Kludge的例子。在采用即兴实现的算法(ad hoc algorithm)[②]来处理程序输入的时候就有可能发生这种反模式。例如,如果程序接受用户的自由文本输入,即兴实现的算法可能会错误处理很多合法的和非法的输入字符串组合。关于Input Kludge有一种有趣的说法:“最终用户只要接触键盘一会儿就能让新程序完蛋。”
重构方案
对于非演示用途的软件,应使用具有产品质量的输入处理算法。例如,词法分析和解析软件是很容易获得的自由软件。类似于lex和yacc的程序可以稳健地处理由正则表达式和上下文无关的语法构成的文本。我们建议采用这些技术来建立具有产品质量的软件,以保证正确处理非预期的输入。
变化
许多软件缺陷都是由于用户可访问功能的非预期组合造成的。我们建议在具有图形用户界面的复杂程序中使用功能矩阵。功能矩阵就是在程序中用于在用户操作之前启用或禁用某些功能的状态信息。当用户调用一个功能的时候,功能矩阵将指出需要禁用哪些其他功能,以避免出现冲突。例如,在显示菜单之前常会使用功能矩阵来突出显示或非突出显示某些菜单命令。
背景
程序员受到培训要避免会导致程序和系统崩溃的输入组合。在一次有关OpenDoc的实验培训课上,我们使用了该技术的一个Alpha版,该版本对于产品质量的开发来说还不够稳健。也就是说,它很容易通过一些看起来正确的输入命令和鼠标操作序列让整个操作系统崩溃。学生们的第一天几乎都花在无数次的系统崩溃和等待系统重启上。在体验了这个“崩溃实验室”后,我们很怀疑该版本的稳健程度是否能支持进行任何形式的复杂软件开发。不过到那一周结束的时候,我们已经学会了如何绕过这些限制完成编程任务和输入操作,这些远远超过了我们在第一天形成的期望。我们已经被这些能够避免系统崩溃的输入序列潜移默化了。
小型反模式:Walking through a Mine Field(穿越雷区)
反模式问题

使用当今的软件技术就像是穿越高科技雷区[Beizer 1997a](见图5-18)。该反模式也被称为Nothing Works(没什么能工作)或Do You Believe Magic(你相信魔法吗)?发布的软件产品中有无数的错误;实际上,专家估计在原始代码中每行代码包含2~5个错误。这意味着需要对每行代码进行两处以上的改动才能去除所有错误。毫无疑问,许多产品在发布的时候还远不能支持可运行的系统。一位有见识的软件工程师说过:“没有真正的系统,即使我们的也不是。”
图5-18 穿过雷区的最好办法就是跟着别人走
软件缺陷的位置和后果与它们的表面原因无关,即使一个微小的错误也可能会产生灾难性后果。例如,操作系统(UNIX和Windows等)含有许多已知或未知的安全缺陷,让它们易于受到攻击。而且,因特网显著增加了系统攻击的可能性。
最终用户常常会遇到软件错误。例如,大约有1/7正确拨号的电话呼叫没有被电话系统(一种软件密集的应用)正确完成。而且请注意,与软件失败的频率相比,用户抱怨的比例是相当低的。
商业软件测试的目的是限制风险,尤其是限制支持工作的成本[Beizer 1997a]。对于简装软件产品,每当一个最终用户联系供应商以获取技术支持,他的大部分甚至全部利润就被答复电话花掉了。
相比过去较简单的系统,我们是很幸运的。出现软件错误的时候,最可能的结果就是什么都没有发生。而对于当今的系统,包括计算机控制的载客火车和太空飞船控制系统,错误的后果会是灾难性的。已经有超过半打的重大软件失败导致了超过1亿美元的经济损失。
重构方案
通过在软件测试中的适当投入才能让系统相对而言没有错误。在某些领先的公司中,测试人员的数量超过了编程人员[Cusumano 1995]。对测试过程的最重要改变是对测试用例的配置控制[Beizer 1997a]。典型的系统要求的测试用例软件会是产品软件的5倍。测试软件常常比产品软件更复杂,因为对很多错误的检测都需要对执行时机进行明确的管理。测试软件检测到一个错误时,这个错误更有可能来自测试本身而不是来自被测试的代码。配置控制让对测试软件资产的管理成为可能;例如,可以支持回归测试。
测试的其他有效方法包括对测试执行过程和测试设计的自动化。手工执行测试是劳动密集型的工作,手工测试也没有经过验证的依据。与之相反,自动执行测试可以让测试的运行与构建周期保持一致。可以在没有手工干预的条件下进行回归测试,保证对软件的修改不会在以前测试过的行为中产生缺陷。测试设计自动化支持生成严格的测试套件,有数十种很好的工具可以支持测试设计的自动化。
变化
有一些应用使用形式验证来保证设计没有错误。形式验证包括校对(以数学方式)对需求的满足程度。不过,受过培训来进行这种类型的分析的计算机科学家相对稀少。此外,形式验证的成本很高,而且结果可能是主观的。因此,我们通常对大部分机构都不推荐它作为可行的方法。
软件评审是一种替代方法,在相当大范围的各种机构中都显示出是有效的[Gilb 1995]。软件评审是审查代码和文档产品的正规过程。它要求仔细审查软件文档来寻找缺陷;例如,它建议每个评审员对每页文档花大约45分钟来寻找缺陷。然后,在评审记录会议上列出多个评审员发现的缺陷。文档编辑可以移除这些缺陷,供评审组进行后续的审阅。评审组为初步接受文档和完成评审过程建立质量标准。软件评审是一个特别有用的过程,因为它可以应用于开发的任何阶段,从编写最初需求文档到编码时都可以。
背景
“你相信魔法吗?”这是聪明的计算机专业人员有时会提出的问题。如果你相信当今的软件系统是稳健的,那么你一定相信魔法。
当今软件技术的现实与Stephen Gaskin的Mind at Play[Gaskin 1979]中的一个有趣的小故事很相似。在那个故事中,人们开着闪亮的新车,过着舒适的生活。然而,有一个人想看看世界的真实样子。他找到一个权威人物来消除他感觉中的所有幻觉。然后,当他再次看这个世界的时候,他看到人们都走在街上,假装开着华丽的车子。也就是说,那种豪华生活方式是虚假的。最后,这个人放弃了,要求还原到他觉醒之前的状态。
从某个角度来看,当今的技术非常类似于Gaskin的故事。我们很容易相信我们正在强大稳健的平台上使用成熟的软件技术。实际上,这是一个幻想。软件错误非常普遍,而且也没有稳健的平台来承载它们。
|





