表达:用好代码的肢体语言
Spacesoft【暗夜狂沙】
编程从本质上说是一种表达。实际上,编程的过程就是我们使用一种语言向机器表述一个工作以及其完成的步骤,让机器来进行的过程。也就是说:软件开发看起来似乎应该是一件人和机器沟通的工作,就像那位穿黑衣服戴黑墨镜的黑客尼奥干的一样酷。
但是,我们遇到的项目往往太庞大、太复杂,于是我们每天更多的是在试图和人交流——写文档阐述我们的需求,用注释描述我们的设计,用图表表示系统的结构。也就是说:我们每天要为我们的思想维护两套描述:代码和文档。而且我们还必须时时小心,保证它们一致。然而在比较大的项目里,文档的数目有时候会多得简直可怕,而假如这个项目的文档管理又有点混乱的时候,文档就变成了灾难。
当用大量文档作为条条框框保障起来的规矩把众人弄得苦不堪言时,革命发生了。有人喊出了口号:“源代码就是设计”。是啊,当文档和注释作为必要的辅助出现在开发中时,它们有效的提高了设计的可读性;而当它们开始喧宾夺主,当工程师们每天有效的工作时间更多的被消耗在不直接产生效益的附件上时,我们就有必要开始反思了。我们之所以不用我们日常的语言来编程,是因为它们有二意性,它们不精确,不易为编译器所理解。那么我们的设计为什么在用代码描述之后还要用模糊的自然语言再来做进一步的解释呢?也许我们的表达方式有问题?
这篇文章试图总结一些用代码来表述设计的技巧,当然我还不打算也没办法完全回答刚才提出的问题。这些技巧仅仅包括编码的方式,所以我称之为代码的“肢体语言”,也就是仅仅采用代码本身,来表述你的设计思路和限制。
一.起个好名字
一个好名字能够清晰的描述对象的含义,也可以清晰的描述操作的目的和方式。ctype.h 里面的这个函数名字就起得很好:
int isalpha( int c );
很明确的告知了函数的意图。把你代码里面的
(((ch >= 'A') && (ch <= 'Z')) || ((ch >= 'a') && (ch <= 'z')))这样的代码扔掉把,一个isalpha多么清晰的描述了你要做的动作啊!
各个开发团队的代码规范往往对命名有比较严格的规定。变量的命名往往使用匈牙利规则或者与它类似。匈牙利规则很好的利用前缀,使一个简单的变量名携带了更多的信息。比如 lpszFormat 这样一个变量,熟悉对应编码规范的人会很容易看出来这其实是一个字符数组指针。于是他就不用跑到变量的声明处去看它的类型,也不用从上下文推敲,这个变量到底是CString 还是一个char *。遵守代码规范也带来同样的好处:确实省掉了到处找变量声明的麻烦。当男生常常起名二狗,女生常常叫做翠花的时候,我们往往可以省去想翠花是男是女的麻烦。
当然了,起个好名字,最关键还是要选个好词。好词汇是表达中非常关键的东西,这对写代码还是写文章都有效。由于我们开发的主流还是用的英文编程,建议变量起名还是不要用汉语拼音了。int ShiBuShiZiFu( int c ); 这样的命名实在是别扭,还是能免则免了,我觉得比英文难懂多了:)
二、利用语言要素描述限制条件
编程语言为了增强表达能力,往往提供了很多的限定,但是我们常常没有很好的利用。举个Delphi 的例子:
function MyExample (const nParamIn: Integer; var nParamOut:Integer): Integer;
这样的参数列表就很清晰的告诉了调用者:nParamIn 是输入用的,nParamOut里面则会返回一个数据。
又比如,C++ 中都有这样的规定:一个类中有纯虚函数的话,这个类不能实例化,于是,你可以用这个特点来向基类的使用者表达:“你一定要Overload 这个函数”这样的要求。
断言也是说明限制的一个好手段。比如我们要实现这样一个函数:
char *strcpy( char *strDestination, const char *strSource);
我们将把strSource缓冲区中的字符串拷贝到strDestination里面,怎样表达“strDestination不能为空指针” 这样一条限制呢?
首先想到的办法是在函数的开始加一句 if (!strDestination) return NULL; 然而这并不是一个良好的表达。它给调用者传递了一个含混的信息:我碰到问题了。至于具体是什么问题呢?就不告诉你。或者可以进一步:用一个全局变量式的方法储存错误信息(就像GetLastError()做的那样),或者用异常。然而,这也不是一个良好的表达:GetLastError 和异常更加合适的读者是软件的最终用户,而不是程序员。对程序员更友好的方式是在编译或者调试的时候就能报告出错误的所在。于是我们用断言。一句assert(strDestination); 明确的告知了使用这个函数的程序员你需要的限制。
三、使用众人都了解的表达方式
当一种表达方式为众人所接受和熟悉的时候,使用它往往可以省很多口舌,比如自然语言中的成语和典故,就往往被用来表述一些原本要长篇大论才能说明白的意思。
模式(Patterns)就是这样的一种表达方式。在我看来,一个模式就是总结出一类问题,以及这些问题的经典解决之道,最后给这样的一套东西起个名称。于是当这样的思维方式为众人所认同的时候,你可以使用模式名称来指代一些说明起来很麻烦的问题和手段。
举个例子。我有一个对象CService,它为另外一个对象CClient 提供一些服务,两个对象都在本地作通信。现在需求变了,需要把CMyClient 移到其他机器上,但是我又不想改写现有的两个模块。于是乎,我就写了一个类来管理CClient 对CService的调用。这样的设计思想怎么描述?洋洋洒洒写上那么几百字的注释?不用!我把这个类叫做CServiceRemoteProxy 好了。熟悉remote proxy 模式的人一下子就会理解我要作什么了。当然啦,对模式不熟悉的程序员就麻烦了^_^
所以我觉得,模式对程序员的重要性在于,模式使得程序员的交流有了更多共同语言。于是了解模式就和当初我们在小学里学成语一样重要了。
另外一个例子是对公有库的态度。很多人对公有的函数库、类库,甚至编译器都有不信任感,或者本来就文人相轻,觉得用别人的东西不是显得自己没水平?于是就自己实现一个自己版本的东西。倘若是为了优化或者原来的公有东西有bug, 那也就罢了,据说有牛人自己从Framework到编译器自己完全作了一套,有任务的时候完全在自己的环境里完成,那未免就有走火入魔之嫌了。这样的代码完全没有办法作交流,也就没有什么表达的需要,当然不在本文的讨论范畴。我觉得若有可能,还是尽量使用公有的东西,大家都熟悉,也就比较容易理解。怕就怕在自己另外作了一套东西,接口相似,偏偏又有不少东西和而不同,特性大相径庭。这样旁人阅读这样的代码,就难免觉得郁闷,难免要你写文档了。
比如,我不喜欢string.h 提供的strcpy,于是我就实现了一个自己的版本:
int strcpy( char *strDestination, const char *strDestination, const char *strSource );
还要给个规定strcpy 成功返回1,失败返回-1。完了,这回没搞头了,想不写注释,这样的代码谁看了不糊涂啊?于是只好老老实实写注释。你说这又是何苦捏^0^?
最后,还是要说明一下,我在这里强调代码的表达,不是说从此就不再需要注释和文档了,有的时候,一小段注释,说明的效果比什么都好,那么我们又何必吝啬那一点注释呢?
其实,编程就是表达,精妙的表达可以省去很多口舌,也可以部分的把我们从文档的海洋里解救出来。所以用好代码的肢体语言吧:)
欢迎光临作者的个人主页:http://www.mrspace.net/