海到无边天作岸,山登绝顶我为峰。
作为猿类的我们,对自己创造的代码有着一种天生的无比自信。这是好事~
可是,对于我们的读者而言它真的有那么自信吗?它真的有那么优雅吗?他真的从来没有吐槽过吗?
前段时间,像挤牙膏一样每天挤一点时间终于啃完了这本书-《代码整洁之道》,感觉收获颇深。在这篇文章里,我将会带你走进作者眼中的整洁世界,希望这篇文章可以帮助更多朋友们快速吸收代码整洁之道的精华。
一. 为什么代码整洁如此重要?
破窗理论
首先让我来看三张图。
窗户破损了的建筑让人觉得似乎无人照管,于是别人也不关心,放任窗户继续破损。最终自己也参加破坏活动,在外墙上涂鸦,任垃圾堆积,一扇破损的窗户开辟了大厦走向倾颓的道路。
可见:一件很小坏事儿,如果不加控制的话,也会演变成严重的事件
糟糕的代码毁了这家公司
先来研读下下面这张流程图:
你发现了什么?
糟糕的代码会形成恶性循环!制造混乱这种事或许我们都干过。我们都曾经瞟过一眼自己亲手造成的混乱,决定弃之而不顾,走向新一天。我们都曾经看到自己的烂程序居然能运行,然后断言能运行的烂程序总比什么都没有强。我们都曾经说过有朝一日再回头清理。当然,在那些日子里,我们都没听过勒布朗(LcBlanc)法则
:稍候等于永不(Later equals never),难道不是吗?
由此可见:
- 花时间保持代码整洁不但有关效率,还有关生存
- 制造混乱无助于赶上期限,混乱只会立刻拖慢你,叫你错过期限
- 赶上期限的唯一方法就是始终尽可能保持代码整洁
二. 什么是整洁的代码?
编写整洁代码的程序员就像是艺术家,他能用一系列变换把一块白板变成由优雅
代码构成的系统-代码感
。
整洁代码的艺术
我们来看看几位计算机届顶级大牛是怎么说的。
Bjarne Stroustrup (C++ 发明者):
我喜欢优雅和高效的代码。代码逻辑应当直截了当,叫缺陷难以隐藏;尽量减少依赖关系,使之便于维护;依据某种分层战略完善错误处理代码;性能调至最优,省得引诱别人做没规矩的优化,搞出一堆混乱来,整洁的代码只做好一件事。
在此强调“优雅”一词:外表或举止上令人愉悦的优美和雅观;令人愉悦的精致和简单。就像我们心中的那位女神,你看,多么赏心悦目!
Grady Booch(《面向对象分析与设计》作者):
整洁的代码简单直接。整洁的代码如同优美的散文。整洁的代码从不隐藏设计者的意图,充满了干净利落的抽象和直截了当的控制语句。
三. 如何尽力做到代码整洁?
好啦,重点来啦,大家拿起笔做好笔记哟。
1. 有意义的命名
核心词:表达力
代码是写给人看的,顺便能在机器上运行。写代码就像写小说或散文一样,要把你想描述的场景通过准确优雅的词汇表达得淋漓尽致。根据故事的需要和发展一步步调整结构和布局,最终演化为如此精妙的全篇。
言到意到,意到言到。所以选个好词汇非常重要,它省下了读者(包括自己)理解意图的时间。
- 包名、目录名、文件名:表达的是系统架构的设计方式
- 变量名、函数名、参数名、类名:表达的是业务具体的实现方式
1.1 名副其实
好的名称本身就具有一定的自我说明性,它本身就表达着它为什么会存在、它做什么事、应该怎么用。如果名称需要注释来补充,那就不算名副其实
。
int d; //消逝的时间,以日计
int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;
public List getThem(){
List list1 = new ArrayList();
for(int[] x : theList)
if(x[0] == 4)
list1.add(x);
return list1;
}
public ListgetFlaggedCells(){
List flaggedCells = new ArrayList();
for(int[] cell : gameBoard)
if(cell[STATUS_VALUE] ==FLAGGED)
flaggedCells.add(cell);
return flaggedCells;
}
public ListgetFlaggedCells(){
List flaggedCells = new ArrayList();
for(Cell cell : gameBoard)
if(cell.isFlagged())
flaggedCells.add(cell);
return flaggedCells;
}
| |
只是简单改一下名称,就能轻易之道发生了什么。这就是选好名称的力量。
1.2 避免误导歧义、别用双关词
用accountGroup要比accountList来指称一组账号更好;
避免将同一单词用于不同目的。用insert或append来表达通过增加或连接两个现存值来获得新值,要比用add命名更好,add是双关词。
1.3 做有意义的区分
废话是一种没有意义的区分,例如:Product、ProductInfo、ProductData类,名称虽然不同,但意思却无区别。Info和Data就像a、an、the一样,是意义含糊的废话。
废话都是冗余。Variable永远不要出现在变量名中,Table永远不应当出现在表名中,NameString会比Name好吗?Customer与CustomerObject区别何在?
1.4 使用读得出来的名称、使用可搜索的名称
genymdhms(生成日期,年月日时分秒)怎么读;
搜索WORK_DAYS_PER_WEEK很容易,但想找数字5就麻烦了;
1.5 添加有意义的语境
- 给中间变量取更有意义的名称;
- 把算法切成不同的语句块,然后把语句块封装为更有意义的函数,这让算法变得更为
干净利落
;
1.6 永远不要出现错误的名称
- 单词拼写错误,你让读者怎么猜?
2. 函数
2.1 短小
函数的第一规则是要短小,第二规则是还要更短小。作者说,我无法证明这个断言,但是经验告诉我,函数就该短小。
2.2 只做一件事
函数应该做一件事,做好这件事,只做这一件事。
单一职责原则
:只有一个修改它的理由。
如果函数只是做了该函数名下同一抽象层上的步骤,则函数还是只做了一件事。编写函数毕竟是为了把大一些的概念拆分成另一抽象层上的一系列步骤。
2.3 每个函数在同一个抽象层级
要确保函数只做一件事,函数中的语句都要在同一抽象层级上。函数中如果混杂不同抽象层级,往往让人迷惑,读者可能无法判断某个表达式是基础概念还是细节。更恶劣的是,就像破损的窗户,一旦细节与基础概念混杂,更多的细节就会在函数中纠结起来(耦合),这一点非常重要。
自顶向下,逐步求精
(也是结构化程序设计方法)(推荐一本书《 金字塔原理》
)
程序设计时,应先考虑总体,后考虑细节;先考虑全局目标,后考虑局部目标。不要一开始就过多追求众多的细节,先从最上层总目标开始设计,逐步使问题具体化。
2.4 使用描述性的名称
函数越短小,功能越集中,就越便于去个好名字
- 别害怕长名称。长而具有描述性的名称,要比短而令人费解的名称好。
- 别害怕花时间取名字。
- 命名方式要保持一致。
2.5 函数参数
尽量写零参数、单参数和双参数函数,应尽量避免定义三参数函数。超过三个参数请封装成对象,有足够特殊的理由才能用三个以上参数(多参数函数)—— 所以无论如何也不要这么做。
- 标识参数丑陋不堪。向函数传入布尔值简直就是骇人听闻的做法,这样做,方法签名立刻会变得复杂起来,大声宣布本函数不止做一件事。
String render(Boolean isSuite);
String renderForSuite();
String renderForSingleTest();
- 某些参数是否应该封装成类?
Circle makeCircle(double x, double y, double radius);
Clicle makeCircle(Point center, double radius);
2.6 无副作用
副作用是一种谎言。函数承诺只做一件事,但还是会做其他被隐藏起来的事,这就不对了。这对函数具有破坏性,会导致古怪的时序性耦合及顺序依赖。
- 尽量避免使用输出参数。如果函数必须要修改某对象的状态,就应该调用对象的方法去修改对应的状态,这才是面向对象的思维。
public void appendFooter(StringBuffer report, String footer);
//可以替换成面向对象的形式
report.appendFooter(footer);
2.7 错误处理就是一件事
函数应该只做一件事,错误处理就是一件事。因此,处理错误的函数不该做其他事。
Try/catch代码丑陋不堪,把错误处理和正常业务流程混为一谈,搞坏了代码结构。最好把try/catch代码块的主体部分抽离出来,另外形成函数。
public void delete(){
try{
deletePageAndAllReferences(page);
}catch(Exception e){
logError(e);
}
}
private void deletePageAndAllReferences(Page page) throws Exception{
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
}
private void logError(Exception e){
logger.log(e.getMessage());
}
2.8 别重复自己(DRY原则)
维基百科:DRY
是软件开发领域的一条原则,目的是减少各种信息的重复,在多层架构中特别有用。DRY 的含义是:在一个系统中,任何知识点都应该拥有唯一、清晰、可信的表示。
对于任何东西,都应该有且只有一个表示,其它的地方都应该引用这一处。这样需要改动的时候,只需调整这一处,所有的地方就都变更过来了。
以下是某网友总结的,我觉得很有道理。
DRY 中的不重复,真正要考虑的是:
- 职责问题:每个模块的职责究竟是什么?确定范畴、界限非常重要。要明确每个功能的职责,并在同一个系统中,保持其唯一性。
- 粒度问题:模块的粒度问题。细粒度还是粗粒度?根据具体场景来做合适的取舍、决策。并非看上去有几条语句相同就要抽象出来。
2.9 梦想中的函数
想象一下:某段程序中每个函数都只有2~20行,每个函数都只说一件事,它们都依次把你带到下一个函数,像剥洋葱一样,一层层地剥开更内层的细节,是不是一目了然?
- 好函数可能是打磨出来的。初稿的函数或许有太多缩进和嵌套循环、有过长的参数列表、有随意的名称、有重复的代码,然后我们应该对它进行反复的打磨,分解函数、修改精准名称、消除重复。
3 迭进
3.1 通过迭进设计达到整洁目的
Kent Beck关于简单设计的几条规则:
- 运行所有测试
- 不可重复
- 表达了程序员的意图
- 尽可能减少类和方法的数量
以上规则按其重要程度排列。
3.2 简单设计规则1: 运行所有测试
全面测试并持续通过所有测试的系统,就是可测试的系统。看似浅显,但却重要。不可测试的系统同样不可验证,不可验证的系统,绝不应部署。
幸运的是,只要系统可测试,就会导向保持类短小且目的单一的设计方案。
测试是代码优化和重构的前提,测试消除了对清理代码就会破坏代码的恐惧。
3.3 简单设计规则2~4:重构
可以应用有关优秀设计的一切知识。提升内聚性,降低耦合度,切分关注面,模块化系统性关注面,缩小函数和类的尺寸,选用更好的名称,如此等等。
3.4 不可重复
重复是拥有良好设计系统的大敌。它代表着额外的工作、额外的风险和额外且不必要的复杂度。
如果你的系统多次出现重复代码,或许意味着你需要抽象。
3.5 表达力
软件项目的主要成本在于长期维护。为了在修改时尽量降低出现缺陷的可能性,很有必要理解系统是做什么的。当系统变得越来越复杂,开发者就需要越来越多的时间来理解它,而且也极有可能误解。所以,代码应该清晰地表达期作者的意图。作者把代码写得越清晰,其他人话在理解代码上的时间也就越少,从而减小缺陷,缩减维护成本。
- 选用好命名来表达。好类名、好函数名、好变量名。
- 保持函数和类尺寸短小来表达。短小的类和函数通常易于命名、易于编写、易于理解。
- 采用标准命名法来表达。设计模式很大程度上就关于沟通和表达,如commond、factory等。
不过,表达力的最重要方式却是尝试。有太多时候,我们写出能工作的代码,就转移到下一个问题上,没有下足功夫调整代码,让后来着易于阅读。记住,下一位读代码的人最有可能是你自己。
3.6 尽可能少的类和方法
我们的目的是在保持函数和类短小的同时,保持整个系统短小精悍。不过要记住,这是优先级最低的一条。所以尽管使类和函数的数量尽量少是很重要的,但更重要的却是测试、消除重复和表达力。
4 味道与启发
让读者闻起来不舒服的“代码味道”,作者把自己受到的一些启发总结出一份清单,供大家参考。
4.1 注释
不恰当的信息、废弃的注释、冗余的注释、糟糕的注释、注释掉的代码,统统删掉。因为代码管理工具都可以追踪到。
4.2 函数
过多的参数、输出参数、标志参数、死函数
4.3 一般性问题
- 重复
- 在错误的抽象层级上的代码。
四. 总结
- 整洁代码是优雅的、干净利落的、直截了当的
- 有意义的命名
2.1 让代码具有表达力,言到意到,意到言到
2.2 名副其实
2.3 避免误导歧义、别用双关词
2.4 做有意义的区分
2.5 使用读得出来的名称、使用可搜索的名称
2.6 添加有意义的语境
2.7 永远不要出现错误的名称 - 函数
3.1 函数的第一规则是要短小,第二规则是还要更短小
3.2 函数应该做一件事,做好这件事,只做这一件事
3.3 每个函数一个抽象层级,自顶向下,逐步求精
3.4 使用描述性的名称,别害怕长名称,别害怕花时间取名字
3.5 尽量写零参数、单参数和双参数函数,应尽量避免定义三参数函数
3.6 标识参数丑陋不堪
3.7 函数应无副作用,副作用是一种谎言,尽量避免使用输出参数
3.8 错误处理就是一件
3.9 别重复自己(DRY原则)
五. 说到最后
码江湖思想流派的多样性
实际上,书中很多建议都是存在争议。或许你并不完全同意这些建议。你可能会强烈反对其中一些建议。这样挺好的。我们不能要求做最终权威。另一方面,书中列出的建议,乃是我们长久苦思、从数十年的从业经验和无数尝试与错误中得来。无论你同意与否,如果你没看到或是不尊重我们的观点,就真该自己害臊咯。
童子军军规
让营地比你来时更干净
如果每次迁入时,代码都比迁出是干净,那么代码就不会腐坏。清理并不一定要花多少功夫,也许只是一个变量名,拆分一个有点过长的函数,清除一点点重复代码,清理一个嵌套if语句。
你想要成为一个代码随时间流逝而越变越好的项目工作吗?你还能相信有其他更专业的做法吗?难道持续改进不是专业性的内在组成部分吗?
最后,我们用一句经典结束此文:
细节之中自有天地,整洁成就卓越代码