首页 > 代码库 > 编写可读代码艺术之表面层析
编写可读代码艺术之表面层析
前言
4年前,我拒绝自己承认程序员,那时在8位MCU上用C语言处理ROM芯片时序问题。
1年前,我不承认自己是一个程序员,那时我在处理工业相机返回的三维数据。
现在,我不得不承认乐于去成为自己是一个程序员、工程师,程序员几乎无所不能,虽然很苦逼。程序员就要干程序员事,这篇就算小铺开张吧,写的不好,多多原谅。
如果说自己的编程历史,07年我在用C语言,09年我在用Verilog,11年我开始用C++。不管什么时候、什么语言,我对自己的基本要求是:
代码要有注释,代码架构要清晰。
但是在我看完《the art of readable code》 之后,我对“清晰代码”的认知可能发生了一下改变,这本书其实很罗嗦,因为讲的一些东西可能是每个程序员每天都在做的事情,但是和《clean code》一样,每次看完可能都会有一些收获。
说实话,我经历过几个公司都在要求代码在规定时间内完成什么功能,但是没有人去要求你或者告诉你应该怎么写代码之类的,可能你也是其中的一员,代码的可读性或者说清晰,只能自己来要求自己来完成。我用了一个周末来把这本书加上自己的一些见解搬到博客上来,就是希望有更多的人去思考一些代码可读性,让自己对周围的众屌丝更加的亲和。
作者在书的开始就阐明在这本书的基本原则:代码的写法应当使别人理解它所需的时间最小化。声明一点:代码的可读性如果可以从机器和人两个角度来理解,机器当然优先,每个程序员都想把代码的效率提高上去,但是我们每天处理的很多事情其实主要是顶层逻辑,大多可能是类似于大型游戏这样的多人协同开发,这个时候牺牲一点顶层逻辑的效率而提高人的合作效率,明显是更高明的选择,这也就是本书或者类似的书出现的原因。
这篇文章主要从表面层析进行介绍,后续会对结构方面阐述。作者对“避免使用像tmp和retval这样泛泛的名字“,”用具体的名字替换抽象的名字“,”为名字附带更多信息“,”利用名字的格式来传递含义“等等进行讨论,不过我个人理解为: 如何让名字传递更多的意义,让阅读者马上就能明白代码要传递的意义?可以分为两层来看,一层就是作者用很大篇章描述的字面意思,选用更专业、更有表现力、不容易被误解的、带有附加信息的名字,第二层就是名字的格式问题,类似于匈牙利标示法等等.
1 选择更专业、更具表现力的词汇,而不使用泛泛而谈的名字
比如,"Get" 这个词汇,只要是获取我们首先想到的就是这个词,下面的语句:
void GetPage(url)
表面意思理解就是“获取页面”,但是"Get"除了获取之外,你看打不到更多的意思,有可能是从数据库,也有可能是从互联网?
如果是从互联网你可以使用更专业的词汇:DownLoad,如果是 void DownLoadPage(), 阅读者是不是更清晰呢?Opencv中有一个从摄像机获取函数的名字 void GrabImage() , 是不是比GetImage()更能表达从设备回去一帧图像的意思呢?
英文其实和中文一样,有很多同义词,但是每个词又有其独特的意义,比如下面作者列举出来的:
send -> deliver, dispatch, announce, distribute, routefind -> search, extract, locate, recoverstart -> lanuch, create, begin, openmake -> create,set up, build, generate, compose, add, new
小伙伴们还是学好英语吧~~
2 给使用temp、retval等空泛名字一个理由
在c++、c#等类似的语言已经对变量有效范围作了很好的处理,所以很多工程师已经意识到在命名空间、类等全局中和在一个for循环中对变量的命名是不一样的,在小的循环中还是很喜欢用这种泛泛的名字,比如下面:
for( int i = 0 ;i < 10 ; ++i ){ int temp = i ;}
在这个循环之外,i,temp已经没有作用域,所以没有太多的误解存在,但是类似于下面的多重循环可能就要考虑下变量的意义
for (int i = 0; i < clubs.size(); i++){ for (int j = 0; j < clubs[i].members.size(); j++) { for (int k = 0; k < users.size(); k++) if (clubs[i].members[k] == users[j]) cout << "user[" << j << "] is in club[" << i << "]" << endl; }}
3 为名字附加更多的信息
名字应该更加直接的描述变量或者方法的作用,阅读后马上知道这个名字背后是要做什么,比如变量的作用域、变量作用等等,作者给出了几条建议:
(1) 增加重要细节,比如对于一些带有物理性质的变量,在变量后面加上单位更直观:
Start(int delay) --> delay → delay_secs //时间单位 CreateCache(int size) --> size → size_mb //字节单位ThrottleDownload(float limit) --> limit → max_kbps //下载速度单位 Rotate(float angle) --> angle → degrees_cw //角度单位
password -> plaintext_password //文本需要加密才能使用 comment -> unescaped_comment //注释需要转义之后才能显示 html -> html_utf8 //html格式data -> data_urlenc //数据输入格式
(2)附带其他属性,最有名的莫过于匈牙利命名法,通过类型前缀就可以给出变量的类型信息,虽然这个东西争议很大,包括微软现在在C#中也开始使用Pascal和Camel混合的命名方式,不过对于我这样从VC开始写程序的人而言,匈牙利还是印象深刻,比如下面:
m_ | Data member of a class | 一个类的数据成员 |
msg | message | 消息 |
p | Pointer | 指针 |
s | string | 字符串型 |
(3) 对于名字的长短问题,作者指出:在比较小的作用域内,可以使用较短的变量名,在较大的作用域内使用的变量,最好用长一点的名字,编辑器的自动补全都可以很好的减少键盘输入。对于一些缩写前缀,尽量选择众所周知的(如str),一个判断标准是,当新成员加入时,是否可以无需他人帮助而明白前缀代表什么。
(4)利用名字的格式来传递含义,比如字母大小写、下划线等。这个在很早之前就用到,静态:s_Xxx, 全局:g_Xxx, 类变量m_Xxx,指针:pXxx;
4 使用不会误解的名字
作者这里的意图更偏向于程序员内部的默认规则,当你写程序的时候不应该去挑战这个规则,从而引发一些不必要的麻烦。
(1)命名最清楚的方式是在要限制的东西前面加上max_,min_
CART_TOO_BIG_LIMIT = 10 if shopping_cart.num_items() >= CART_TOO_BIG_LIMIT: Error("Too many items in cart.") // 替换MAX_ITEMS_IN_CART = 10 if shopping_cart.num_items() > MAX_ITEMS_IN_CART: Error("Too many items in cart.")
(2)用first,last表示包含的范围,用start,end表示包含\排除范围
(3)bool值明确意义,建议加上is\has\can\should这样的词汇,这个有又要说,我一般是使用is前缀,不过这里给出的几个词汇就更加具体
SpaceLeft() --> hasSpaceLeft()bool disable_ssl = false --> bool use_ssl = true
(4)函数名应当不带歧义,并且符合大多数程序员的期望,比如get().size(),肯定是轻量级别的
5 审美(整体格式
(1)基本原则:使用一致的布局,让阅读者很快去适应这种风格;相似的代码看上去相似;相关代码分组,形成代码块
(2)重新安排换行来保持一致和紧凑,占用更多的行列,不见得是好事哈
(3)用方法来整理不规则的东西,比如使用函数替代大量的重读代码,让函数本身去处理主要负责的事情
(4)在需要的时候使用列对齐,比如同一个方法的连续调用,数组的初始化等等
(5)选用一个有意思的顺序,推荐原则:最重要到最不重要,按字母顺序,和HTML表单循序匹配等等
(6)把声明或者代码组织成段落,相似功能或者想法的放在一起
6 一致行
最重要的:一致的风格比“正确”的风格更重要。我把这个单独列出为一条,相对而言,风格没有所谓的对与错,但是对于团队而言一致的风格更容易交流、沟通。
7 注释
看完这章之前,我的代码基本是充满注释,习惯性了已经,但是这本书给我最大的冲击就是这里,无用的注释其实是徒劳,只会增加阅读者的反感和难度,我的注释经常就像下面的,所以作者说:好代码 > 坏代码 + 注释 。
// The class definition for Account class Account
{ public: // Constructor Account(); // Set the profit member to a new value void SetProfit(double profit); // Return the profit from this Account double GetProfit(); };
(1)不要为不好的名字写注释,而是应该把名字改好
// Enforce limits on the Reply as stated in the Request, // such as the number of items returned, or total byte size, etc. void CleanReply(Request request, Reply reply);
上面这段注释的大部分都在解释clean是什么意思,那不如换个正确的名字:
// Make sure ‘reply‘ meets the count/byte/etc. limits from the ‘request‘ void EnforceLimitsFromRequest(Request request, Reply reply);
(2)加入“导演评论”,为什么代码写成这样而不是那样,比如你对算法效果、可能存在的改进方案
// Surprisingly, a binary tree was 40% faster than a hash table for this data. // The cost of computing a hash was more than the left/right comparisons. // This heuristic might miss a few words. That‘s OK; solving this 100% is hard. // This class is getting messy. Maybe we should create a ‘ResourceNode‘ subclass to // help organize things.
(3)代码的瑕疵,代码将来如何改动 如TODO:xxxx
// TODO: use a faster algorithm // TODO(dustin): handle other image formats besides JPEG NUM_THREADS = 8 # as long as it‘s >= 2 * num_processors, that‘s good enough.
(4)给常量加注释,可能并不是常量本身,而是常量的值应用情形
// Impose a reasonable limit - no human can read that much anyway. const int MAX_RSS_SUBSCRIPTIONS = 1000;
(5)公布可能存在的陷进
// Calls an external service to deliver email. (Times out after 1 minute.) void SendEmail(string to, string subject, string body);
(6)为意料之外的行为加注释,比如代码实现的技巧
// Force vector to relinquish its memory (look up "STL swap trick") vector<float>().swap(data);
(7)全局观注释,比如为文件/ 类级别上使用,其实还有接口、类、类间交互等等
// This file contains helper functions that provide a more convenient interface to our // file system. It handles file permissions and other nitty-gritty details.
(8)总结性注释(比如在比较长的代码段中,为每段代码加上注释),不让读者迷失在细节中
void GenerateUserReport(): # Acquire a lock for this user ... # Read user‘s info from the database ... # Write info to a file ... # Release the lock for this user
编写可读代码艺术之表面层析