1、编程的技巧与艺术编程的技巧与艺术解题在编程技巧流行的今天,程序员们仍然需要不断编程,不断写代码,网络上的技巧集不是成熟代码,也不是编程的技巧,而是实现某个功能的知识。至于艺术,对于很多程序员来说简直是天方夜谭,但又分明地感觉自己和艺术并不遥远,这种感受好像欲回家的游子还没有找到归路。或许编程的技巧与编程的艺术本来就是一回事,只是一个说的得现实点,一个说得幻想点。可仔细想来,又觉得技巧就是技巧,艺术还是艺术。那死板的技巧死记硬背都可以,怎么能用艺术两个字冠之。但是,当打开那些优秀软件的代码,一种浑然天成,美轮美奂的感觉依然无可阻挡地袭来,无论是细看,还是粗看,界面还是架构,算法还是逻辑,竟然没有
2、一样不让你觉得美得窒息。技巧就是艺术。这就是本文欲言说的主题。专家解说其实,Delphi本身就是最高的艺术,最好的技巧。您敢说不是吗?无论从设计、架构、代码、思想、效率、实用性,实在无话可说,至少笔者无法用人类鄙陋的语言来形容对Delphi的感受。Delphi太伟大,鄙陋的我们只好退而求其次,先从一些简单的、浅近的角度来窥探一下编程的艺术。笔者从Delphi 1 开始使用Delphi ,虽说和真正的专家比起来还差的太远,但多少积累了一些经验,期望能和读者分享。这样的代码是技巧吗?(此代码节选自一个失败的项目,已作适当的简化)procedure TBaseContentAgent.SetCont
3、ent(const Content: array of const);var s:string;begin s:=; if not Assigned(ContentObject) then exit; /ContentObject是此类欲控制的对象 if ContentObject is TCustomEdit then begin with content0 do case VType of vtString : s := VString; vtPChar : s := VPChar; vtAnsiString: s := string(VAnsiString); vtVariant : s
4、 := string(VVariant); vtWideString: s := WideCharToString(VWideString); vtInteger : s := IntToStr(VInteger); vtObject : if content0.VObject is TStrings then s:=(content0.VObject as TStrings).Text; end; if ContentObject is TCustomMemo then (ContentObject as TCustomMemo).Lines.Text:=s else TCustomEdit
5、(ContentObject).Text:=s; end;end; 从局部看,这段程序不错,可以给任意继承TCustomEdit的控件赋值,并且可以传递任意的数据类型进去。这样TEdit,TMemo,TRichEdit等控件的赋值工作都可以在这个函数中完成。在那个项目中,这段函数是个基础类的虚函数。其意图是设置一个内容对象的内容,在这段代码里实现了对TCustomEdit类型的内容对象设置内容。之所以要使用const Content: array of const 是为了准备用数组的方式传递更复杂的对内容设置的调用。当时甚至考虑了网络指令的调用。并且作成虚函数,准备下一步在继承里面重载它。并完
6、成对各种不同类型对象的内容设置。听上去很美,看上去也没错。但这种思路恰恰就是项目失败的因素之一。在上述显然充满技巧的代码中,虽然也能找到美妙感受,但最终的结局却是失败。于是不得不重新思考什么是技巧,什么是艺术?究竟这样的程序错在哪里?不妨告诉读者,就是这样一个问题,整整考虑了4年,随着经验和年龄的增长,逐渐开始明白真正的编程技巧是什么。真正的编程技巧就是在细节实现上没有技巧,而却可以搭建出一个宏伟壮观的大建筑。这就好比李昌镐的棋风,看似平淡无奇,实则天罗地网。下面就逐步将笔者这些年以来的感悟分享给大家。在稍后的篇幅中将回过头来解说这个代码的问题。批判小技巧好多次,同事得意地让我看他们的代码段,
7、常常,我会赞赏一番,毕竟思想的火花来的不容易,但是,与此同时,我也会深刻思考一下这些技巧所带来的后果,随后,我就会很不客气地指出这种技巧可能会让他们如何的难堪。看看下面这个函数。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.Re
8、cordCount0 then result := ADOQuery.Fields0.AsVariant else result := 0; / 防止结果为空时发生错误 ADOQuery.Free;end;似乎不错,执行一条Sql语句后又能返回其第一值,作者的理由是,如果执行的sql是 select count(*) from . 之类的语句,那么这个函数就可以很容易的返回想要的数据。但是作者并没有考虑到这个函数包含了一些隐含的约定,而这些约定就好像钩子,调用者必须清楚这个函数真正干了些什么才有可能正确调用,否则,就会出错。比如,调用者首先必须知道返回的是第一个字段,其次他必须知道返回值0可能
9、并不意味着是正确答案。并且这个函数没有提供任何判断返回值“0”是否是数据库内数据的信息。这点很要命,其结果也不出所料,正因为这个函数,该程序出了不少错误。与其说这是技巧,不如说这是花样!花样这个字眼有点贬义,本文绝无此意,本文指看似精妙,实则错误的代码。说到这里,本文可以给一些术语稍加规范了。提示: 大多数所谓的技巧集中的技巧其实是提示,它们指示了某个功能可能的实现方法。并且一般来说这些提示需要再加工才能真正使用。方法:大部分技巧集是实现某个功能的方法或者知识点,只有少部分是真正的技巧。技巧:如何编程,如何实现代码的过程才称为技巧。它更类似手段、构思。技巧离不开提示和知识点,否则就是巧媳妇难为
10、无米之炊。技巧关键在于巧上,把技术用得很巧妙,才能算是技巧。花样:很多时候,人会浮想联翩,当固执自己某种想法时,就可能写出看似巧妙,实则是花样的代码。并且,还可能为自己代码沾沾自喜,以为精妙。比如这段程序: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 0 then begin inc
11、(result); inc(i, n+len1-1); end else break; end;end;此程序曾出现在大富翁论坛上,作者本意是列举了4、5种计算子串的方法,这个函数是对其中一个低效率算法进行改造,因而使用了pos(SubStr, string(Sourcei);这样的代码。但是作者这个时候也许只关心如何构造精巧的算法从而取得效率了,这行代码会发生致命的错误,它会修改Source字符串。对于这个错误详细分析请见本书另一篇文章综述字符串的操作和使用。这样的错误还算比较容易查出,有经验的读者就可能一眼看出问题所在。但是如果类似本文所举的第一个例子,就很难看出问题所在了。下面就说说这些
12、花样的问题所在。 花样太注重局部的精巧但是精巧的局部并不意味着就一定能够带来整体的效率,相反,很有可能成为整体的拖累,经常发生的事情是,为了一个局部的精巧的代码,整体大局上却为它牺牲了很多。“我这段代码很好,非常精巧,我要保留他,我们修改一下其他的逻辑保证它运行正常吧”。这种想法正是这种思维的写照。这样的思想会使得自己拒绝合理的建议,并且明知道问题也不愿意丢弃。案例:如RetrieveSqlResult这个函数,作者最后说:“我现在就只用于Select Count(*) from . 这种情况。” 说实在的,这真不是个好主意。对于一个已经收尾的小项目,再去大面积修改确实必要性不大,但这样的问题
13、从一开始就应该避免。 太爱护代码是愚蠢的举动特别是爱护局部代码,甚至一个小功能,小逻辑,小技巧,很多程序员创造出它们之后,就非常爱护他们,不舍得删去,仍掉,甚至宁可其他地方麻烦点,也想要保留自己这个智慧的结晶。案例:由于需求的关系,需要设计一个类似级联菜单的下拉框控件来替代一组下拉框,于是另一个开发人员去设计这个功能,在这个功能设计完成之前,先前的开发人员开发了一个转换下拉框为菜单的设计。但事实上这是换汤不换药的设计,并没有完成级联的要求。但这个开发人员非常喜欢自己设计的这个功能,竟然弃好的设计不用,仍然使用自己换汤不换药的设计。 懒惰想少写点代码,结果却花费了大量无益的思考时间常常会有这么一
14、种情况,碰到一个问题,马上想到了一个解决方案,那个方案是最直接的,但需要写很多代码,不原意,于是拼命思考如何构造一个精巧的方法使得能花费很少的代码解决这个问题。在大富翁论坛上,经常可以看见提问者嫌回答者的答案代码太长,而反复问还有没有更简单的方法。并且一问就是很长时间,我很怀疑这些问题是否是为了项目,一个项目怎么经得起在这么一个小问题上如此耗费时间呢?我们这个时代不再是AppleII的时代,一个字节都可能要珍惜,CPU的速度也已经足够惊人,无需我们过分为速度担心,编程本身的效率才是我们该去关心的事情。 为局部精巧而营造了复杂环境局部精巧的构造也许需要依赖更多的环境条件,比如更多的参数,或者意图
15、返回更多的资料和数据。这样的代码一般都比较脆弱。一般开发者不一定能处理好每种可能发生的情况。案例:本文所给出的第一例子就属于这种情况。局部确实精巧了,但却给调用者带来了非常重的负担。因为那个设计的思路是上层调用者可以传入非常复杂的指令流,但调用者又不清楚这样做是否正确或受到支持。这样上层调用者必须非常了解这个层面上的实现,否则将无法进行工作。而下层又企图不断扩展自己的能力,这样就陷入了上层调用代码不知道该如何开发的境地。此项目的失败也就逐渐成为一种必然了。 为局部精巧而设计了复杂或不明确的调用法则这种情况象一个钩子,普通的调用好比电源插座,不管三七二十一插进去就可以用,但钩子不是这样,如果不清
16、楚这些局部是如何实现或者怎么样的调用约定,那么就可能错误的使用他们。前面RetrieveSqlResult这个例子就是这样。尽管设计者会很巧妙的使用它得到一个正确的结果,但当一旦频繁使用,就会逐渐忘记当初的潜在调用约定,这样就可能发生严重错误。显然,为了局部精巧而损失调用的明确性和函数的功能单一性是不足取的。 局部过于精巧的代码损失了代码易维护性和易调试性这样的代码难以看懂,甚至即使写满了注释,过一段时间再阅读也非常困难。由于其逻辑的过于精巧,需要仔细琢磨才能明白代码的意图,这样的代码是本文所不推荐的。除非出于核心代码效率的需要,否则只会给编写、调试、维护带来莫大的痛苦。因为这样的代码编写困难
17、、调试困难、维护困难。本人不太赞成使用C+的某些语法,特别是在一个for中放入一堆代码,还加上很多-和+,这样的代码也许非常精巧,但确实极端难以阅读。long a=10000,b,c=2800,d,e,f2801,g; main()for(;b-c;)fb+=a/5; for(;d=0,g=c*2;c-=14,printf(%.4d,e+d/a),e=d%a) for(b=c;d+=fb*a,fb=d%-g,d/=g-,-b;d*=b);这是一段号称“外星人程序”的代码,据说是可以计算小数点后800位的程序,本人猜测其算法不会太复杂,但它的写法确实够呛。这段程序是为了特殊目的刻意得这样设计,这
18、问题还不大。如果在一个项目中以这样的想法写代码的话,其结果就是灾难。不要以为Delphi写不出晦涩难懂的代码。 过于精巧的结构,需要架设空中楼阁式的逻辑。这种情况未必不好,而且还可能比较常见。比如要做个检索引擎,那么显然需要制作索引和检索两部分程序,但当程序的结构设计得过于精巧时,就会发生需要两部分程序都完成才能同时调试的情况,显然这非常不利。一旦出错了,会很难确认究竟是哪个部分出错了。对于这点,如果是发生在局部,那么最好不要这么做,如果是整体架构,那还情有可原,关于这个问题更深入的讨论,可以看下面的篇幅。 被局部功能蒙蔽了透视全局的眼睛这是件非常悲哀的事情,有些开发人员,在开发一个细节特性时
19、候,开始进入一种兴奋状态,慢慢丢开主程序不管了,考虑的只是如何完成这个软件中他所期望的那个功能特性。甚至,这个功能不完成,就不继续做下去,或者因为这个功能没做到,而放弃了整个项目。案例:曾经有个财务软件的项目,用Delphi编写,开发人员想完成那个中国特色的财务金额输入框,于是开始钻研各种相关技术,但由于项目紧,并没有给他太多学习时间,于是项目在这个部分完全卡住了,最后差点导致整个项目失败。在本文第一个例子所在的项目中,本人也曾经因为研究RichEidt如何实现一个超链接而荒废了大量精力和时间。尽管超链接在那个项目中存在特殊意义。 好意的附加功能带来痛苦的不良恶果。当你期望一个协作开发人员开发
20、一个小功能或者小函数,而他却完成得比你要求的还要多,甚至改变了你开始的意图,因为他认为那样更好。而事实上,你的要求是根据程序框架而来,并不需要更多,甚至是不能更多,这个时候也许你只能感谢他,并默默地把多余的代码删除掉。我们太容易突发奇想地给系统加上一些莫名其妙的东西,而不是在尊重需求的前提下给系统增加功能。一个朋友在自己的资料收集软件中增加了闹钟的功能,在我向他戏称这是“不伦不类”的附加功能时,他摘去了这个模块。其实大家可以设想,闹钟这样的功能加在资料收集软件中后,这个软件在收集完资料之后是关闭好,还是不关闭好呢?这个程序还能运行两份吗?等等诸如此类的问题就会让人困惑。附加的功能一般都有着自己
21、的运行逻辑,而这个逻辑很有可能和主要功能冲突。总得来说,局部技巧不是不需要,而是不因该被密切关注,编程的兴奋点也最好不要落在那些局部代码上,而是落在全局上。并且,经常兴奋于局部代码会使得自己慢慢对无休止的代码编写感到厌烦,我经常和同事们说,“充满灵性地设计,然后像傻瓜一样编程”。局部无技巧,总体优美和谐就是大智若愚的表现。理由说简单点就是:一个良好的架构可以产生巨大的效率,而平直简练的代码可以最大限度的加快编程速度,提高编程质量,并且产生容易调试和维护的代码,这使得编程者更可以把精力和智慧放到架构上。把灵性的创造欲望,编程的兴奋点放到编程的开始阶段,放到设计、架构之中。这是本文的第一个慎重建议
22、。对局部的兴奋和执着害处多多,应尽可能的改变这种习惯。平直朴实地实现细节代码!这就是编程的艺术。画家落到纸上的每一笔都不会有花花肠子,也许直到他落下最后一笔别人才能明白他所想表达的真意。画家不会盯着自己某一笔沾沾自喜。书法也是如此,某个笔画写得再好,再花哨,字形不好还是白搭。更别说书法是要通篇来看的。对小技巧的批判到这里算是结束了,再次慎重的告诉读者,不要再为一个细节的精巧而费尽心机了,能平铺直叙的实现就赶快实现它,代码多点没有关系,节约的时间多考虑整体的架构。必须体现技巧的数据和架构画家体现技巧的地方之一就是颜色,有些画家对颜色的使用甚至到了登峰造极的程度,他们用颜色表达思想,表达情感。画家
23、体现技巧的另外一个地方是布局和线条,有些漫画甚至只剩下线条,但仍旧活灵活现,让人无法忘怀。而有些画家则依赖一种布局给人强烈的视觉冲击。程序也有同样的要素必须体现出技巧,这就是数据结构和程序架构。一个合适而精巧的数据结构,一个合适而优美的程序架构,当有了这两者,一个项目就完成了大半。这好比建造一幢大楼,随心所欲的想到哪里就搭到哪里肯定会出问题。而正确的做法显然是先打好地基,树好桩子,搭好横梁,这样施工下去即便出了问题也只能是小问题。除非建筑设计师设计错了。编程也是如此。编程的第一步是对项目需求的想象和描述。第二步是虚拟出一些角色想象实际最后会如何使用,并以此深入分析细节的需求。然后就要进入数据和
24、结构的设计。这里要说明一下,本文不打算再次重复软件工程里反复强调的知识,本文将从经验出发给读者一些建议。 把每个变量都看成是局部变量这也许是个夸张的想法,而事实是,其实根本不应该存在全局变量,你所看到的Delphi里大量的全局变量都其实是在某个层面上的局部变量。比如,Application,你可以认为是全局变量,但你要知道,这个变量其实是对一个程序的标识,从两个程序有两个不同的Application这个角度来看,Application还是一个局部变量。这就好比在一个函数里面声明的变量对于这个函数显然是“全局”可见的。又如HInstance这个全局变量,他指示了当前所在的模块。同一个运行的程序里
25、HInstance就未必一样了,如果程序带有DLL的话,DLL显然应该使用主程序的Application,但却一定拥有不同的HInstance。这句话换成比较容易接受的说法就是:每个变量都应该被封装,并且,封装的层次尽可能得低。封装的层次不同,可见性自然不同。最低的封装层次自然就是函数内的局部变量,最高的封装也许应该是网址,所有的电脑都能访问。这点上不要和常数混淆起来,常数是一本字典,封装不封装都没有太大关系这样做的好处是明确了数据的归属,谁能够操作。有些人为了简便,直接用全局变量作为交换数据的场所,这千万要不得,您可用通讯、函数等方式来交换数据,不要在多个模块之间直接共享自己的私有数据。此间
26、顺便提一下Delphi的一个小问题。你看,每当您创建一个新的Form ,就会产生一个全局变量,比如Form1,其类型是TForm1。因此很多初学者这样写代码:procedure TForm1.Button1Click(Sender: TObject);begin Form1.Caption:=Hello;end;这是多么可怕的思路。Delphi为什么要这么做呢?在TForm1这个类的实现里面怎么可能需要出现类型是TForm1的变量呢?类实现中一定不需要出现引用自己这个类的变量,用Self关键字就一定可以了。面向对象的思维给这样的代码破坏殆尽。由此我们倒可以引出另外一个准则:尽量使用被本模块封装
27、了的数据。或者说:实现功能所用到的变量和这些功能模块的封装具有一一对应的关系。或者说:尽量不引用不需要的上层模块的数据。让上层模块把所需处理的数据传递给下层去处理,并且能传递最根本的数据就不要传递更多的数据。这就是说,你实现的功能里如果没有需要牵涉高一层模块的要求,那么引用外部变量就是不合理的。不是这层模块设计不合理,就是高一层的模块设计不合理。看看下面的小例子。这个例子本意用于统计一个Memo内相同行出现的次数。procedure TForm1.CountList(List,CountList:TStrings);var i:integer; procedure CountToList(co
28、nst Name:string; Dest:TStrings); var ID:Integer; begin ID:=Dest.IndexOfName(Name); if ID=0 then Dest.ValuesName:=IntToStr(StrToInt(Dest.ValuesName)+1) else Dest.Add(Name+=1); / Name为空时,必须如此加入。 end;begin for i:= 0 to List.Count - 1 do CountToList(List.Stringsi,CountList);end;这里不讨论该实现是否需要子函数和这个功能是否该用其
29、他方法实现,目的是为了让读者能看见一个清晰的模块层次架构,从而明白本文的意图。为了说明问题,这里再给出一个难看的实现方式。procedure TForm1.CountList2;var i:integer; Name:string; ID:Integer; procedure CountToList; begin ID:=Memo2.Lines.IndexOfName(Name); if ID=0 then Memo2.Lines.ValuesName:= IntToStr(StrToInt(Memo2.Lines.ValuesName)+1) else Memo2.Lines.Add(Nam
30、e+=1); end;begin for i:= 0 to Memo1.Lines.Count - 11 do begin Name:=Memo1.Lines.Stringsi; CountToList; end;end;两段程序一定结果相同,不会出什么错误(为了简化代码对于Name里面存在“=”这种情况没有处理),分别是如下方式调用:/方法1 CountList(Memo1.Lines,Memo2.Lines);/方法2 CountList2;方法2把自己需要的变量声明到了上层函数中,尽管这样不会出错,但显然不是个好方法,看似两个函数都为了实现一个功能,因此会给你一种同属于一个层次的错觉,因
31、此尽管分了模块层次却没有封装模块的数据。在这个例子中,这个问题是明显的,但真正编程的时候,却非常容易犯这个毛病。因此看一个程序变量设置合理不合理,就可以知道编写人的思路清晰不清晰。其次方法2都直接去操作目标的数据,而不是像方法1一样只是操作它该操作的数据。很显然,方法1虽然调用复杂了,但代码很清晰,每个模块都仅仅用自己需要的数据在工作,不多也不少,刚刚好。这样的代码兼容性也特别强,每个模块也都可被容易地分拆用于其他的目的,代码的重用性非常高。 虽然先前的本意只是操作Memo1,Memo2,但从实现后的代码来看却是通用的,因此它可以适应项目的变化或者其他地方的需求。而所需要作的工作仅仅是合理使用变量。仅仅如此。从调用来看,其实也非常合理,可以理解成“帮Memo1的Lines属性统计一下,并整理到Memo2的Lines属性里面,并且,给你Lines这个属性的值就够你用了,无需把整个Memo类型传给你。” 这不正体现了下层模块不应该引用自己不需要的上层模块数据这个法则吗?能传递Lines进去,就不必
copyright@ 2008-2022 冰豆网网站版权所有
经营许可证编号:鄂ICP备2022015515号-1