这是鼎叔的第七十篇原创文章。行业大牛和刚毕业的小白,都可以进来聊聊。

欢迎关注本专栏和微信公众号《敏捷测试转型》,星标收藏,大量原创思考文章陆续推出。

相关文章:聊聊单元测试的整洁 https://mp.weixin.qq.com/s?__biz=MzkzMzI3NDYzNw==&mid=2247484306&idx=1&sn=3e894ecdee46cd063fe5fe709ec40c79&chksm=c24fb6f0f5383fe6de52d66e83acd15c7a7a71a8cb5fa5a2dd61e5795f3e9b07e065f61ad32f&scene=21#wechat_redirect

《代码整洁之道》这本书最有影响力的一个观点,就是代码的好名字本身就解释了最重要的信息,如无必要,不要增加代码注释。这个观点和传统软件质量观点是不同的。

Part1 为什么代码需要整洁

代码永远都需要有人来书写,因为我们永远无法抛弃背后的精确性。人工智能无法创造出满足客户模糊感觉的成功系统来,它无法透彻地理解需求,需求的细节是无法被忽略或抽象的。

经常有人说烂代码回头有空再清理,实际上再也没有机会清理,这就是 LeBlanc 法则 - 稍后等于永不。

能分辨代码是否整洁,并不代表能写出整洁代码,实践上需要贯彻大量的小技巧,刻苦才能习得 “整洁感”,好的 “代码感” 能帮助程序员选出最好的方案。

程序开发大师们对于整洁代码的定义如下:

优雅和高效的代码令人愉悦,整洁代码只做好一件事。每个例程都像是为了专门解决某个问题而存在的。

整洁的代码在细节上花心思,果断决绝,展示出要解决问题的张力和表达力。阅读代码时无需花费力气,简单到一气呵成。

整洁的代码便于让其他人增补,可读性强,代码块越小越好,没有重复,充分体现了系统的设计理念,且只包含尽量少的实体。

代码的整洁还和测试息息相关,光有可读性也做不到 “干净”。

整洁代码的实践军规,就是每次代码签入时,都比签出时干净。

Part2 有意义的命名

给代码起好名字的几条简单规则:

一 名副其实

多花时间选择好名字,一旦有更好的命名,就替换旧的。变量、函数和类的名称应该已经答复了所有的大问题,包括为什么存在,做什么事,该怎么用。如果你还需要注释来补充,那就不算是名副其实。

比如,表示消逝时间的名称 d,可以换成 XXXTimeInDays。再如,开发扫雷游戏,盘面是名为 theList 的单元格列表,可以改为 gameBoard。

如果我们发现代码用状态值 4 来表示 “已标记”,就用 isFlagged 来掩盖 4 这个魔术数,提高可读性。

二 避免误导

开发者应该避免留下掩盖代码本意的错误线索。比如:

不要使用平台专有名称,比如 Unix 平台的 “hp”,“aix”。名称中谨慎包含 “list”,它对程序员有特殊含义。

提防使用外形相似度较高的名称。提防拼写前后不一致带来的误导。如果相似的名称按字母顺序放在一起时,差异很明显,那就相当有助益。

警惕命名中包含的小写 “l” 和大写 “O”,它们看起来像数字 1 和 0。

三 做有意义的区分

很多人喜欢按数字系列命名,如 a1,a2,a3...aN,这样的名字非常误导,没有提供导向作者意图的线索。如果我们想把 a1 的值赋给 a2,我们可以把参数名称改为 source 和 destination。

废话是另外一种没有意义的区别,比如 ProductInfo 和 ProductData 这两个类,从含义上看没有不同。

废话都是冗余。比如在变量名中出现 variable,在表名中出现 table。NameString 中的 String 都是废话,名称肯定是字符类型。

如果缺乏明确约定,很多变量看起来就没有区别。

四 使用读得出来的名称

人类对于能读出来的单词就更容易记忆,不善加利用就是一种耻辱了。另外,读不出来的名称,在开发者讨论时就会显得特别傻。

例如,genymdhms 这个函数,本意是生成的日期 - 年月日时分秒,我们修改为生成时间戳-gengeration timestamp,就舒服多了。

五 使用便于搜索的名称

开发环境经常会通过代码搜索来提高效率,如果使用单字母名称(比如 e)或纯数字常量(比如 7),在搜索时就会很郁闷。

名称长短应该和它作用域大小相对应,尤其对于可能多次使用的变量或常量,要赋予其便于搜索的名称。比如 WORK_DAYS_PER_WEEK 比数字 5 要好找得多,也体现了作者意图。

六 避免使用没有共识的编码语言

早期的编译器不做强制类型检查,程序员需要用匈牙利语标记法帮助自己记住类型,但现代编程语言不需要了,也降低了类型编码误导读者的可能性。

人们也习惯无视代码的前缀(如 m_)和后缀,只看道名称中有意义的部分。也不需要使用 “I” 前缀来代表这是个接口,只需要告诉对方我们要实现什么。某些情况下,我们可以用前缀给一系列相关变量提供必要的语境,让读者明白它们是某个更大结构的一部分。

记住,只有程序员才会认真读你的代码,原则上多用计算机科学的术语,少用涉及业务领域的专有名词,毕竟协作程序员不可能经常跑去问客户这些名词的含义。但是如果没有更贴切的术语来命名,用业务领域的专有名词也是可以的。

七 避免抖机灵

专业程序员把 “明确” 作为王道,编写他人善于理解的代码,而不是炫耀聪明,也不是用笑话耍宝来展示幽默感。

也别用 “双关语”,将同一词语用于不同目的。比如别用 eatMyShorts() 来表示 abort()。

八 精确,每个概念对应一个词

给每一个抽象概念选定一个词,并一以贯之。

类名和对象名应该是名词或名词短语。名字的精确性很重要,比如 address 类下的 web 地址实体,我们可以使用 PostalAddress,MAC,URI 来命名。主要短名词足够清楚,就比长名称好。

方法名应该是动词或动词短语。

起好名字最有挑战的地方是,需要良好的描述技巧和共有的文化背景,需要教学,持续下去,效果会立竿见影。

Part3 函数的整洁

足够短小

函数的第一条规则是短小,第二条规则是要更短小。函数应该只做一件事情,做好这件事情。

上个世纪,对于函数短小的说法是,函数长度不超过一屏,相当于 20 行,到今天,我们也认为 20 行以内的函数长度最佳。

像 if,else,while 等语句,其中的代码块最好就是一行 - 比如就是一个命名清晰的调用函数,即函数不应该大到足以容纳嵌套结构。

只做一件事的函数是无法被合理地切分为多个区段的,函数中的语句应该在同一抽象层级上,否则,读者无法判断某个表达式是基础概念还是细节。

我们想让代码拥有自上而下的阅读顺序,每个函数后面都跟着位于下一个抽象层级的函数,并说明为了达到什么目的而调用它。

函数的参数

函数的参数个数,最好是 0,其次是 1,再次是 2,尽量避免 3 。参数们带有太多的概念性,它们和函数名不在同一个抽象层次,需要读者了解目前并非特别重要的细节。如果参数较多,说明其中一些参数应该封装成类。

从测试角度,参数越多,确保参数各种组合的运行正确性非常困难。输入参数如果是布尔值,更违背了本函数只做一件事的初衷。

使用输出参数更让人困惑,要小心其是否导致古怪的时序耦合错误。我们习惯于信息通过返回值从函数中输出,而不是通过输出参数。

对于单参数函数来说,参数和函数应该形成非常良好的动名词对的形式,如 writeField(name), assertExpectsEqualsActual(expected,actual)。

函数要么做什么事,要么返回什么信息,不要两者同时干,这样容易混淆。

错误代码处理

发生错误时,应使用异常替代错误码,这样就能把异常处理代码从主路径代码中分离出来,达到 “每个函数只做一件事” 的目的,处理错误的函数不应该做其他事。而返回错误码,就是在要求调用者立刻处理错误。

使用异常替代错误码还有一个好处,新异常就可以从异常类派生出来,无需重复编译或部署。

别重复自己

重复可能是软件中一切邪恶的根源。整个模块的可读性会因为重复的消除而提升,许多实践规则都是为了控制重复而创立的。比如,面向对象编程中,将代码集中在基类。

结构化编程

所谓结构化编程,就是每个函数,以及函数中的每个代码块,都应该只有一个入口和一个出口。那么,老编程语言中的 GOTO 语句就不应该存在,而循环中也不应该有 break 语句。越是在大函数中,这些规则就越有效。

写代码和写文章类似,一开始没有严格规则,不断推敲打磨,直到它变成心目中的样子。大师级程序员都是把系统当成故事来讲,而不是当作程序来写,函数编写必须被干净利落地拼装在一起,形成精确而清晰的语言,帮助我们讲好故事。

对象和数据结构

使用数据结果的过程式代码,难以添加新数据结构,因为必须修改所有函数;面向对象代码难以添加新函数,因为必须修改所有类。

Demeter 定律认为,模块不应该了解它所操作对象的内部情景,对象应该隐藏数据和内部结构,暴露操作接口,因此,方法不应该调用由任何函数返回的对象的方法。

最为精炼的数据结构,是一个只有公共变量,没有函数的类,它有时被称为数据传送对象(DTO:Data Transfer Objects),主要被用于数据库通信和解析套接字消息之类的场景中。

错误处理

当我们提到错误处理,首先不要返回 null 值,也不要给其他方法传递 null 值(除非 API 指定这么做)。

下篇,将分享注释、代码格式、类、系统如何做到整洁,以及代码坏味道的处理清单。


↙↙↙阅读原文可查看相关链接,并与作者交流