追求神乎其技的程式设计之道(十一)- 抽象化与命名

休眠已久的神乎其技系列又復活了!这篇文章其实写很久了,只是一直断断续续到今天才完成它,久到让很多人觉得这系列已经完结了…。但我想只要我还有在写程式,这系列就永远不会结束吧。

简洁、弹性、效率
我一直觉得写程式是一种艺术活动。程式语言是一种要求极度精确的表达方法,只要少打一个字母就可能造成完全不同的结果,但同时却又不限制你要如何达到目标。

程式设计师有极大的自由来让一个程式按照自己的想法「活起来」,不同人针对同样的目标所写的程式也一定不同。有人会用极简主义来把变数命名为a、b、c,也有人会把用匈牙利命名法让变数前后长出鬍子和尾巴;有人坚守DRY原则(Don’t repeat yourself),只要类似的程式出现两次,就把他们抽象化成一个函数,也有人用copy/paste写程式,不管怎么page up或page down都一直看到一样的东西还能泰然自若;有人写程式把所有东西都塞在main里面,也有人写个Hello world就要搞一个class HelloWorld(虽然有些时候是被啰嗦的J语言强迫的…);有人没听过Big O也写程式写得很开心,但也有人嫌stdlib的qsort太慢硬是要自己重写一个…。

尽管每个人的信仰和原则不同,但大体上程式艺术家也不过是在「简洁」、「弹性」、「效率」这三大目标上进行一连串的取捨(trade-off)和最佳化。

「简洁」的程式也「易读」,没有多余的叙述或重复的程式码,每个概念都只有唯一的一段码在描述它。如果多了,就容易产生不一致的行为,如果少了,就是没做到该做的事。有「弹性」的程式容易修改和扩充,只要在一个对的地方弹弹手指,不用因为老闆朝三暮四或是需求改变就得把整个程式重新翻修一次。有「效率」的程式会用最适合的资料结构存放每一样资料,用最快的演算法做每一项必要的计算,并去除任何不必要的间接行为 (indirection)。

虽然目标很明确,但程式设计之所以像艺术就是因为大部分时候我们都没办法兼顾这三项目标:为了效率,可能就得牺牲弹性和简洁;反过来说,为了弹性或简洁,也常得牺牲效率作为代价。幸运的是,效率的追求在电脑硬体和编译器技术的进步下已经不像20年前那么重要,只要选对资料结构和演算法,几乎已经没有必要手动做低阶的最佳化。除去效率之外,弹性和简洁其实是比较容易同时达到而又不互相冲突的目标。要达到这目标,其中关键的能力就是今天的主题:「抽象化」(abstraction)。

最简单但也是最难的事情
很多人没听过抽象化这个词,甚至以为自己不会这件事,但其实从我们宣告第一个变数起,抽象化就已经开始了。

「这个变数要叫什么名字?」

帮变数命名时,其实就是在赋予那个变数一个「意义」。人的记忆力有限,很难记住大量且没有意义的资讯。但如果资讯有了一个固定且有逻辑的名字,我们也就有一个容易记忆的符号来代替整个复杂的概念。换句话说,我们可以把非常复杂的概念浓缩为一个容易处理和记忆的小单位,这个过程就叫做「抽象化」。

抽象化可以让程式变得简洁。好的程式设计师会习惯从重复的程式码中找出共同或相似的部份,并且把这个部分提取出来变成一个更通用的概念。任何复杂的概念都可以被抽取出来替换成一个变数、一个函数、一个类别、一个模式、一个模组、甚至是一个系统,并加上适当的命名,就能让这个程式「一看就懂」,任何註解都不需要写。抽象化也能让程式有弹性。经过适当抽象化的程式,每个概念都有一个独立的「单位」(可能是变数、函数、类别、模组、或系统)可以表示,每个概念中包含的细节也被隐藏在适当的范围内,不管要修改或扩充原本的程式都能让需要碰触的地方减到最少。

虽然抽象化是让程式简洁又有弹性的关键,但出乎意料的这是一个容易理解却很难精通的能力。抽象化做得太少,程式会变得凌乱不堪,不同层级的概念和资讯互相交杂在一起,不仅让程式变得难读也难改。抽象化做得太多,就是所谓的over design,明明需求只有印一个Hello World,却用了10种design patterns盖起101大楼以应付根本就不会出现的「未来需求」。

抽象化这个主题可以讲三天三夜讲不完,但今天我只想提其中最简单也最难的事:「命名」。

命名可以说是写程式时最简单但也是最难的事了。这件事没什么人会教,没多少书会写,因为这件事看起来非常容易,即使你把程式里的变数照字母顺序a, b, c, d, e, …命名也是行得通,反正对编译器来说变数或函数的名字不过就是一个没有意义的符号,不管你取什么名字最终都只是对应到一个像是0×08048374这个样子的记忆体位置而已。

简单来说,一个变数是叫「小狗」或是「小猫」,对电脑来说都没有区别,但对人来说,差别可大了。

很多初学者以为程式是写给电脑看的,只要看起来好像能跑出正确结果就好,所以变数位置随便乱放、名字也随便乱取、每个变数都是public、甚至一个函数有几百行,为了在一个画面中塞下更多程式码还把IDE的字型缩小到要瞇着眼才看得见。也有很多人觉得高手写的程式看不懂是正常的,等到自己等级提昇后应该就会看得懂了,但其实事实完全不是这样。我认识的每个高手和大师写的程式码都是干净、简单、易懂,即使是极端复杂的演算法,都能直接从程式码中看懂作者的想法。

Martin Fowler的 “Refactoring – Improving The Design of Existing Code” 一书中有一句话我很喜欢。

Any fool can write code that a computer can understand. Good programmers write code that humans can understand. (任何一个傻瓜都能写出计算机可以理解的程式码。唯有写出人类容易理解的程式码, 才是优秀的程式员。)

一段好的程式码是不需要任何额外註解或说明的。如果名字都取得好,每个变数就能适当的解释了自己的角色,每个函式都说明了自己的功能,整个程式读起来就会像在读说明文件一样自然。在这种境界下,只要有了基本背景知识的程式员应该都要能轻易地看懂。英文中有个词叫做explain itself很适合用在这,也就是自己应该要能完美的解释自己的一切,不需要其他的人或文件来帮忙。

但是,命名是很难的一件事,可以说是写程式中最接近「艺术」的一部分了。我说的命名,不是要用大小写混杂的「CamelCase」或是底线分隔的「underscore_separated_style」这种风格问题,而是一个方形到底要叫rectangle或是x的差别。名字取得好,不但自己或其他人未来再回来看这份程式码时容易进入状况,对于正在开发中的程式也可以减少很多不必要的bug。

我之前当一门课的助教时,有个作业是要学生实作一个西洋棋游戏,画面上要有个棋盘,还有该有的棋子。既然是个棋盘,底层很自然的就会用个二维阵列来表示棋盘的状态,例如说我们会有

Chess board[N][N]这样子的一个阵列。接下来,真正的问题来了,程式中势必会有一些两层的for迴圈去对这个阵列做操作,如果是你会把这两个迴圈的index变数取做什么名字?

最常见也最不用脑的index命名就是i和j,在一般没有特殊意义的迴圈中用i是没什么太大问题的,因为大家都知道这只是一个单纯的index。但如果用到j,通常就代表程式可能有些臭味了,至于会用到k、l、m… 那这个程式一定是彻底腐败了。

我看了很多学生的程式,我发现很多有bug的程式都是用i、j,或是x、y来命名,而那些写得很漂亮的程式,几乎都是用row和column来命名(或是他们的缩写r和c,或是row和col)。

用i、j的问题在哪?

问题在这两个名字没有和棋盘的位置有直接关连,看程式的人没办法一眼看出你的i到底是指row还是指column,或是指到宇宙里的一颗星星。即使是正在写程式的作者本人,也得一直在心中做i是row、j是column的转换,但只要精神稍不集中,或是吃个饭休息回来,很轻易就会忘记这些隐晦(implicit)的对应关系。而这种隐晦的对应,就是伤害程式码可读性和造成bug的通缉要犯之一。有的人为了避免自己忘记这些细节,就会把这种隐晦的关系或假设写在程式的註解里。但话说回来,既然要写,直接写在程式码里不是更好吗?

除了用i、j的这群人外,还有另外一群用x、y的程式也是让人非常头痛,如果要我比较的话,我会说用xy比用ij还糟糕。为什么?因为这个程式最终要把棋盘画在萤幕上,而所有2D绘图的函式库都是用x、y来表示萤幕上的位置,如果棋盘用xy,萤幕绘图也用xy,这样如何分辨这个xy是棋盘的位置还是萤幕的位置?用xy这群人的解决方法都大同小异,比较懒惰的就是用x1、x2,甚至是x和xx;好一点的会用boardx和screenx,但以index变数来说还是太长太啰嗦了。

与其费这么大力气区分两种xy,如果一开始就用完全不同的名字来存取棋盘和萤幕,不就没事了?以二维阵列来说,用row和column符合natural mapping,不用再心中自己多做一次转换。此外,现代程式语言的多维阵列大多是row-major排列,也就是说A[r]就能取到第r个row,A[r][c]就能取到第r个row的第c个元素;但如果用xy来存取二维阵列,就要把xy反过来,写成A[y][x]才能取到第y个row的第x个column。
(在这个程式中很多用xy的人都把row和column顺序搞反,导致初始化的盘面整个转了90度。)

我以前参加程式比赛时,看过很多经过长期训练的选手因为比赛的时间压力而养成不好的习惯,像是把所有程式码写在main里面,变数不是aa就是bb这种没意义的名字。在程式比赛这种特殊的环境里,每个程式的目的就是解一个有明确输出入规定的问题,加上有时间限制,所以选手们都是尽量用最短的code来实作自己的想法。这种情况下写的程式可以说是用完就丢,只要比赛一结束这个程式的生命也就到了尽头,所以很多人就不会去思考命名的问题。

到大学的时候,我也常帮同学在作业deadline前夕看他们的程式帮忙debug。很奇妙的是,大学课程的期末专题或是作业应该都有充裕的时间可以慢慢「设计」一个程式,但很多人都是在最后一两天才开始动手,于是在作业死线的压力下也没心情去好好设计一个程式的架构,更别提要好好想每一个变数的命名和位置,也就浪费了许多可以好好练习这个命名艺术的机会。

命名和抽象化是一体两面的事情。当你能把一个概念用一个适当的名称来称唿它时,你才有办法把这个概念当成一个基石往上建构更复杂的事物。在此同时,人们也才能用这些简单的名称来讨论复杂的概念或想法。如果你在写程式时常常没办法用很简单的话跟别人解释你的程式,通常也代表你的程式是一团浆煳,没有条理和层次。在这种情况下,你怎么知道浆煳里是不是黏了一堆臭虫呢?反过来说,当你能用简单清晰的白话跟人解释你的程式时,你也一定能把程式写得一样干净漂亮有条理。

如果你现在还在用a, b, c这种变数写程式,不妨先暂停一下,好好想想每个变数的意义是什么,你的程式就会自然的变得越来越简洁和漂亮。

(待续)

2/1 更新:
有朋友提到一篇有趣的相关文章:软体业的重要职缺 命理大师!。这文章说软体公司应该有个专门掌管命名的人,才能保持整个project的一致性,并顺便算个命看看这些名字吉不吉利。

这让我想到,其实现有open source程式这么多,我们可以很容易的写一个「命理大师」程式出来。只要到几个project host site,像github、google code之类的地方,把所有project里的程式码token抓出来做一些简单的分析和统计,就可以得到一些有趣的资讯和命名时的参考。例如说,我们可以知道有多少程式里面用Box表示方形,多少程式用Rectangle,多少程式用deleteXXX,多少用removeXXX,他们之间的区别又在哪。甚至在设计library或API时,连function参数的多寡和排列顺序,都可以从此得到参考资讯。更进一步,可以用word net把这些token做clustering,之后我们就可以打一些关键字,甚至打中文,让这个程式建议最多人用的习惯命名法…。

 

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/bvbook/archive/2011/02/17/6190582.aspx

posted @ 2011-02-17 09:03 博文视点 阅读(...) 评论(...) 编辑 收藏