大規模なチームに Kotlin を導入する

新しい言語への移行には大きな困難が伴います。成功のこつは、最初から多くを求めず、段階的に進み、頻繁なテストによってチームを成功に向けて動かすことにあります。 Kotlin は JVM バイトコードにコンパイルされ、Java との完全な相互運用が可能であるため、Kotlin を使用すると移行が容易になります。

チームの構築

移行前の最初のステップは、チーム内で共通の基本的理解を構築することです。ここでは、チームの学習促進に役立つヒントをいくつかご紹介します。

学習グループの形成

学習グループは、学習と定着を円滑に進めるための効果的な方法です。研究によると、グループ環境で学んだことを復唱することで、学習内容を強化できることが示唆されています。グループの各メンバーに Kotlin の書籍などの学習資料を用意し、毎週数章を読むようグループに指示してください。それぞれの会合において、グループ内で、学んだ内容を比較し、質問や意見を話し合うとよいでしょう。

教える文化を築く

人に教えるのがうまいと自覚している人は少ないとしても、教えることは誰にでもできます。テクノロジー リーダーやチームリーダーから個々のコントリビューターに至るまで、誰もが成功に役立つ学習環境を推進できます。それを円滑に進める方法の 1 つとして、チームの 1 人を指名して学んだことや共有したいことを話してもらうプレゼンテーションを定期的に行うことが挙げられます。新しい章のプレゼンテーションをする有志を、チームが言語に慣れるまで毎週募ることで、学習グループを活用できます。

チャンピオンを指名する

最後に、学習の取り組みをリードするチャンピオンを指名します。導入プロセスを開始する際には、この人物がチームリード(SME)の役割を果たすことができます。Kotlin に関連するすべての練習会にこの人物を含めることが重要です。チャンピオンとしては、すでに Kotlin に興味を持ち、ある程度の実践的知識を持っている人物が理想的です。

時間をかけて統合する

重要なのは、初めに時間をかけて、エコシステムのどの部分を最初に動かすべきかを戦略的に考えることです。これは多くの場合、主要アプリではなく組織内の 1 つのアプリを特定して行うのが効果的です。選択したアプリの移行について、状況がそれぞれ異なることを前提に、一般的なスタート ポイントをいくつか紹介します。

データモデル

データモデルは、たいていの場合、多くの状態情報といくつかのメソッドで構成されています。データモデルには、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 + '\'' +
               '}';
   }
}

この Java クラスを、次のような 1 行の Kotlin で置き換えることができます。

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

このコードについては、現在のテストスイートに照らし合わせて単体テストを実施できます。ポイントは、一度に 1 つのモデルで小規模に始め、(ほとんどが行動ではなく状態である)クラスを移行させることです。移行中にテストを頻繁に行ってください。

テストを移行する

もう 1 つの開始方法としては、既存のテストを変換して、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 拡張機能を使用すると、同じ Utils インターフェースを Java 呼び出し元に提供するとともに、拡大する 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 コードベースが拡大していくと、次のようなことによって整合性の実現を図れるようになります。

一般的なコーディング標準

導入プロセス初期にコーディング規則の標準セットを定義しておく必要があります。適用可能な Kotlin スタイルガイドからそれることも可能です。

静的分析ツール

Android Lint などの静的分析ツールを使用して、コーディング標準セットをチームに適用します。サードパーティの Kotlin リンターである klint からも、Kotlin の追加ルールが提供されています。

継続的インテグレーション

必ず共通のコーディング標準に則り、Kotlin コードを十分にテストしてください。これを自動化ビルドプロセスの一環として行うと、これらの標準の一貫性と遵守を確保できます。

相互運用性

Kotlin はほとんどの部分において Java とシームレスに相互運用しますが、次の点に注意してください。

null 値許容

Kotlin は、コンパイル済みコードの null 値許容アノテーションを利用して、Kotlin 側での null 値許容を推測します。アノテーションが提供されない場合、Kotlin はデフォルトで、null 値許容型としても null 値非許容型としても扱えるプラットフォーム型になります。ただし、慎重に取り扱わないとランタイム NullPointerException 問題につながる可能性があります。

新機能を採用する

Kotlin にはボイラープレートを削減するための新しいライブラリと糖衣構文が多数用意されています。これにより、開発スピードを向上させることができます。ただし、コレクション関数コルーチンラムダなど、Kotlin の標準ライブラリ関数を使用するときには注意と秩序が必要です。

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 を回避することです。このコードは想定どおりに動作しますが、次の例に示すように、単純な null チェックやスマート キャストに比べると、読みやすさでは劣ります。

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 の使用について詳しくは、以下のリンクを参照してください。