代码整洁之道
勒布朗法则:Later equals never.
随着混乱的增加,团队生产力也持续下降,趋近于零。生产力下降的时候,管理层只能增加更多的人手,期望提高生产力。
什么是整洁代码
我喜欢优雅和高效的代码。代码逻辑应当直截了当,叫缺陷难以隐藏;尽量减少依赖关系,使之便于维护;依据某种分层战略完善错误处理代码;性能调至最优,省得引诱别人做没规矩的优化,搞出一堆混乱来。整洁的代码只做好一件事。— Bjarne Stroustrup,C++ 语言发明者
整洁的代码应可由作者之外的开发者阅读和增补。它应有单元测试和验收测试。它使用有意义的命名。它只提供一种而非多种做一件事的途径。它只有尽量少的依赖关系,且要明确地定义和提供清晰、尽量少的 API。代码应通过其表面表达含义,因为不同的语言导致并非所有必需信息均可通过代码自身清晰表达。— Dave Thomas, OTI 公司创始人
整洁的代码总是看起来像是某位特别在意它的人写的。几乎没有改进的余地,代码作者什么都想到了。— 《修改代码的艺术》作者
有意义的命名
对于变量,如果其需要注释来补充,那就不算是名副其实。比如你需要定义一个变量,这个变量存储的是消逝的时间,其单位是天,那么下面是一些比较好的命名:
int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;
别用 accountList
来指一组账号,除非它真的是 List
类型,List
一词对于程序员有特殊意义,所以用 accountGroup
或 bunchOfAcounts
,甚至用 accounts
都会好一些。
别说废话,废话都是冗余。假如你有一个 Product
类,如果还有一个 ProductInfo
或 ProductData
类,它们虽然名称不同,意思却无区别。Info
和 Data
就像 a
、an
和 the
一样,是意义含混的废话。下面三个函数的命名,我们怎么知道应该调用哪个呢?
getActiveAccount();
getActiveAccounts();
getActiveAccountInfo();
使用常量,WORK_DAYS_PER_WEEK
比数字 5 要好找的多。
对于类名,其应该是名词或名词短语,如 Customer
、WikiPage
、Account
和 AddressParser
,避免使用 Manager
、Processor
、Data
或 Info
这样的类名。类名不应当是动词。
对于方法名,其应当是动词或动词短语,如 postPayment
、deletePage
或 save
。
为每一个抽象概念选一个词,并且一以贯之。例如使用 fetch
、retrieve
和 get
来给在多个类中的同种方法命名,你怎么记得住哪个类是哪个方法呢?在一堆代码中,有 controller
,又有 manager
,还有 driver
,就会令人困惑。
多数变量都依赖一个类、一个函数来给读者提供语境,但如果做不到的话,你可能就需要加上前缀。例如 addrFirstName
比 firstName
更能说明,你想表达的是地址的一部分,当然更好的方案是创建一个名为 Address
的类。当然也没必要添加不必要的语境,只要短名称足够清楚,就比长名称好。
语境不明确的变量
有语境的变量
如何写好函数
函数的第一个规则是短小。第二条规则还是要短小。
函数应该做一件事,做好这件事,只做这一件事。如何判断函数做了是否不止一件事,看是否能再拆出一个函数。要确保函数只做一件事,函数中的语句都要在同一抽象层级上。getHtml()
位于较高抽象层级,PathParser.render(pagePath)
位于中间抽象层,.append("\n")
位于相当低的抽象层。函数中混杂了不同的抽象层级,往往容易让人迷惑,读者无法判断出某个表达式是基础概念还是细节。
像如下带有 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
代码块丑陋不堪,最好把 try
和 catch
代码块的主题部分抽离出来,另外形成函数。错误处理本身就是一件事,这意味着在 try
应该是函数的第一个单词,catch/finally
是这个函数的最后的内容。
注释
代码在变动,在演化,但注释不能总是随之变动,注释会撒谎。注释不能美化糟糕的代码。
直接把代码注释掉是讨厌的做法,其他人不敢删除注释掉的代码,他们会想代码依然放在那儿,一定有其原因。
格式
代码每行展现一个表达式或一个子句,每组代码行展示一条完整的思路。这些思路用空白行区隔开来。每个空白行都是一条线索,标识出新的独立概念。往下读代码时,你的目光总会停留于空白行之后的那一行。
若某个函数调用了另外一个,就应该把他们放到一起,而且调用者应该尽可能放在被调用者上面,这样,程序有一个自然的顺序。
对象和数据结构
乱加 set
和 get
时最坏的选择,不要暴露数据细节,而要以抽象形态表述数据。
暴露了数据细节的车辆
百分比抽象
过程式代码便于在不改动现有数据结构的前提下添加新的函数,面向对象代码便于在不改动现有函数的前提下添加新的类。
The Law of Demeter 认为模块不应了解它所操作对象的内部情形,对象应该隐藏数据,暴露操作。下面代码违反了:
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
最为精炼的数据结构,是一个只有公共变量、没有函数的类,这种数据结构就是 DTO(Data Transfer Objects),这种数据结构在与数据库通信、解析套接字传递的消息之类场景中,非常有用。
错误处理
使用 Checked Exception
的依赖成本要高于收益,每个调用该函数的函数都要捕获它,或者添加合适的 throw
语句,最终得到的时一个从软件最底端贯穿到最高端的修改链,封装被打破,抛出路径上的每个函数都要去了解下一层的异常细节。
将第三方 API 打包是个良好的实践手段,降低了对它的依赖,未来可以不太痛苦地改用其它代码库,你也可以不必绑死在某个特定厂商的 API 设计上。
返回 null
的时候,考虑是否可以直接抛出异常,或者返回一个特定的对象,尽量不要返回 null
,它在给调用者添乱。返回 null
是糟糕的做法,那么传递 null
值给其它方法就是更糟糕的了。
单元测试
测试带来一切好处。
类
系统应该由许多短小的类而不是少量巨大的类组成。
对类加以组织,可以降低修改的风险。
一个必须打开修改的类
一组封闭类