批判小技巧
好多次,同事得意地让我看他们的代码段,常常,我会赞赏一番,毕竟思想的火花来的不容易,但是,与此同时,我也会深刻思考一下这些技巧所带来的后果,随后,我就会很不客气地指出这种技巧可能会让他们如何的难堪。
看看下面这个函数。
function TDM.RetrieveSqlResult(sql: string ): variant;
var
ADOQuery: TADOQuery;
begin
ADOQuery := TADOQuery.Create( nil );
ADOQuery.Connection := ADOConnection;
ADOQuery.SQL.Text := sql;
ADOQuery.Open;
if ADOQuery.RecordCount>0 then
result := ADOQuery.Fields[0].AsVariant
else
result := 0; // 防止结果为空时发生错误
ADOQuery.Free;
end ;
似乎不错,执行一条 Sql 语句后又能返回其第一值,作者的理由是,如果执行的 sql 是 select count(*) from ... 之类的语句,那么这个函数就可以很容易的返回想要的数据。但是作者并没有考虑到这个函数包含了一些隐含的约定,而这些约定就好像钩子,调用者必须清楚这个函数真正干了些什么才有可能正确调用,否则,就会出错。比如,调用者首先必须知道返回的是第一个字段,其次他必须知道返回值 0 可能并不意味着是正确答案。并且这个函数没有提供任何判断返回值" 0 "是否是数据库内数据的信息。这点很要命,其结果也不出所料,正因为这个函数,该程序出了不少错误。
与其说这是技巧,不如说这是花样!
花样这个字眼有点贬义,本文绝无此意,本文指看似精妙,实则错误的代码。
说到这里,本文可以给一些术语稍加规范了。
提示: 大多数所谓的技巧集中的技巧其实是提示,它们指示了某个功能可能的实现方法。并且一般来说这些提示需要再加工才能真正使用。
方法:大部分技巧集是实现某个功能的方法或者知识点,只有少部分是真正的技巧。
技巧:如何编程,如何实现代码的过程才称为技巧。它更类似手段、构思。技巧离不开提示和知识点,否则就是巧媳妇难为无米之炊。技巧关键在于巧上,把技术用得很巧妙,才能算是技巧。
花样:很多时候,人会浮想联翩,当固执自己某种想法时,就可能写出看似巧妙,实则是花样的代码。并且,还可能为自己代码沾沾自喜,以为精妙。
比如这段程序:
function CountSubStr( const SubStr, Source: string ): Integer;
var
i, n, len1, len2: Integer;
begin
result := 0;
i := 1;
len1 := Length(SubStr);
len2 := Length(Source)-len1+1;
while (i <=len2) do
begin
n := pos(SubStr, string (@Source[i])); // 算法的关键,但却错了。
if n > 0 then
begin
inc(result);
inc(i, n+len1-1);
end
else break;
end ;
end ;
此程序曾出现在大富翁论坛上,作者本意是列举了 4 、 5 种计算子串的方法,这个函数是对其中一个低效率算法进行改造,因而使用了 pos(SubStr, string(@Source[i])); 这样的代码。但是作者这个时候也许只关心如何构造精巧的算法从而取得效率了,这行代码会发生致命的错误,它会修改 Source 字符串。对于这个错误详细分析请见本书另一篇文章《综述字符串的操作和使用》。
这样的错误还算比较容易查出,有经验的读者就可能一眼看出问题所在。但是如果类似本文所举的第一个例子,就很难看出问题所在了。下面就说说这些花样的问题所在。
l 花样太注重局部的精巧
但是精巧的局部并不意味着就一定能够带来整体的效率,相反,很有可能成为整体的拖累,经常发生的事情是,为了一个局部的精巧的代码,整体大局上却为它牺牲了很多。
"我这段代码很好,非常精巧,我要保留他,我们修改一下其他的逻辑保证它运行正常吧"。这种想法正是这种思维的写照。这样的思想会使得自己拒绝合理的建议,并且明知道问题也不愿意丢弃。
案例:如 RetrieveSqlResult 这个函数,作者最后说:"我现在就只用于 Select Count(*) from ... 这种情况。" 说实在的,这真不是个好主意。对于一个已经收尾的小项目,再去大面积修改确实必要性不大,但这样的问题从一开始就应该避免。
l 太爱护代码是愚蠢的举动
特别是爱护局部代码,甚至一个小功能,小逻辑,小技巧,很多程序员创造出它们之后,就非常爱护他们,不舍得删去,仍掉,甚至宁可其他地方麻烦点,也想要保留自己这个智慧的结晶。
案例:由于需求的关系,需要设计一个类似级联菜单的下拉框控件来替代一组下拉框,于是另一个开发人员去设计这个功能,在这个功能设计完成之前,先前的开发人员开发了一个转换下拉框为菜单的设计。但事实上这是换汤不换药的设计,并没有完成级联的要求。但这个开发人员非常喜欢自己设计的这个功能,竟然弃好的设计不用,仍然使用自己换汤不换药的设计。
l 懒惰想少写点代码,结果却花费了大量无益的思考时间
常常会有这么一种情况,碰到一个问题,马上想到了一个解决方案,那个方案是最直接的,但需要写很多代码,不原意,于是拼命思考如何构造一个精巧的方法使得能花费很少的代码解决这个问题。在大富翁论坛上,经常可以看见提问者嫌回答者的答案代码太长,而反复问还有没有更简单的方法。并且一问就是很长时间,我很怀疑这些问题是否是为了项目,一个项目怎么经得起在这么一个小问题上如此耗费时间呢?我们这个时代不再是 AppleII 的时代,一个字节都可能要珍惜, CPU 的速度也已经足够惊人,无需我们过分为速度担心,编程本身的效率才是我们该去关心的事情。
l 为局部精巧而营造了复杂环境
局部精巧的构造也许需要依赖更多的环境条件,比如更多的参数,或者意图返回更多的资料和数据。这样的代码一般都比较脆弱。一般开发者不一定能处理好每种可能发生的情况。
案例:本文所给出的第一例子就属于这种情况。局部确实精巧了,但却给调用者带来了非常重的负担。因为那个设计的思路是上层调用者可以传入非常复杂的指令流,但调用者又不清楚这样做是否正确或受到支持。这样上层调用者必须非常了解这个层面上的实现,否则将无法进行工作。而下层又企图不断扩展自己的能力,这样就陷入了上层调用代码不知道该如何开发的境地。此项目的失败也就逐渐成为一种必然了。
l 为局部精巧而设计了复杂或不明确的调用法则
这种情况象一个钩子,普通的调用好比电源插座,不管三七二十一插进去就可以用,但钩子不是这样,如果不清楚这些局部是如何实现或者怎么样的调用约定,那么就可能错误的使用他们。前面 RetrieveSqlResult 这个例子就是这样。尽管设计者会很巧妙的使用它得到一个正确的结果,但当一旦频繁使用,就会逐渐忘记当初的潜在调用约定,这样就可能发生严重错误。
显然,为了局部精巧而损失调用的明确性和函数的功能单一性是不足取的。
l 局部过于精巧的代码损失了代码易维护性和易调试性
这样的代码难以看懂,甚至即使写满了注释,过一段时间再阅读也非常困难。由于其逻辑的过于精巧,需要仔细琢磨才能明白代码的意图,这样的代码是本文所不推荐的。除非出于核心代码效率的需要,否则只会给编写、调试、维护带来莫大的痛苦。因为这样的代码编写困难、调试困难、维护困难。
本人不太赞成使用 C++ 的某些语法,特别是在一个 for 中放入一堆代码,还加上很多 -- 和 ++ ,这样的代码也许非常精巧,但确实极端难以阅读。
long a=10000,b,c=2800,d,e,f[2801],g;
main(){for(;b-c;)f[b++]=a/5;
for(;d=0,g=c*2;c-=14,printf("%.4d",e+d/a),e=d%a)
for(b=c;d+=f[b]*a,f[b]=d%--g,d/=g--,--b;d*=b);}
这是一段号称"外星人程序"的代码,据说是可以计算π小数点后 800 位的程序,本人猜测其算法不会太复杂,但它的写法确实够呛。这段程序是为了特殊目的刻意得这样设计,这问题还不大。如果在一个项目中以这样的想法写代码的话,其结果就是灾难。
不要以为 Delphi 写不出晦涩难懂的代码。
l 过于精巧的结构,需要架设空中楼阁式的逻辑。
这种情况未必不好,而且还可能比较常见。比如要做个检索引擎,那么显然需要制作索引和检索两部分程序,但当程序的结构设计得过于精巧时,就会发生需要两部分程序都完成才能同时调试的情况,显然这非常不利。一旦出错了,会很难确认究竟是哪个部分出错了。对于这点,如果是发生在局部,那么最好不要这么做,如果是整体架构,那还情有可原,关于这个问题更深入的讨论,可以看下面的篇幅。
l 被局部功能蒙蔽了透视全局的眼睛
这是件非常悲哀的事情,有些开发人员,在开发一个细节特性时候,开始进入一种兴奋状态,慢慢丢开主程序不管了,考虑的只是如何完成这个软件中他所期望的那个功能特性。甚至,这个功能不完成,就不继续做下去,或者因为这个功能没做到,而放弃了整个项目。
案例:曾经有个财务软件的项目,用 Delphi 编写,开发人员想完成那个中国特色的财务金额输入框,于是开始钻研各种相关技术,但由于项目紧,并没有给他太多学习时间,于是项目在这个部分完全卡住了,最后差点导致整个项目失败。在本文第一个例子所在的项目中,本人也曾经因为研究 RichEidt 如何实现一个超链接而荒废了大量精力和时间。尽管超链接在那个项目中存在特殊意义。
l 好意的附加功能带来痛苦的不良恶果。
当你期望一个协作开发人员开发一个小功能或者小函数,而他却完成得比你要求的还要多,甚至改变了你开始的意图,因为他认为那样更好。而事实上,你的要求是根据程序框架而来,并不需要更多,甚至是不能更多,这个时候也许你只能感谢他,并默默地把多余的代码删除掉。
我们太容易突发奇想地给系统加上一些莫名其妙的东西,而不是在尊重需求的前提下给系统增加功能。一个朋友在自己的资料收集软件中增加了闹钟的功能,在我向他戏称这是"不伦不类"的附加功能时,他摘去了这个模块。
其实大家可以设想,闹钟这样的功能加在资料收集软件中后,这个软件在收集完资料之后是关闭好,还是不关闭好呢?这个程序还能运行两份吗?等等诸如此类的问题就会让人困惑。附加的功能一般都有着自己的运行逻辑,而这个逻辑很有可能和主要功能冲突。
总得来说,局部技巧不是不需要,而是不因该被密切关注,编程的兴奋点也最好不要落在那些局部代码上,而是落在全局上。并且,经常兴奋于局部代码会使得自己慢慢对无休止的代码编写感到厌烦,我经常和同事们说,"充满灵性地设计,然后像傻瓜一样编程"。局部无技巧,总体优美和谐就是大智若愚的表现。理由说简单点就是:一个良好的架构可以产生巨大的效率,而平直简练的代码可以最大限度的加快编程速度,提高编程质量,并且产生容易调试和维护的代码,这使得编程者更可以把精力和智慧放到架构上。
把灵性的创造欲望,编程的兴奋点放到编程的开始阶段,放到设计、架构之中。这是本文的第一个慎重建议。对局部的兴奋和执着害处多多,应尽可能的改变这种习惯。
平直朴实地实现细节代码!
这就是编程的艺术。画家落到纸上的每一笔都不会有花花肠子,也许直到他落下最后一笔别人才能明白他所想表达的真意。画家不会盯着自己某一笔沾沾自喜。书法也是如此,某个笔画写得再好,再花哨,字形不好还是白搭。更别说书法是要通篇来看的。
对小技巧的批判到这里算是结束了,再次慎重的告诉读者,不要再为一个细节的精巧而费尽心机了,能平铺直叙的实现就赶快实现它,代码多点没有关系,节约的时间多考虑整体的架构。