在开启Kotlin之旅时,我们正负责维护和增强关键业务系统,不能只专注于将Java代码库转换为Kotlin。我们总是不得不在满足新业务需求的同时将代码迁移到Kotlin,并在这样做的同时维护一个混合的Java/Kotlin代码库。我们通过小步修改来控制风险,使每个修改都易于理解,并且在发现它破坏了某些东西时可以低成本地抛弃。在这个过程中,我们首先将Java代码转换为Kotlin,这为我们提供了Kotlin语法的类Java设计。然后,我们逐步应用Kotlin的语言特性,使代码越来越易于理解,类型更安全、更简洁,并且具有更多的组合结构,更容易改变,不会出现令人不快的意外。
细微的、安全的、可逆的改变都会改善设计,以此将地道的Java代码重构为地道的Kotlin代码。
不同语言之间进行重构通常比单一语言中进行重构更难,因为重构工具无法很好地在跨语言边界的情况下工作。将逻辑从一门语言移植到另一门语言必须手动完成,这需要更长的时间并会引入更多的风险。一旦使用了多门语言,语言的边界就会阻碍重构,因为当你重构一门语言的代码时,IDE不会更新那些用其他语言编写的依赖代码以使其兼容。
Java和Kotlin的组合之所以独特,是因为这两种语言之间的(相对)无缝边界。得益于Kotlin语言的设计、映射到JVM平台的方式,以及JetBrains在开发者工具上的投资,可以将Java重构为Kotlin,以及重构一个混合的Java/Kotlin代码库,这几乎与重构一个单语言的代码库一样简单。
我们的经验是,可以在不影响生产力的情况下将Java重构为Kotlin,而且随着我们将更多的代码库转换为Kotlin,生产力也会随之加快。
自Martin Fowler于1999年出版《重构:改善既有代码的设计》(Addison-Wesley)一书以来,重构的实践已经有了长足的发展。这本书在当时还不得不详细地说明重构的手动步骤,即使是简单的重构,如重命名标识符。该书还指出,一些先进的开发环境已经开始提供对重构的自动化支持,以减少这种烦琐的工作。现如今,我们期望我们的工具能够自动处理更加复杂的情况,如提取接口或更改函数签名。
这些单个的重构很少是单独存在的,既然(代码)构建块的重构可以自动执行,我们就有时间和精力将它们组合起来,对代码库进行更大规模的修改。当IDE没有为我们期望进行的大规模改造提供专门的用户界面操作时,我们必须将其拆解为一连串更细化的重构步骤来执行。尽可能地使用IDE的自动重构功能,而当IDE不能自动完成所需要的转换时,我们就会退回到文本编辑。
通过文本编辑进行重构是很乏味的,而且容易出错。为了减少风险和避免无聊的工作,我们尽量减少必须进行的文本编辑的次数。即便必须要做,我们也希望只编辑单个表达式。因此,我们先使用自动重构方法来转换代码,以便只编辑一个表达式,然后使用自动重构方法有序地向着我们所期望的最终状态前进。
在首次描述这种大规模重构时,我们会一步一步地进行说明,并展示每一步的代码变化。这会占用相当多的篇幅,并且你需要花费一些时间来阅读和理解。然而,在实践中,应用这些大型重构是很快的。它们通常只需要几秒,最多也就几分钟。
我们预计随着工具的改进,本书中所发布的重构将会很快过时。个别的重构步骤可能会被重新命名,一些(重构)组合可能会作为单独的重构来实现。在你的环境中进行实验,如果能找到能够逐步安全地改造代码的更好方法,那么请分享出来。
正如Martin Fowler在《重构:改善既有代码的设计》中所说:“如果你想重构,最重要的先决条件是进行可靠的测试。”良好的测试覆盖率确保我们的代码改动只是改进了设计,不会意外地改变系统的行为。在本书中,假设已有良好的测试覆盖率这个前提。本书不涉及如何编写自动化测试。其他作者已经更详细地讨论了这些主题,例如Kent Beck的 Test-Driven Development By Example (Addison-Wesley)、Steve Freeman与Nat Pryce的 Growing Object-Oriented Software Guided By Tests (Addison-Wesley)。尽管如此,我们仍然展示了如何使用Kotlin特性来改进测试。
在一系列的代码转换过程中,我们并未说明什么时候该运行测试,而是假定在每一次展示代码变化之后,都应在编译之后运行测试,无论变化有多么小。
如果你的系统还没有很好的测试覆盖率,则在代码中添加测试可能会很困难(而且很昂贵),因为你要测试的逻辑与系统的其他方面纠缠在一起。你陷入了一个先有鸡还是先有蛋的境地:必须重构代码才能增加测试,以便安全地重构。同样,其他作者已经更详细地讨论了这些话题,例如Michael Feathers的 Working Effectively with Legacy Code 。我们在参考书目中列出了更多关于这些主题的书籍。
就像我们没有明确说明何时运行测试一样,我们也不会明确说明何时提交修改,而是假定只要修改给代码增加了价值就会提交,无论修改是多么细微。
我们知道我们的测试套件并不完美。如果不小心破坏了一些没有被测试覆盖的代码,我们希望能尽快找到引入错误的提交并进行修复。
git bisect命令支持自动搜索,我们可以写一个新的测试来描述错误场景,然后通过git bisect对提交历史进行二分搜索,找出导致该测试失败的第一个提交。
如果历史记录中的提交很多,而且包含一些不相关的改动,那么git bisect就帮不上什么了,它不能告诉我们哪个提交中的源码变化导致了错误。如果在提交中混合了重构和系统行为的改变,则回退不良的重构步骤很可能会破坏系统中的其他行为。
因此,我们会提交一些小而集中的修改,将重构与系统行为的修改分开,以便了解哪些部分发生了变化,并修复错误的变更。出于同样的原因,我们很少积压提交。
我们更喜欢直接将修改提交到主干分支上——“基于主干的开发”——当在多个分支中工作且合并频率不是那么高时,以一连串小而独立的提交来改变代码也同样有益。