劣质代码带来的劣质体验
时下很流行讨论用户体验。当我们浏览一个网站或者使用一个软件的时候,如果它的界面组织的不够友好,我们就会感到沮丧,离开,而且不想再次回来。程序员修改程序的时候所产生的体验就是对代码的体验。他们时而因为代码的精彩而感到兴奋;时而因为代码的结构混乱而愤恨不已;时而因为某个精彩的方案而感到激动;时而因为某个糟糕的命名而焦头烂额。就像网站的用户体验包括页面布局、文字、图片、链接等各种属性一样,代码通过其命名、注释、风格和结构给程序员带来不同的体验。
糟糕的代码不仅仅给程序员带来糟糕的体验,它同时带来质量问题,也让人们为它付出额外的心血,却难以产生额外的价值。对于程序员来说,劣质代码就是噩梦,它带来各种不愉快的体验。本系列将讨论如何通过改善编码的方式来改善阅读体验,同时对软件质量进行提升。因此,本系列并不仅仅是面向程序员的,它同样适用于公司经营层、项目经理、过程管理人员和测试人员,以及其他我没有列出但是能够从中获益的人们。
如果你在编码中遇到了下列问题,那么你多半是遇到了糟糕的代码:
1.Bug难以复现
常常会有bug以“无法复现”而关闭。bug为什么难以复现呢?因为形成bug现象的原因条件组合太复杂了。很难再次通过有效地组织操作手法来复现bug,或者说,当时能够发现这个bug纯属是偶然因素。
2.Bug难以追踪
虽然Bug的复现步骤已经明确了,但是Bug的发生原因却找不到,不知道到底是A变量引起的还是B方法造成的。为了能够跟踪Bug,需要在诸多函数方法中打印无数个Log,需要跟踪大量的变量值变化情况。需要设置若干个断点。这花费若干时间,而下次Bug再次发生时,还要重复上述步骤。
3.Bug难以修改
Bug的产生原因费尽千辛万苦找到了,但是却不敢动手,因为涉及到的函数(方法)被调用的地方太多了,不知道修改了之后会引起什么问题;又或者涉及到的变量就算设置了我们想要的值却又被莫名其妙的重置了,因为某个函数(方法)被调用了6次。你甚至难以确定为什么要对同一个函数(方法)调用6次。
4.难以扩展
当用户提出要求要增加新的功能时,你感到很为难,因为已经写好的代码好像无从下手了。但是硬着头皮你开始修改,却又忐忑不安。你发现新加的功能如果想要实现,就必须要放弃另外的一个功能,但是管理者又要求你鱼和熊掌兼得。很显然,这不像找一头会捉鱼的熊那么简单。
5.代码难于理解
刚接手代码的时候你打算从中找到业务需求上所描述的流程关系,可是并没有那么明显的入口让你开始着手,你只好一行一行的看,但是更多的时候你在滚动鼠标滚轮,而不是看代码,更多的时候你在各个函数(方法)中跳跃,而不是理解逻辑关系。更多的时候你在琢磨那句过时了的注释,或者在庞大的注释中挑出代码,就好像从沙子中挑米一样。
6.难以自动测试
你想用自动测试来保证代码的质量,但是代码却并不那么爽快,调用代码所涉及到的外部数据足够你准备上一整天的,而且,就算你准备好数据了,代码也不是那么容易调用的。 它可能要求必须从UI上来运行,因为其中有UI相关的处理;或者它要求你必须传递一个你无法准备的Context参数。总之,想要启动自动测试,你得先对代码做出手术。
是什么带来这些不愉快的体验呢?
让我们从几个小例子开始吧,
1.命名不易懂
请看这4个命名,它们都是什么意思。
“Todofuken”,“Research”,“showFlag”,“doLogin”
“Todofuken”是什么意思?有人居然告诉我这是to do fuck的意思。正确的答案是“都道府县”,这是一个日语词汇,表示省级行政单位。那么province产生误解的可能性就降低多了。
“research”应该不会翻译错了吧,“研究”是吗?不,作者想表达的是“重新搜索”,为什么不用searchAgain呢?
“showFlag”一定是“显示标志”了,没错,这次对了,但是“visibility”会更好的表达这一意思。
“doLogin”呢?“执行登录”,可以肯定地说,但是“authenticate”更为贴切。
这就像“字母连连看”游戏一样,考验我们的英语理解力。
2.注释不友好
1 //Tom Modify Start 2009/10/12 #3124==> 2 //setViewVisible(true); //Tom removed 3 resetFocus(); //Tom added 4 //Tom Modify End 2009/10/12 #3124<==
这是Tom为了修改3124号bug,而作的改动,将setViewVisible替换成了resetFocus()。那为什么不直接将setViewVisible去掉替换成resetFocus呢?据说是因为想要留下修改履历,以便后查。但是这样做的直接结果是:代码变得更难以阅读了——因为原来的代码被若干行用不着的注释分隔开了。可见滥用注释是问题的另一个起因。
这就像“翻牌找对”游戏一样,考验我们的记忆力。
3.代码过长
有一段长达600行的函数(方法),到处都是if-else,需要在其中的6个条件下增加一个关于焦点的处理。这是多么悲催一件事情,那些恼人的条件不在同一个屏幕上,需要不停地滚动鼠标滚轮,才能够看到最大的那个嵌套,然后才能看到里面的直到最小的那个。
这就像“是男人就下一百层”游戏一样,考验我们的忍耐力。
4.结构混乱
1 public class GameView { 2 public Hero hero; 3 }
1 public class Hero { 2 public GameView view; 3 }
这就像“无间道”一样,考验我们的辨别能力
5.逻辑难以理解
代码1:
1 if (model.getAnimationRun() != false) { 2 return; 3 }
这段代码的意思是:当getAnimationRun()不是false的时候就返回,getAnimationRun什么时候会返回false?哦,不我指的是true。
代码2:
1 if (model.isPlayingAnimation()) { 2 return; 3 }
这段代码的意思是:当model正在播放动画的时候就返回,我才不关心它的true,false是怎么算出来的,我只关心这段代码的意思是,正在播放动画就返回。
这就像“数独”一样,考验我们的逻辑思维能力。
6.重复和类似
代码1:
1 public void deleteModels(Model[] models) { 2 for (Model m : models) { 3 m.dispose(); 4 m = null; 5 } 6 }
代码2:
1 public void deleteModels(Object[] objects){ 2 for (Object o : objects) { 3 ((Model)o).dispose(); 4 o = null; 5 } 6 }
这两段代码出自不同的两个程序员,他们为同一个功能贡献了两段风格近似的代码。
这就像“大家来找茬”一样,考验我们的眼力。
7.嵌套过深
代码1:
1 public String nextChar(String text) { 2 for (String[] row : table) { 3 for (int i = 0; i < row.length(); i++) { 4 if (column.equals(text)) { 5 if (i == row.length() - 1) { 6 return row[0]; 7 } else { 8 return row[i + 1]; 9 } 10 } 11 } 12 } 13 }
这段代码嵌套有4层。想要表达的是从一个二维数组中找到当前字符所在行的下一个字符,如果当前位于行尾,则返回行首。
但是仔细阅读之后,就可以看到改进的余地。
代码2:
1 public String nextCar(int rowIndex, String text) { 2 for (int i = 0;i < table[rowIndex].length(); i++) { 3 return tabl[rowIndex][(i + 1) % table[rowIndex].length()]; 4 } 5 }
改进之后只有1层嵌套。
像这样的若干层嵌套就像圈套一样考验我们的警惕性。
如果我们愿意列,还有很多考验我们的地方。换做你,你是愿意玩那些游戏,还是像被面试一样每天重复着接受这些能力的考验?
那么又是什么引起了这样的代码书写风格呢?请看下一章。