我之所以称呼这个坏味道为落后的代码,主要是看其是否跟随语言的发展和趋势,是否使用了语言的新特性。通常来说,新的语言特性都是为了提高代码的表达性,减少犯错误的几率。所以,在实践中鼓励大家采用新的语言特性写代码的。
新特性举例:
Java 8 引入的 Optional 和函数式编程
核心要点:
Optional是一个对象容器,它的出现是为了规避空对象带来的各种问题。Optional 的引入可以减少由于程序员的忽略而引发对空对象的问题。团队内部可以约定,所有可能返回空对象的地方,都要返回 Optional,以此降低犯错的几率。
函数式编程是一个影响代码整体风格的重要编程范式。懂得列表转换思维,首先要懂得最基本的几个操作:map、filter 和 reduce,然后,就可以把大部分的集合操作转换成列表转换。想要使用这种思维写好代码,一方面,要懂得声明式代码的重要性,另一方面,要懂得写出短小的函数,不要在 lambda 中写过多的代码。
记住一句话:
作为一个精进的程序员,我们要不断地学习“新”的代码风格,改善自己的代码质量,不要故步自封,让自己停留在上一个时代。
坏味道示例:
1. Optional
我们先来看一段代码:
String name = book.getAuthor().getName();
因为它没有考虑对象可能为 null 的场景。所以,这段代码更严谨的写法是这样:
Author author = book.getAuthor();
String name = (author == null) ? null : author.getName();
然而,在很多真实的项目中,这种严格的写法却是稀有的,所以,在实际的运行过程中,我们总会惊喜地发现各种空指针异常。对于这个如此常见的问题,Java 8 中已经给出了一个解决方案,它就是 Optional。Optional 提供了一个对象容器,你需要从中“取出(get)”你所需要的对象,但在取出之前,你需要判断一下这个对象容器中是否真的存在一个对象。
class Book {
public Optional getAuthor() {
return Optioanl.ofNullable(this.author);
}
...
}
Optional author = book.getAuthor();
String name = author.isPresent() ? author.get().getName() : null;
这种做法和之前做法的最大差别在于,你不会忘掉判断对象是否存在的过程,因为你需要从 Optional 这个对象容器中取出存在里面的对象。正是这多出来的一步,减少了“忘了”的概率。
也是因为多了 Optional 这个类,这段代码其实还有更简洁的写法:
Optional author = book.getAuthor();
String name = author.map(Author::getName).orElse(null);
有了 Optional,我们可以在项目中做一个约定,所有可能为 null 的返回值,都要返回 Optional,以此减少犯错的几率。
2. 函数式编程
循环语句本身就是一个坏味道:
public ChapterParameters toParameters(final List chapters) {
List parameters = new ArrayList<>();
for (Chapter chapter : chapters) {
if (chapter.isApproved()) {
parameters.add(toChapterParameter(chapter));
}
}
return new ChapterParameters(parameters);
}
如果按照 Java 8 之前的版本理解,这段代码是一段很正常的代码。当 Java 的时代进入到 8 之后,这段代码就成了有坏味道的代码。Martin Fowler 在《重构》的第二版中新增的坏味道就包括了循环语句(Loops)。之所以循环语句成了坏味道,一个重要的原因就是函数式编程的兴起。不是我们不需要遍历集合,而是我们有了更好的遍历集合的方式。
一般来说,采用列表转换写出来的代码相较于传统的循环语句写出来的代码,表达性更好,因为它们都是描述做什么,而传统的循环语句是在描述怎么做。这是两种不同的抽象层次,描述做什么比怎么做的代码,在表达性上要好得多。
这段代码可以改写成这样:
public ChapterParameters toParameters(final List chapters) {
List parameters = chapters.stream()
.filter(Chapter::isApproved)
.map(this::toChapterParameter)
.collect(Collectors.toList());
return new ChapterParameters(parameters);
}
或许有人会说,这段代码看着还不如我原来的循环语句简单。不过,你要知道,两种写法根本的差别是侧重点不同,循环语句是在描述实现细节,而列表转换的写法是在描述做什么,二者的抽象层次不同。
对于理解这段代码的人来说,二者提供的信息量是完全不同的,循环语句必须要做一次“阅读理解”知晓了其中的细节才能把整个场景拼出来,而列表转换的写法则基本上和我们用语言叙述的过程一一对应。所以,理解的难度是完全不同的。
这段代码只是为了说明问题,而选择了简单的代码,但在实际工作中,需求会比这复杂得多。而且,如果要添加新的需求,循环语句里的代码会随之变得越来越复杂,原因就是循环语句里都是细节,而列表转换则是一段一段的描述,就像在阅读一篇文章。
lambda 本身相当于一个匿名函数,所以,很多人在写函数中犯的错误在 lambda 里也一样出现了,最典型的当然就是长函数。
在各种程序设计语言中,lambda 都是为了写短小代码提供的便利,所以,lambda 中写出大片的代码,根本就是违反 lambda 设计初衷的。最好的 lambda 应该只有一行代码。那如果一个转换过程中有很多操作怎么办呢?很简单,提取出一个函数。
一种编程风格会过时,本质上是因为它存在问题,新代码风格就是用更好的方案解决它,所以,我们要不断学习新引入的语言特性,了解它们给语言带来的“新”风格,而不要停留在原地。