购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

4.2 从Optional重构为nullable

Travelator中的旅行被分为多个Leg,其中每个Leg都是一段完整的旅程。这是我们在代码中找到的一个实用函数:

测试用于验证代码是否按预期工作,并让我们一目了然地看清它的行为:

让我们看看Kotlin可以做些什么来让它变得更好,将Legs.java转换为Kotlin后,我们得到如下代码(稍作格式化):

方法的参数如我们所料,在Kotlin中,List<Leg>可以透明地接收java.util.List(我们将在第6章中详细介绍Java和Kotlin的集合)。值得一提的是,当Kotlin函数声明一个参数(此处为legs和duration)不可为空时,编译器会在函数体之前插入一个空检查。这样,如果Java调用方混入一个空值,我们就会立即知道。由于这些防御性检查,Kotlin能够尽可能接近其源头检测到意外的空值,这与Java不同,Java中的引用可以在距离它最终拋出异常的时空很远的地方设置为空值。

回到这个例子,Kotlin中的for循环与Java中的非常相似,除了使用in关键字而不是:,并且同样适用于所有实现了Iterable的类型。

迭代器和for循环

实际上,在Kotlin的for循环中,除了Iterable之外,我们还可以使用其他类型。编译器允许for与满足以下条件的任何东西一起使用:

● 扩展了Iterator。

● 有一个返回迭代器的iterator()方法。

● 有一个当前范围内的扩展函数,operator fun T.iterator()返回一个迭代器。

不幸的是,最后一点并没有让其他的类可迭代。它只是使for循环可以工作。这一点令人遗憾,因为如果我们能够前瞻性地使类型支持迭代,就可以对它们应用map、reduce等操作,因为这些操作被定义为Iterable<T>上的扩展函数。

转换后的findLongestLegOver代码并不是地道的Kotlin(可以说自从引入了Stream之后,它也不是非常地道的Java)。我们应该寻找更能表达意图的方法而不是for循环,但现在我们暂时搁置这个问题,因为我们的主要任务是从Optional迁移到nullable。我们将通过一一转换我们的测试(类)来进行阐述,这样我们将会得到一个混合体,就像当时正在迁移的代码库一样。要在调用端中使用可空性,它们首先必须是Kotlin,所以让我们先将测试用例转换为Kotlin:

现在要逐步迁移,我们需要有两个版本的findLongestLegOver函数:当前版本返回Optional<Leg>,另一个新版本则返回Leg?。我们可以通过提取当前实现的核心部分来做到这一点,如下所示:

除了return语句之外,我们对findLongestLegOver的其他内容进行函数提取(IDE提供的重构功能)。由于不能用相同的方法名来命名新的方法,因此我们用longestLegOver这个名字。将其设为public,因为这是我们的新接口:

上述重构在findLongestLegOver中留下了一个残余的结果变量。我们可以选中它并进行内联(I D E提供的重构功能),得到以下代码:

现在我们有了两个版本的接口,其中一个版本是基于另一个版本定义的。现在可以让我们的Java调用端来使用findLongestLegOver返回的Optional,并让Kotlin调用端调用返回可空值的longestLegOver。我们用测试来展示转换过程。

先从缺失返回值的场景开始。它们目前调用assertEquals(Optional.empty<Any>(), findLongestLegOver...):

所以将它们改为assertNull(longestLegOver(...):

请注意,我们已经将测试方法的名称更改为使用“反引号引用标识符”。如果我们在带有下划线的测试方法名上按Alt-Enter键,IntelliJ将为我们执行此操作。现在,对于不返回空的方法调用如下:

Kotlin中等效于Optional.orElseThrow(Java 10之前又名get())的是!!运算符。无论是Java的orElseThrow还是Kotlin的!!都会返回值,如果没有值则抛出异常。Kotlin逻辑上抛出NullPointerException,Java同样逻辑上抛出NoSuchElementExecption。它们只是以不同的方式处理缺失!如果我们不依赖于异常的类型,则可以将findLonges-tLegOver(...).orElseThrow()替换为longestLegOver(...)!!:

我们已经用!!运算符对第一个非空返回测试(is longest leg when one match)进行了转换。如果它失败了(虽然它并没有失败,但我们喜欢为这些事情做好计划),测试会失败并抛出NullPointerException,而不是得到测试通过的结论。对于第二种情况,我们用安全调用操作符?.解决了这个问题,它仅在其接收器不为空时才继续求值。这意味着当leg为空时,将展示如下错误(读起来要好得多):

测试代码是我们为数不多使用!!的场景之一。实际上,即便在这种场景下,通常也会有更好的替代方案。

我们可以通过对调用端进行上述重构,将它们转换为Kotlin,然后再使用longestLegOver方法。一旦完成了所有转换,我们就可以删掉返回Optional的findLongestLegOver方法了。

扩展和收缩重构

在本书中,我们将使用这种技术(也称为并行更改(h ttps://oreil.ly/jxSPE ))来管理对接口的更改。这是一个简单的概念:添加新接口,将旧接口的使用迁移到新接口,当旧接口不再被使用后,将其删除。

在本书中,我们经常将重构与转换为Kotlin结合起来。通常,如本章所述,我们会将接口的定义和实现转换为Kotlin,然后将新接口添加到其中。当我们将调用端转换为使用新接口时,也趁机将其转换为Kotlin。

尽管接口的迁移和语言的转换都是这个过程的一部分,但我们尽量避免同时进行这两项工作。就像登山者与岩石保持三点接触一样,不要同时放开双手!迈出一步,确保测试通过,然后再继续下一步。如果感觉变更有风险,那么现在可能是添加一些保护措施的好时机(运行提交前的测试套件,签入,甚至部署金丝雀版本),这样即便出现问题,我们也不会陷入太深。

然后完成工作,重构以使代码更好,这几乎总是意味着更简单,而“更简单”很少与“更大”相关联。我们允许代码在变得更好(每个人都使用新接口)之前变得更糟(用两种方式做同样的事情),但避免陷入不得不维护两个版本的困境。如果最后的结果是在较长时间内同时支持一个接口的两个版本,这两个版本可能会出现分化,或者需要对两个版本都进行测试以确保它们不会分化,并且旧版本可能会被新的调用端调用,我们可以将代码标记为已弃用,但最好先放一放,继续完成工作。也就是说,允许在一小段间隙内支持遗留版本。我们喜欢Kotlin,但也希望将时间花在增加价值上,而不是转换那些不需要在意的Java代码。 VKEyhz1b+jULpdzH8+eRDu3YBGHc5fhy+A8SPBrB+BaCcdaV1wqzmlh9lqs9FMCO

点击中间区域
呼出菜单
上一章
目录
下一章
×