代码整洁之道

代码整洁之道

勒布朗法则:Later equals never.

随着混乱的增加,团队生产力也持续下降,趋近于零。生产力下降的时候,管理层只能增加更多的人手,期望提高生产力。

时间和生产力

什么是整洁代码

我喜欢优雅和高效的代码。代码逻辑应当直截了当,叫缺陷难以隐藏;尽量减少依赖关系,使之便于维护;依据某种分层战略完善错误处理代码;性能调至最优,省得引诱别人做没规矩的优化,搞出一堆混乱来。整洁的代码只做好一件事。— Bjarne Stroustrup,C++ 语言发明者

整洁的代码应可由作者之外的开发者阅读和增补。它应有单元测试和验收测试。它使用有意义的命名。它只提供一种而非多种做一件事的途径。它只有尽量少的依赖关系,且要明确地定义和提供清晰、尽量少的 API。代码应通过其表面表达含义,因为不同的语言导致并非所有必需信息均可通过代码自身清晰表达。— Dave Thomas, OTI 公司创始人

整洁的代码总是看起来像是某位特别在意它的人写的。几乎没有改进的余地,代码作者什么都想到了。— 《修改代码的艺术》作者

有意义的命名

对于变量,如果其需要注释来补充,那就不算是名副其实。比如你需要定义一个变量,这个变量存储的是消逝的时间,其单位是天,那么下面是一些比较好的命名:

int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;

别用 accountList 来指一组账号,除非它真的是 List 类型,List 一词对于程序员有特殊意义,所以用 accountGroupbunchOfAcounts,甚至用 accounts 都会好一些。

别说废话,废话都是冗余。假如你有一个 Product 类,如果还有一个 ProductInfoProductData 类,它们虽然名称不同,意思却无区别。InfoData 就像 aanthe 一样,是意义含混的废话。下面三个函数的命名,我们怎么知道应该调用哪个呢?

getActiveAccount();
getActiveAccounts();
getActiveAccountInfo();

使用常量,WORK_DAYS_PER_WEEK 比数字 5 要好找的多。


对于类名,其应该是名词或名词短语,如 CustomerWikiPageAccountAddressParser,避免使用 ManagerProcessorDataInfo 这样的类名。类名不应当是动词。


对于方法名,其应当是动词或动词短语,如 postPaymentdeletePagesave


为每一个抽象概念选一个词,并且一以贯之。例如使用 fetchretrieveget 来给在多个类中的同种方法命名,你怎么记得住哪个类是哪个方法呢?在一堆代码中,有 controller,又有 manager,还有 driver,就会令人困惑。

多数变量都依赖一个类、一个函数来给读者提供语境,但如果做不到的话,你可能就需要加上前缀。例如 addrFirstNamefirstName 更能说明,你想表达的是地址的一部分,当然更好的方案是创建一个名为 Address 的类。当然也没必要添加不必要的语境,只要短名称足够清楚,就比长名称好。

语境不明确的变量

语境不明确的变量

有语境的变量

有语境的变量 有语境的变量

如何写好函数

函数的第一个规则是短小。第二条规则还是要短小。

函数应该做一件事,做好这件事,只做这一件事。如何判断函数做了是否不止一件事,看是否能再拆出一个函数。要确保函数只做一件事,函数中的语句都要在同一抽象层级上。getHtml() 位于较高抽象层级,PathParser.render(pagePath) 位于中间抽象层,.append("\n") 位于相当低的抽象层。函数中混杂了不同的抽象层级,往往容易让人迷惑,读者无法判断出某个表达式是基础概念还是细节。

像如下带有 switch 函数的代码,有几个问题。太长、违反单一原则、违反开放闭合原则(添加新类型,必须修改)等,该问题的解决方案是将 switch 语句埋到抽象工厂底下,不让任何人看到。

Switch 语句

switch 语句

用多态封装 Switch 语句

用多态封装 Switch 语句

好名称的价值怎么好评都不为过,别害怕长名称,长而具有描述性的名称,要比短而令人费解的名称好,要比描述性的长注释好。别害怕花时间取名字。

关于函数参数,除非你有足够特殊的理由,才能用三个以上的参数。对于有一个参数的函数,如果要对这个参数进行某种转换操作,那么应该使用返回值来返回转换后的值:StringBuffer transform(StringBuffer in) 要比 void transform(StringBuffer out) 强。

如果函数看来需要两个、三个或三个以上的参数,说明其中一些参数就需要封装为类了:

Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double radius);

给函数起一个好名字,能够解释函数意图、参数顺序的名字。writeField(name) 要比 write(name) 强,assertExpectedEqualsActual(expected, actual) 要比 assertEqual 强,这大大减轻了记忆参数的负担

确保函数无副作用,函数承诺做这件事,不要在其内部偷偷地做其它事情。

try/catch 代码块丑陋不堪,最好把 trycatch 代码块的主题部分抽离出来,另外形成函数。错误处理本身就是一件事,这意味着在 try 应该是函数的第一个单词,catch/finally 是这个函数的最后的内容。

注释

代码在变动,在演化,但注释不能总是随之变动,注释会撒谎。注释不能美化糟糕的代码

直接把代码注释掉是讨厌的做法,其他人不敢删除注释掉的代码,他们会想代码依然放在那儿,一定有其原因。

格式

代码每行展现一个表达式或一个子句,每组代码行展示一条完整的思路。这些思路用空白行区隔开来。每个空白行都是一条线索,标识出新的独立概念。往下读代码时,你的目光总会停留于空白行之后的那一行。

若某个函数调用了另外一个,就应该把他们放到一起,而且调用者应该尽可能放在被调用者上面,这样,程序有一个自然的顺序

对象和数据结构

乱加 setget 时最坏的选择,不要暴露数据细节,而要以抽象形态表述数据

暴露了数据细节的车辆

暴露了油箱容量和剩余油量的车辆

百分比抽象

百分比抽象

过程式代码便于在不改动现有数据结构的前提下添加新的函数,面向对象代码便于在不改动现有函数的前提下添加新的类。

The Law of Demeter 认为模块不应了解它所操作对象的内部情形,对象应该隐藏数据,暴露操作。下面代码违反了:

final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();

最为精炼的数据结构,是一个只有公共变量、没有函数的类,这种数据结构就是 DTO(Data Transfer Objects),这种数据结构在与数据库通信、解析套接字传递的消息之类场景中,非常有用。

错误处理

使用 Checked Exception 的依赖成本要高于收益,每个调用该函数的函数都要捕获它,或者添加合适的 throw 语句,最终得到的时一个从软件最底端贯穿到最高端的修改链,封装被打破,抛出路径上的每个函数都要去了解下一层的异常细节。

将第三方 API 打包是个良好的实践手段,降低了对它的依赖,未来可以不太痛苦地改用其它代码库,你也可以不必绑死在某个特定厂商的 API 设计上。

返回 null 的时候,考虑是否可以直接抛出异常,或者返回一个特定的对象,尽量不要返回 null,它在给调用者添乱。返回 null 是糟糕的做法,那么传递 null 值给其它方法就是更糟糕的了。

单元测试

测试带来一切好处

系统应该由许多短小的类而不是少量巨大的类组成。

对类加以组织,可以降低修改的风险。

一个必须打开修改的类

一个必须打开修改的类

一组封闭类

一组封闭类 一组封闭类