《代码整洁之道》- 代码标准

每次看之前写的代码,都想删掉重新写%>_<%

引言

代码越写越多,从一开始的能跑通就行,到现在对自己的要求越来越高,不仅要留下好的注释,对于函数和变量的命名也要合乎其名,慢慢融入学到的设计模式,还有优化逻辑来提高性能。

所以看完这本《代码整洁之道》,发现获益良多,将书中最后一章的启示贴一下,用来提醒自己以后编码的要求~


启示(约定规则)


一个源文件中减少存在多种语言

这个问题应该是存在于以前的JSP开发,现在一般都是前后端分离,后端代码中,基本没有前端代码的存在了


要实现函数名对应的行为

遵循“最小惊讶原则”(The Principle of Least Surprise),函数或类应该实现有理由期待的行为。


边界行为要明确

追索每种边界条件,并编写测试。要求所有代码都有单元测试,这个条件比较严格,主要要注意的应该是在生产代码中,注意业务边界,避免NPE,数组越界等错误。


不要忽视安全

这里提到安全不仅仅是边界和单元测试,还有无限循环和线程安全等意识。


避免重复代码

这个就是常说的胶水代码,这里copy一份到另一个地方。每次看到重复代码,都代表遗漏了抽象

可以将重复的代码抽象到上一层,或者封装成公共方法,很多地方使用到的,可以当成一个工具类的静态方法等等。

学习到的设计模式,责任链、模板、抽象工厂等等,都可以用来消除重复代码~


编码要在正确的抽象层级上

创建抽象类来容纳较高层级概念,创建派生类来容纳较低层次概念。

同时要注意,较低层级和较高层级概念不应该混杂在一起,分离要完整。


基类不应该依赖于派生类

将概念分解到基类和派生类的最普遍的原因是较高层级概念可以不依赖于低层级派生类概念。(也就是说,基类应该对派生类一无所知,不提到派生类的名称)。


避免信息过多

设计良好的模块,让接口保持简单。

要学会限制类或模板中暴露的接口数量,减少类中的方法,减少函数的变量数目。(当然如果业务复杂时,简单变量可能越加越多,这个时候就该建立一个新的参数类来封装参数)。

尽量保持接口紧凑,通过限制信息来控制耦合度。


删除死代码

有些方法可能在代码修改后就不会再调用了,这个时候路径就不会到达到,现在IDE很方便能看到方法是否被调用到。看到这些永远不会运行的代码,可以考虑将其删除~


垂直分隔

变量和函数应该靠近被使用的地方定义。

本地变量应该正好在其首次被使用的位置是哪个命名,垂直距离要短。

私有函数应该刚好在其被使用的位置下面定义。


保持前后一致

从一而终。小心选择约定,一旦选中,就小心持续遵循。

例如参数命名,如果选中了HttpServletRequest request,那么之后的参数命名都按request来统一,让代码更加易于阅读和修改。


减少人为耦合

不互相依赖的东西不该耦合。

不要将两个没有直接目的之间的模块耦合,例如将变量、常量或函数放在不恰当的位置,普通的enum不应在特殊类中。


减少特性依赖

类的方法只应对其所属类中的变量和函数感兴趣,不该垂青其他类中的变量和函数。

当方法通过其他对象的访问器和修改器来操作该对象内部数据,则它就依恋于该对象所属类的范围。(当然在业务开发中,经常会遇到条件判断,然后修改对象的属性值,我觉得这这种场景是正常,但是下面说的例子就要避免出现了)

Bad Example:在B类方法中,基本依赖A类的属性

1
2
3
4
5
6
7
8
9
10
11
public class HourlyPayCalculator {
public Money calculateWeeklyPay(HourlyEmployee e) {
int tenthRate = e.getTenthRate().getPennins();
int tenthsWorked = e.getTenthsWorkeds()
int straightTime = Math.min(400, tenthsWorked);
int overTime = Math.max(0, tenthsWorked - straightTime);
int straightPay = straightTime * tenthRate;
int overtimePay = (int) Math.round(overTime * tenthRate * 1.5)
return new Money(straightPay + overtimePay);
}
}

可以看到,上面的HourlyPayCalculator类的计算工资方法,侵入到是钟点工类e的作用范围。

现在看起来,这个方法更应该下沉到HourlyEmployee类中,让它对自己类中的属性进行操作(虽然之前这样写没感觉到什么不妥,但之后会改~)


避免在函数末尾出现选择算子函数(selector)

个人理解上,应该是将复杂的选择算子函数拆分成多个子函数,然后通过组合调用,让函数的意图更加清晰。


避免晦涩的意图

代码要尽可能具有表达力。

以前写Android代码时,看到匈牙利语标记语法和联排表示式,例如m_/f_等前缀,现在想想这些前缀有些多余,同时也要减少魔术数,毕竟写个数字5,在没有注释的情况下,除了开发者,估计其他人也无法理解这个5的实际含义~


使用解释性变量

一个例子你就能懂:

1
2
3
4
5
6
Matcher match = headerPattern.matcher(line);
if (match.find()) {
String key = match.group(1);
String value = match.group(2);
headers.put(key.toLowerCase(), value);
}

是不是感觉变量会说话,key和value让人一看就了解它们的含义,这种情况下都不需要加注释了~


函数名称应该表达其行为

以下这种应该是杜绝的:

1
Date newDate = date.add(5);

函数名add(int),无法表达它的含义,是天数还是月份增加5,所以这些表意不明的函数名是要杜绝的。


理解算法

这里的算法,应该是确认自己理解了这个函数是怎样工作,在此基础上,进行修改或者重构。


用多态替代If/Else或Switch/Case

觉得这条建议应该是在业务复杂的情况下或者需要扩展代码逻辑情况下实施的,在简单业务情况下,为了避免类膨胀的话,是可以使用If/Else~(给自己偷懒找借口(^o^)/~)


用命名常量替代魔术数

避免在代码中硬编码数字,可以将常量数字写在类最上面的静态变量,或者抽象一个枚举,阐释这个数字的含义。


封装条件

在if表达式中,将判断式封装起来(此方法会在条件简单偷懒=-=):

例如:
if (shouldBeDeleted(timer))
要好于
if (timer.hasExpired() && !timer.isRecurrent())


避免否定性条件

例如:
if (buffer.shouldCompact())
要好于
if (!buffer.shouldNotCompact())


函数只该做一件事

函数只做一件事有点难,可以将一个函数拆解出几个小函数,例如付工资函数,可以从一个大方法拆分成三个小方法,获取人员、计算工资、打钱到银行账户,这样这些小方法可以被其它方法复用。


掩蔽时序耦合

常常有必要使用时序耦合,上一个步骤的结果交给下一个步骤处理,这个时候就不应该掩蔽它们之间的时序性。

例如:

1
2
3
4
5
6
7
8
9
10
11
public class MovieDiver {
Gradient gradient;
List<Spline> splines;

public void dive(String reason) {
saturateGradient();
reticulateSplines();
diveForMoog(reason);
}
···
}

dive()方法的代码没有强调时序耦合,如果其他开发人员调换了执行顺序,这样dive方法就无法正常执行了。

可以更新成下面的形式:

1
2
3
4
5
public void dive(String reason) {
Gradient gradient = saturateGradient();
List<Spline> splines = reticulateSplines(gradient);
diveForMoog(reason);
}

这样通过创建顺序队列暴露时序耦合。


封装边界条件

这个规则挺像封装判断条件,不过这条规则同样适用于封装变量:
例如,将出现过两次的level + 1封装成一个变量

1
2
3
4
5
int nextLevel = level + 1;
if (nextLevel < tags.length) {
parts = new Parse(body, tags, nextLevel, offset + endTag);
body = null;
}


在较高层级放置可配置数据

可以理解为,变量定义在类起始的位置,不要将共有变量定义在函数后面。


函数名称要说明函数副作用

例如:

1
2
3
4
5
6
public ObjectOutputStream getOos() throws IOException {
if (oss == null) {
oos = new ObjectOutputStream(socket.getOutputStream());
}
return oos;
}

这个方法名只说了获取输出流,但没说明如果输出流不存在的情况下,它将新建一个输出流,所以这个函数名称应该修改成createOrReturnOos


避免传递游览

例如有A、B、C三个类,下面这种行为是要禁止的:

1
a.getB().getC().doSomething();

正确的做法应该是:

1
myCollaborator.doSomething();

让我们的直接协作者提供所需的全部服务~~


参考书籍

  1. 《代码整洁之道》-第十七章