我当面试官的时候,曾经问过一个问题:你觉得怎样的代码才是好的代码?这个问题其实没有标准答案,我是希望能了解面试者对待代码的态度。如果问我自己这个问题,我觉得第一个答案一定是:容易被其他程序员看懂的代码,才是好代码。
为什么代码需要容易被人看懂?因为这样才不容易改出 Bug 来。软件经常就是会被反复修改的。我知道某遥遥领先的大厂是不在乎这个的,因为早期他们家的代码最后是烧到硬件里的,所以改的没有那么频繁。但是对于其他行业来说,软件从诞生之日起,就会一直修改,直到这个程序彻底被人丢弃。不过,需要说明的是,光是容易被看懂,还不足以满足频繁的修改,还有其他很多要求及其解决方法,那些是软件工程更进一步的知识了。本文还是集中于“容易被看懂”这个目标。
然而,要让代码容易被人看懂,却一点都不容易。其实代码和文章、演讲有很多近似之处。如果一篇文章错字很多,词不达意,语序混乱,冗长而没有重点,那么这篇文章肯定就不容易被看懂;同样的,如果一个人说话口音很重,词汇缺乏,啰啰嗦嗦不着边际,甚至说的话前后颠倒没有逻辑,同样很难让人理解。所以代码要容易被看懂,又称为“易读性”好,是要遵循一些通行的标准的。这些标准通常会聚焦于以下几个方面:
命名: 代码的词汇
格式: 代码的句子和段落
模块划分: 代码的篇章
命名 :程序代码变量、函数、类、文件、目录,统统都是由程序员来进行命名的。这些无处不在的名字,就是这个代码工程的“词汇”。你可以把这些名字都编成“扰码程序”处理过的类似 v00000001 f____1ll() 的样子,也可以给出准确描述这些代码含义的名字。关于代码中命名的话题,从最基本的代码规范、设计模式、直到需求建模,都会一直反复的进行论述。
格式 :格式远远不止是段落缩进、括号对齐,而包含更多东西,譬如一个函数,应该设计五个参数,还是一个结构体类型的参数,返回值应该如何设计;一段代码中,if 嵌套应该由几层组成;一个分支代码段或者函数,应该包含多少行代码;哪些地方应该注释,内容应该写什么……这些都是代码格式的范畴。好的代码格式,能让即便很长的程序,也很容易被读懂和理解。而有一些秀技巧的代码,只需要一行就能让读者头晕眼花。
下面的例子是1987年国际C语言混乱代码大赛获奖的一行代码(结果是打印字符"unix"):
main() { printf(&unix["\021%six\012\0"],(unix)["have"]+"fun"-0x60);}
模块划分 : 我们一般不会在一个函数里面,或者一个源码文件里面把全部代码都塞进去。那么如何去划分代码模块,哪些代码应该被组织成函数,那些函数应该被视为一个“库”。如果你在写程序,那么这些决策无时无刻不在进行。这些决策显然应该有一些思想去指导来做出,而不是随当时的心情。所以一些代码规范和易读性标准,会提供建议。
网上有个中文版:https://zh-google-styleguide.readthedocs.io/en/latest/
Google 对很多语言的代码,都提出了自己的风格指引。当然这份指引不是唯一的选择,你也可以选微软或者别的某些人提出的。唯一需要注意的是,一旦选择了一个风格,就在一个项目中坚持使用下去,而不要混合几种风格。
Google 代码风格指引中包含了大量针对具体语言的技术性规定,这些规定往往能预防很多难以发现的 bug。譬如这份风格规定了,函数的输入参数应该使用 const 引用,输出参数使用指针。而由于 const 会“传染”,作为输入参数的对象,如果是 const 的话,调用的所有方法也必须声明为 const,所以方法内部的代码就不能修改成员属性。——这个 const 传染链路有可能会让代码写起来有的额外的麻烦,但却能逼着开发者分清楚一个操作,到底是否会产生不可预知的副作用。在 C++ 的代码规范中,这类的规定是特别多的,因为 C++ 的语言特性的用法非常灵活,在其他语言规范中,这种“技术性”约定就会少很多。
void fun(const &Obj param) {
...
param.do(); // param 被声明是只读的,只能调用 Obj 的“只读”方法
}
void Obj::do() const { // do() 方法必须要声明为 const--只读
... // 这里面的代码不能修改 Obj 对象
}
如果团队遵循了这份规定,那么在共同阅读代码的时候,自然就有很多不言自明的部分,从而提高易读性。譬如 C++ 规范中定义了可以单独编译的头文件,后缀使用 .inc,这样只要你看到一个 .inc 文件,就不必费心去想要链接什么库文件了。
尽可能使用描述性的命名, 别心疼空间, 毕竟相比之下让代码易于新读者理解更重要. 不要用只有项目开发者能理解的缩写, 也不要通过砍掉几个字母来缩写单词.
关于命名,其实大小写、下划线我觉得都不是核心问题,更重要的是里面的含义。我相信大部分程序员都不会使用 aa b01 这种名字,但是我在项目里面,却往往看到其他的一类“毫无意义”的名字: xxInfo xxManager xxCfg ……我自己也常常会陷入“不知道如何起名字”的困难中,特别是对于一些明显很“技术性”的代码开发的时候,这个时候往往已经举例业务逻辑比较远,更接近网络、数据库等等纯技术步骤,这个时候不但涉及的变量、函数很多,而且更加难对应业务领域的东西。但是每次这种时候,如果我不是着急把代码写完,而是仔细去想想这些名字代表的数据或代码的“原因”,往往并不难定义一个便于读懂的名字,只需要你不要吝啬多打几个字即可。每当你觉得不知道该怎么起一个有意义的名字的时候,可能背后的原因是没有好好的设计代码的结构。
关于格式问题,我觉得最重要的,就是坚持:不要让任何一个代码块(譬如函数、循环体、分支)在屏幕上的高度,超过你的脑袋的高度。——长长的代码块一定是有问题,要么里面包含了大量的重复代码,要么就是各种逻辑随便塞到一起。特别值得重视的,有一类“超长”代码块,是对于某个复杂的数据结构/类对象(可能包含几十个字段)的赋值,这类语句在平时一般危害不大,但是一旦对结构体字段/类属性进行修改,这种长长的赋值语句就会出错,如果是改了名字导致编译不过还好,如果是加了字段但没注意,使用了默认值,那才是难找的 bug。如何解决这种超大型结构体,是需要一定软件技巧的,例如使用 ORM 框架、用一份 IDL 文件生成所有此类数据需要的结构体等等……
说到命名和格式,有一些编程语言,真是把代码格式刻进了骨子里,譬如最出名的 Python,强迫统一缩进,又譬如 Go 语言,大写字母开头的成员是 public,小写字母代表 private。有时候我们学习语言的时候,也要了解这方面的一些观点。
向读者解释为什么写这段代码。注意解释的不是这段代码具体是怎么做事情,因为代码已经可以说清楚了。
用作某种特殊的程序“注解”,譬如用来生成接口文档(javadoc/doxygen),或者定义 C 接口定义(go 语言 C 函数),或者是作为单元测试标记、ORM 标记、序列化标记等等。
个人觉得上述第一种注释,是值得多花精力去写的。但也是最累人的,因为很多代码是在需求多次变更的情况下“演化”成这个样子的,要持续的维护注释,向一个虚空中的读者解释为什么会这样,是烦人的——所以不妨把这些注释的对象想象成自己,可能明天你失忆了,还得维护这个代码,你就需要这些文字。对于上面说的第二种注释,因为是功能性的,所以应该简洁清晰,甚至我会用不同的注释符号来标注(很多语言都支持行注释和段注释)。
最后说一下 lint 工具。这是最初是一个 python 编写的,用于检查源代码是否符合风格规范的小程序,譬如 cpplint 这个软件就是检查 C++ 代码。这个工具可以可以读入一份用 xml 标注的风格规范检查配置,你可以换上 google 或者别的风格规范。然后对源码中不符合规范的地方进行报告。这个工具以前我会放到“持续继承”的工作流里面,而现在的 IDE 很多都会有插件,在你写下代码的时候就直接提示你(通常是以 warning 提示)。所以把 warning 搞清楚,然后消除掉,其实也是一个很好的习惯。有些商业软件如 coverity 会检查源码的时候做更多的分析,甚至使用大模型进行检查,试图找出内存泄漏或者别的一些 bug,这类“静态检查”工具对于 C++ 这类语言其实还挺重要的,建议大家都找来试试。
如果你已经很熟悉了“代码风格规范”,那么下一步可能需要去啃一下所谓的“Effective”系列的书。这种书一般都会针对某个特定的语言,给你一大堆“建议”,有些建议看起来相当的“八股”,但是最重要的,不是去记住这些“条款”,而是去理解作者为什么会提出这些建议。根据我的经验,一般会基于以下几个原因:
我特别需要提醒的是最后一条:“关于代码易读性上的需求”。——这是个在相当多的程序员观念中不重要的问题,太多的程序员非常乐于追求程序的运行“性能”,而不在乎代码可读性。甚至我看到的技术方案,大多数也是描述性能问题如何解决的,而很少有把代码结构、理解程序作为重点来描述的。然而,真正让一个软件项目出问题的底层原因,正是代码易读性的问题:如果一段代码,连开发者自己都不愿意去理解它,那么肯定是通过随意补丁和无序的修改来实现功能的,随着需求不断变化,这个代码一定会成为“屎山”。每次当我们接手这种烂摊子的时候,除了心底默默问候某人以外,还要提醒自己不要也制造这种玩意出来。
之所以专门提及这本书,并不是说它里面的内容有特别特别牛逼的地方,而是这本书代表了一种观念,一种价值观:我们应该追求代码本身在可读性上的价值。这本书比较集中的讨论了代码本身对于可读性的改进手段,而不会像其他的书一样搅合进其他的诸如性能优化的目的。
从这本书的目录来说,大部分其实是和《Google 代码风格指引》类似的,所述内容也是有很多类似。但是如果你是希望总结和思考一个问题:怎样的代码才是好的代码,那么这本书可以给一些比较通行的答案。当然这本书里面比较多的例子是 Java 语言的,不过在其他语言中也是有类似的对应物。
光是满足代码规范,或者用心按照 Effective 的要求去写代码,其实并不能满足提升开发效率的全部需求。但这是一个很好的基础,如果连这个基础都没有,那么更多的,更高级的软件工程知识,可能就完全无法继续的。下面一篇我想谈谈“面向对象”,虽然这是一个非常“老土”的话题,但依然重要。
评论区
共 13 条评论热门最新