现在,这个例子中的所有代码都是Kotlin代码了,我们也了解了如何从Optional迁移到nullable。我们可以止步于此,但是为了与之前所说的额外重构策略相一致,我们将继续努力,看看这段代码还能教给我们什么。
以下是Legs的当前版本:
这些函数被包含在一个对象中,因为我们的Java方法是静态的,因此在转换为Kotlin时需要将它们放在某个地方。正如我们将在第8章中了解到的,Kotlin不需要这个额外的命名空间层级,因此我们可以在longestLegOver上执行移动到顶层的操作。在撰写本书时,这个功能还不是很好用,因为IntelliJ未能将isLongerThan与其调用函数一起移动到顶层,而是将其留在Legs中。不过,这个问题很容易解决,在现有代码中留下longestLegOver顶层函数并解决好引用的问题:
你可能已经注意到isLongerThan上没有大括号和return语句了。我们将在第9章中讨论单表达式函数的优缺点。
你会发现这里的isLongerThan(leg, ...)有点奇怪,在英语上它读起来不太正确。毫无疑问,我们对扩展函数的迷恋会让你感到厌倦(准确地说是在第10章结束前),让我们在leg参数上按下Alt-Enter键并选择“Convert parameter to receiver”,这样就可以写成leg.isLongerThan(...):
到目前为止,我们所有的重构都是结构性的,改变了代码定义以及调用方式。结构性重构本质上是相当安全的(大多数情况下,而不是完全安全)。对依赖于多态(通过方法或函数)或反射的代码来说,结构性重构可能会改变其行为,但除此之外,只要代码能够继续编译,应用程序应该就能正常运行。
现在我们将注意力转向longestLegOver中的算法。重构算法更为危险,尤其是像这种依赖修改状态的算法,这是因为工具对这类转换的支持不怎么好。虽然我们有很好的测试,但仅靠阅读代码很难弄清楚算法做了什么,看看我们还能做些什么。
IntelliJ给出的建议是只需将compareTo替换为>,我们先采纳IntelliJ的建议。Duncan已经在这一点上耗尽了他的重构天分(如果我们在结对,也许你会有一些建议?),并决定从头开始重写函数。
为了重新实现该功能,我们问自己:“代码试图做什么?”函数的名字很有帮助,我们的答案就在其中:longestLegOver。为了实现这个计算逻辑,我们可以先找到最长的Leg,如果它比Duration长,则返回它,否则返回null。当我们在函数的开头打出leg.后,我们在IDE的智能提示建议中找到了maxByOrNull。最长的Leg就是legs.maxByOrNull (Leg::plannedDuration),这个API有助于返回Leg?,并通过在函数名中包含orNull来提醒我们,如果legs为空,它就无法返回结果。将“找到最长的Leg,如果它比Duration长,则返回它,否则返回null”的算法直接转换为代码,得到:
虽然它通过了测试,但这么多的return语句显得不美观。IntelliJ可以帮助我们把return从if语句中提取出来:
目前,Kotlin对可空性的支持使你可以用多种方式来重构它,这取决于你的喜好。
我们可以使用Elvis运算符?:,它先对其左侧求值,除非左侧的值为null,才会对右侧进行求值。如果没有找到最长的Leg,则可以提前返回:
我们还可以用单表达式?.let,如果其入参为null,其值则为null;否则,它会将最长的Leg通过管道传入let块中:
所以在let块中,longestLeg不会为空。一切言简意赅,单个表达式令人赏心悦目,但是一眼看上去可能不太好理解,用when更容易表达清楚:
为了进一步简化,我们需要用到一项Duncan(他正在写这篇文章)迄今为止还未能领悟的技巧:takeIf。如果断言为真,它将返回其接收者;否则返回null。这恰好是之前的let块的逻辑,所以我们可以这样写:
根据我们团队对Kotlin的经验,takeIf这种表达方式可能过于隐晦。Nat认为这没什么,但我们倾向于更明确的表达,所以保留了使用when表达式的版本,至少到下一次重构前。
最后,让我们将legs参数转换为扩展函数的接收器,这使得我们可以将函数重命名为不那么令人疑惑的名字:
在结束本章之前,让我们花一点时间来对比一下当前的版本和原始版本,旧的版本有什么优势吗?
通常我们会说“视情况而定”,但是对于当下的场景,我们认为新版本几乎在各个方面都更好。代码更短并且简单;更容易理解它的工作原理;在大多数情况下,它会减少对getPlannedDuration()的调用,而该方法是一个代价相对高昂的操作。如果在Java中采用相同的方法会得到什么呢?直接翻译的代码如下:
事实上还不错,但是与Kotlin的版本相比,可以看到Optional几乎在方法的每一行中都添加了噪声。因此,使用Optional.filter的版本可能更可取,即便它与Kotlin的takeIf一样,存在难以理解问题。也就是说,Duncan无法在不运行测试之前判断它是否有效,但Nat更偏爱这种方式。