什么是整洁代码?
int d; // 消逝的时间,以日记
问题:一个d谁也不知道是啥
改进:变量写明白,注释都不需要,但是这样命名是不是太长了?
int elapsedTimeInDays;
var accountList map[string]int64; // 一组账号
问题:变量名别用List,会让人误以为是个list类型
改进:
var accounts map[string]int64
var accountGroup map[string]int64
提防使用不同之处较小的命名,例如:
XYZControllerForEfficientHandingOfStrings
XYZControllerForEfficientStorageOfStrings
问题,以下三个类虽然名字不同,但是看起来没区别,就像a an the...
Class Product{}
Class ProductInfo{}
Class ProductData{}
以下三个函数看不出区别(但是我好像一直这么用了。。。)
getActiveAccount();
getActiveAccounts();
getActiveAccountInfo();
即避免使用魔术字
const DAYS_FOR_WEEK = 7
const HOURS_FOR_DAY = 24
hoursForWeek = DAYS_FOR_WEEK * HOURS_FOR_DAY
而不是
hoursForWeek = 7 * 24
不要用单字母作为变量名,这样没法搜索。单字母“仅”用于短方法中的本地变量。由此得出一个结论:变量名称长短应与其作用域大小成正比
例如获取,fetch、retrieve、get不要混着用,保持良好的习惯,另外还要做到一词一意,例如add在类中原本用作添加元素,突然在某一个类里面就用成了拼接,很容易混淆
如何做到短小?尽量保证缩进层级不多于1或2层;if,else,while其中的代码块应该只有一行,一般是一个函数调用。
函数应该只做一件事。做好这件事。只做这一件事。
什么叫只做一件事?
如果函数只是做了该函数名下同一抽象层上的步骤,则函数是只做了一件事。编写函数的目的本身也是为了把大一些的概念拆分为另一抽象层上的一系列步骤。
要判断一个函数是否不只做了一件事,可以看是否能再拆出一个函数,该函数不仅只是单纯重新诠释其实现。
只做一件事的函数无法被合理的切分为多个区段。
自顶向下读代码:向下规则
我们想要这样读程序:程序像是一系列函数起头的段落,每一段都描述当前抽象层级,并引用位于下一抽象层级的后续函数段落。
switch天生就是做N件事的。但要做到确保每个switch语句都埋藏在较低的抽象层级,且避免重复。
函数越短小,功能越集中,越便于取个好名字。
入参
函数参数最好是0个,其次1个,其次2个,尽量避免3个或以上。
理由是,参数一般与函数位于不同的抽象层级,这使得阅读函数的时候还要花经历关注其他的事情。另外从测试角度讲,参数太多,测试覆盖所有的可能组合值也比较复杂。
出参
阅读函数时,我们惯于认为信息通过参数输入函数,通过返回值从函数输出。我们不太期望信息通过参数输出,这往往让人难以理解。
标识参数
标识参数丑陋不堪。向函数传入bool值,相当于大声宣布该函数不只做一件事,true会怎样怎样,false会怎样怎样。例:
错误:
render(Boolean isSuite)
正确:
renderForSuite()
renderForSingle()
多参数
当函数是二元或者三元时,一定想清楚这个函数是否需要这么多参数,是否可以使用一些手段来减少函数参数。例:
writeField(outputStream, name)
可以考虑修改为:
1.把writeField方法写成outputStream的成员,从而:
outputStream.writeField(name)
2.把outputStream写成当前类的成员变量,从而无需再传递他。
参数对象
如果函数考虑过后需要2,3,3个以上参数时,说明其中一些参数应该考虑封装成类了。例:
Circle makeCircle(double x, double y, double radius)
Circle makeCircle(Point center, double radius)
动词与关键字
给函数取个好名字,能较好的解释函数的意图,以及参数的顺序和意图。对于一元函数,函数和参数应该形成一种非常良好的动词/名词对形式。
例如write(name),是可以广泛接受的,更好些可以修改为writeField(name),这告诉我们name是一个“field”。
再例如,assertEqual(expected,actual)可以修改为assertExpectedEqualsActual(expected,actual),这样可以避免搞混参数顺序。
所谓函数副作用,个人理解就是函数“偷偷”地做了一些具有“破坏性”的工作。破坏性值得就是,例如,修改入参(即输出参数),修改全局变量等,本身包含这些动作的函数就需要时序性耦合或顺序依赖,如果没有显式地声明函数会作出这种操作,那么调用方极高概率会错误使用函数。
那么如何避免输出参数行为呢?其实this就有输出函数的意味在里面。例如:
appendFooter(s)
这个函数是把s添加到什么东西后面,还是把什么东西添加到s的后面?s是输入参数还是输出参数?或许需要看下函数签名才能搞清楚:
public void appendFooter(StringBuffer report)
如果使用this的特性呢?如下:
report.appendFooter();
函数要么做什么事(指令),要么回答什么事(询问),二者不可兼得。具体说,函数应该修改某对象的状态,或者返回该对象的有关信息,两件事情都做通常会导致混乱。例:
public boolean set(string attribute, string value);
那么就会有以下的使用方法:
if (set("username","unclebob"))...
从读者角度出发,这是在问username是否之前已设为unclebob,还是问username属性值是否成功设置为unclebob呢?为防止混淆的发生,可以使用如下写法:
if(attributeExists("username")) {
setAttribute("username","unclebob");
...
}
从指令式函数返回错误码轻微违反了指令与询问分离的规则。它鼓励在if判断语句中把指令当作表达式使用。例:
if (deletePage(page) == E_OK)
这不会引起动词/形容词混淆,但却导致更深层次的嵌套结构。当返回错误码时,就是在要求调用者立刻处理错误。例:
if (deletePage(page) == E_OK){
if (registry.deletePreference(page.name) == E_OK){
if (configKeys.deleteKey(page.name.makeKey()) == E_OK){
logger.log("page deleted");
}else{
logger.log("configKey not deleted");
}
}else{
lgger.log("deletePreference from registry failed");
}
}else{
logger.log("delete failed");
return E_ERROR;
}
使用异常而不是返回码优化下:
try{
deletePage(page);
registry.deleteReference(page.name);
configKey.deleteKey(page.name.makeKey());
}
catch (Exception e){
logger.log(e.getMessage());
}
抽离try/catch代码块
Try/catch代码块如果把错误处理和正常流程混为一谈,将丑陋不堪。最好把try/catch代码块的主体部分抽离出来,另外写成函数。将以上代码进一步优化:
public void delete (Page page){
try{
deletePageAndAllReferences(page);
}
catch(Exception e) {
logError(e)
}
}
private void deletePageAndAllReferences(Page page) throws Exception{
deletePage(page);
registry.deleteReference(page.name);
configKey.deleteKey(page.name.makeKey());
}
private void logError(Exception e){
logger.log(e.getMessage());
}
Error.java依赖磁铁
返回错误码通常意味着某处有个类或者是枚举,定义了所有的错误码,例如:
public enum Error{
OK,
INVALID,
NO_SUCH,
LOCKED,
OUT_OF_RESOURCES,
WAITING_FOR_EVENT;
}
这样的类就是一个依赖磁铁(dependency magnet),其他许多类都需要导入和使用它,当Error枚举修改时,其他的类都需要重新编译和部署。使用异常代替错误码,新的异常可以从异常类派生出来,无需重新编译和部署。
重复可能是软件中一切邪恶的根源!
Edsger Dijkstra的结构化编程规则认为,每个函数、函数中的每个代码块都应该有一个入口、一个出口。如果遵循这个规则意味着每个函数只能有一个return,循环中不能有break或者continue,而且任何地方不能使用goto。这个规范的目标是好的,但是对于小函数,这些规则助益不大,只要保持函数短小,偶尔出现的return break continue语句并没有坏处,甚至比单入单出更具有表达力;另一方面,goto只在大函数中才有道理,所以应该尽量避免使用。
写函数就像写论文,并不是一开始全部构思清楚,可以先想什么就写什么,然后慢慢打磨。具体可以先按思路写好函数,然后配上完整的UT覆盖到所以代码,最后在慢慢调整和优化函数同时保证UT可以通过。