坏味道自检:
变量声明之后是否有立即赋值,集合声明之后是否有立即添加元素
坏味道存在的问题:
变量的声明和赋值分离带来的问题就是,把赋值的过程与业务处理混杂在一起。
编程规则:
变量要一次性完成初始化
应对策略:
- 1.在声明前面加上final,用不变性的限制约束代码。
- 2.用声明式的方式进行集合的初始化。
- 传统的集合初始化方式是命令式的,而我们要做的就是用声明式的方式进行集合的初始化,让初始化的过程一次性完成。再进一步,以声明式的标准来看代码,会帮助我们发现许多的坏味道。
坏味道示例:
1. 变量的初始化
EpubStatus status = null;
CreateEpubResponse response = createEpub(request);
if (response.getCode() == 201) {
status = EpubStatus.CREATED;
} else {
status = EpubStatus.TO_CREATE;
}
上面的代码,从语义上说,第一行的变量初始化其实是没有用的,这是一次假的初始化。这段代码里的变量赋值是在声明很久之后才完成的,也就是说,变量初始化没有一次性完成。
这种代码真正的问题就是不清晰,变量初始化与业务处理混在在一起。通常来说,这种代码后面紧接着就是一大堆更复杂的业务处理。很多代码难读,一个重要的原因就是把不同层面的代码混在了一起。
这种代码在实际的代码库中出现的频率非常高,只不过,它会以各种变形的方式呈现出来。有的变量甚至是在相隔很远的地方才做了真正的赋值,完成了初始化,这中间已经夹杂了很多的业务代码在其中,进一步增加了理解的复杂度。所以,我们编程时要有一个基本原则:变量一次性完成初始化。
final CreateEpubResponse response = createEpub(request);
final EpubStatus status = toEpubStatus(response);
private EpubStatus toEpubStatus(final CreateEpubResponse response) {
if (response.getCode() == 201) {
return EpubStatus.CREATED;
}
return EpubStatus.TO_CREATE;
}
在这段改进的代码中,我们提取出了一个函数,将 response 转成对应的内部的 EPUB 状态。
还有一点不知道你注意到了没有,在新的变量声明中,我加上了 final,在 Java 的语义中,一个变量加上了 final,也就意味着这个变量不能再次赋值。尽可能编写不变的代码,尽可能使用不变的量。所以,在能够使用 final 的地方尽量使用 final,限制变量的赋值。
对于 Java 程序员来说,还有一个特殊的场景,就是异常处理的场景,强迫你把变量的声明与初始化分开,就像下面这段代码:
InputStream is = null;
try {
is = new FileInputStream(...);
...
} catch (IOException e) {
...
} finally {
if (is != null) {
is.close();
}
}
如果采用 Java 7 之后的版本,采用 try-with-resource 的写法,代码就可以更简洁了:
try (InputStream is = new FileInputStream(...)) {
...
}
2. 集合的初始化
List<Permission> permissions = new ArrayList<>();
permissions.add(Permission.BOOK_READ);
permissions.add(Permission.BOOK_WRITE);
check.grantTo(Role.AUTHOR, permissions);
这和我们前面所说的变量先声明后赋值,本质上是一回事,都是从一个变量的声明到初始化成一个可用的状态,中间隔了太远的距离。
我们可以使用 Guava(Google 提供的一个 Java 库)进行修改优化:
List<Permission> permissions = ImmutableList.of(
Permission.BOOK_READ,
Permission.BOOK_WRITE
);
check.grantTo(Role.AUTHOR, permissions);
这段代码里的 List 用的是一个 ImmutableList,也就是一个不可变的 List,也就是说,这个 List 一旦创建好了,就是不能修改了,对应的实现就是各种添加、删除之类的方法全部都禁用了。
针对Map同样有类似的用法:
private static Map<Locale, String> CODE_MAPPING = ImmutableMap.of(
LOCALE.ENGLISH, "EN",
LOCALE.CHINESE, "CH"
);
对比我们改造前后的代码,二者之间还有一个更关键的区别:前面的代码是命令式的代码,而后面的代码是声明式的代码。
命令式的代码,就是告诉你“怎么做”的代码,就像改造前的代码,声明一个集合,然后添加一个元素,再添加一个元素。而声明式的代码,是告诉你“做什么”的代码,改造后就是,我要一个包含了这两个元素的集合。
声明式的代码体现的意图,是更高层面的抽象,把意图和实现分开,从某种意义上来说,也是一种分离关注点。所以,用声明式的标准来看代码,是一个发现代码坏味道的重要参考。
网友评论