在大型团队中采用 Kotlin

迁移到任何新的语言都可能是一项艰巨的任务。成功的秘诀是循序渐进,分批迁移,并经常测试以确保您的团队取得成功。Kotlin 使迁移变得容易,因为它编译为 JVM 字节码,并且与 Java 完全互操作。

组建团队

迁移之前的第一步是为您的团队建立一个共同的基础理解。以下是一些可能对您加速团队学习有用的提示。

组建学习小组

学习小组是促进学习和记忆的有效方法。研究表明,在小组环境中背诵您学到的内容有助于强化材料。为小组的每个成员获取一本Kotlin 书籍或其他学习资料,并要求小组每周学习几章。在每次会议期间,小组应比较他们学到的内容并讨论任何问题或观察结果。

建立教学文化

虽然不是每个人都认为自己是一位老师,但每个人都可以教学。从技术或团队负责人到个人贡献者,每个人都可以鼓励学习环境,从而帮助确保成功。促进这种环境的一种方法是定期举办演示,在演示中,团队中的一人被指定谈论他们学到的或想分享的内容。您可以利用您的学习小组,要求志愿者每周介绍新的一章,直到您的团队对这门语言感到满意为止。

指定一名负责人

最后,指定一名负责人来领导学习工作。当您开始采用过程时,此人可以充当主题专家 (SME)。重要的是在所有与 Kotlin 相关的实践会议中都包括此人。理想情况下,此人已经对 Kotlin 感兴趣,并且具有一定的工作经验。

逐步集成

循序渐进,并从战略角度思考首先迁移哪些部分,这是关键。通常最好将其隔离到组织中的单个应用程序中,而不是旗舰应用程序。在迁移所选应用程序方面,每个情况都不同,但以下是一些常见的起点。

数据模型

您的数据模型可能包含许多状态信息以及一些方法。数据模型还可能具有诸如toString()equals()hashcode()之类的通用方法。这些方法通常可以轻松地进行转换并在隔离状态下进行单元测试。

例如,假设以下 Java 代码片段

public class Person {

   private String firstName;
   private String lastName;
   // ...

   public String getFirstName() {
       return firstName;
   }

   public void setFirstName(String firstName) {
       this.firstName = firstName;
   }

   public String getLastName() {
       return lastName;
   }

   public void setLastName(String lastName) {
       this.lastName = lastName;
   }

   @Override
   public boolean equals(Object o) {
       if (this == o) return true;
       if (o == null || getClass() != o.getClass()) return false;
       Person person = (Person) o;
       return Objects.equals(firstName, person.firstName) &&
               Objects.equals(lastName, person.lastName);
   }

   @Override
   public int hashCode() {
       return Objects.hash(firstName, lastName);
   }

   @Override
   public String toString() {
       return "Person{" +
               "firstName='" + firstName + '\'' +
               ", lastName='" + lastName + '\'' +
               '}';
   }
}

您可以用一行 Kotlin 代码替换 Java 类,如下所示

data class Person(var firstName: String?, var lastName : String?)

然后,可以针对当前测试套件对该代码进行单元测试。此处的想法是从一个模型开始,一次迁移一个模型,并迁移主要包含状态而不是行为的类。务必沿途进行测试。

迁移测试

另一个要考虑的起点是将现有测试转换为 Kotlin 并开始使用 Kotlin 编写新测试。这可以让您的团队在编写要与应用程序一起发布的代码之前,有时间对这门语言感到满意。

将实用程序方法移到扩展函数中

任何静态实用程序类(StringUtilsIntegerUtilsDateUtilsYourCustomTypeUtils 等)都可以表示为Kotlin 扩展函数,并由您现有的 Java 代码库使用。

例如,假设您有一个包含一些方法的StringUtils

package com.java.project;

public class StringUtils {

   public static String foo(String receiver) {
       return receiver...;  // Transform the receiver in some way
   }

   public static String bar(String receiver) {
       return receiver...;  // Transform the receiver in some way
   }

}

然后,这些方法可能在应用程序中的其他地方使用,如以下示例所示

...

String myString = ...
String fooString = StringUtils.foo(myString);

...

使用 Kotlin 扩展函数,您可以为 Java 调用者提供相同的Utils 接口,同时为不断增长的 Kotlin 代码库提供更简洁的 API。

为此,您可以首先使用 IDE 提供的自动转换将该Utils 类转换为 Kotlin。示例输出可能类似于以下内容

package com.java.project

object StringUtils {

   fun foo(receiver: String): String {
       return receiver...;  // Transform the receiver in some way
   }

   fun bar(receiver: String): String {
       return receiver...;  // Transform the receiver in some way
   }

}

接下来,删除类或对象定义,在每个函数名前加上该函数应应用于的类型,并在函数内部使用此类型作为引用,如以下示例所示

package com.java.project

fun String.foo(): String {
    return this...;  // Transform the receiver in some way
}

fun String.bar(): String {
    return this...;  // Transform the receiver in some way
}

最后,在源文件顶部添加一个JvmName 注释,使编译后的名称与应用程序的其余部分兼容,如以下示例所示

@file:JvmName("StringUtils")
package com.java.project
...

最终版本应类似于以下内容

@file:JvmName("StringUtils")
package com.java.project

fun String.foo(): String {
    return this...;  // Transform `this` string in some way
}

fun String.bar(): String {
    return this...;  // Transform `this` string in some way
}

请注意,现在可以使用 Java 或 Kotlin 按照与每种语言相匹配的约定调用这些函数。

Kotlin

...
val myString: String = ...
val fooString = myString.foo()
...

Java

...
String myString = ...
String fooString = StringUtils.foo(myString);
...

完成迁移

当您的团队熟悉 Kotlin 并迁移了较小的部分后,您可以继续处理较大的组件,例如片段、活动、ViewModel 对象以及与业务逻辑相关的其他类。

注意事项

与 Java 具有特定风格类似,Kotlin 也具有其自身的惯用风格,这有助于其简洁性。但是,您可能会发现团队最初编写的 Kotlin 代码看起来更像是它所替换的 Java 代码。随着团队 Kotlin 经验的增长,这种情况会随着时间而改变。请记住,循序渐进的变化是成功的关键。

以下是一些在 Kotlin 代码库不断增长时保持一致性的方法

常见的编码标准

务必在采用过程的早期定义一套标准的编码约定。您可以在有必要时与 Android 的 Kotlin 风格指南 有所偏差。

静态分析工具

使用 Android lint 和其他静态分析工具来强制执行为团队设定的编码标准。第三方 Kotlin linter klint 还为 Kotlin 提供了额外的规则。

持续集成

务必遵守常见的编码标准,并为您的 Kotlin 代码提供足够的测试覆盖率。将此作为自动化构建过程的一部分可以帮助确保一致性和对这些标准的遵守。

互操作性

Kotlin 与 Java 的互操作性在很大程度上是无缝的,但请注意以下几点。

可空性

Kotlin 依靠编译代码中的可空性注释来推断 Kotlin 侧的可空性。如果没有提供注释,Kotlin 默认使用平台类型,该类型可以被视为可空类型或不可空类型。但是,如果不仔细处理,这会导致运行时 NullPointerException 问题。

采用新功能

Kotlin 提供了许多 新库 和语法糖来减少样板代码,这有助于提高开发速度。也就是说,在使用 Kotlin 的标准库函数(例如 集合函数协程lambda 表达式)时,请谨慎行事并采取循序渐进的方法。

以下是一个非常常见的陷阱,新 Kotlin 开发人员会遇到它。假设以下 Kotlin 代码

val nullableFoo: Foo? = ...

// This lambda executes only if nullableFoo is not null
// and `foo` is of the non-nullable Foo type
nullableFoo?.let { foo ->
   foo.baz()
   foo.zap()
}

此示例的目的是在 nullableFoo 不为 null 时执行 foo.baz()foo.zap(),从而避免 NullPointerException。虽然此代码按预期工作,但它不如简单的空检查和 智能转换 那么直观,如下面的示例所示

val nullableFoo: Foo? = null
if (nullableFoo != null) {
    nullableFoo.baz() // Using !! or ?. isn't required; the Kotlin compiler infers non-nullability
    nullableFoo.zap() // from guard condition; smart casts nullableFoo to Foo inside this block
}

测试

在 Kotlin 中,默认情况下类及其函数对扩展是关闭的。您必须显式打开要子类的类和函数。这种行为是语言设计决定,旨在促进组合而不是继承。Kotlin 内置支持通过 委托 来实现行为,以帮助简化组合。

这种行为会对 Mockito 等模拟框架造成问题,这些框架依赖于接口实现或继承来在测试期间覆盖行为。对于单元测试,您可以启用 Mockito 的 Mock Maker Inline 功能,该功能允许您模拟最终类和方法。或者,您可以使用 All-Open 编译器插件 来打开您希望在编译过程中测试的任何 Kotlin 类及其成员。使用此插件的主要优点是它适用于单元测试和工具测试。

更多信息

有关使用 Kotlin 的更多信息,请查看以下链接