この記事は Murat Yener による Android Developers - Medium の記事 "Delegating Delegates to Kotlin" を元に翻訳・加筆したものです。詳しくは元記事をご覧ください。


Kotlin Vocabulary: 委譲


仕事を完了する方法の 1 つは、その仕事を他者に委譲することです。皆さんの仕事を友だちに委譲することを言っているわけではありません。今回のテーマは、あるオブジェクトから別のオブジェクトに委譲することです。


ソフトウェアの世界では、委譲という考え方は新しいものではありません。委譲はデザイン パターンの 1 つで、あるオブジェクトが デリゲートと呼ばれるヘルパー オブジェクトに委譲することでリクエストを処理することを指します。デリゲートの役割は、元のオブジェクトに代わってリクエストを処理し、その結果を元のオブジェクトが利用できるようにすることです。


Kotlin はクラス委譲とプロパティ委譲をサポートしているので、委譲を簡単に扱えます。さらに、いくつかの独自の組み込みデリゲートも提供しています。

クラス委譲

最後に削除された項目を復元できる ArrayList を使うとしましょう。基本的に、必要なのは同じ ArrayList の機能だけですが、最後に削除された項目への参照が必要です。


これを実現する方法の 1 つは、ArrayList クラスを拡張することです。この新しいクラスは、MutableList インターフェースの実装ではなく ArrayList の具象クラスを拡張したものなので、ArrayList の具象クラスの実装と強く結合されることになります。


MutableList の実装で remove() 関数をオーバーライドし、削除した項目の参照を保持できるようにしたうえで、その他の空の実装を他のオブジェクトに委譲したいと思ったことはありませんか?Kotlin では、これを実現する方法が提供されています。具体的には、内部 ArrayList インスタンスに作業の大半を委譲し、その動作をカスタマイズできます。これを行うため、Kotlin には新しいキーワード by が導入されています。


では、クラス委譲の仕組みを確認してみましょう。by キーワードを使うと、Kotlin は innerList インスタンスをデリゲートとして使用するコードを自動的に生成します。


<!-- Copyright 2019 Google LLC.

SPDX-License-Identifier: Apache-2.0 -->


class ListWithTrash <T>(

   private val innerList: MutableList<T> = ArrayList<T>()

) : MutableCollection<T> by innerList {

   var deletedItem : T? = null

   override fun remove(element: T): Boolean {

       deletedItem = element

       return innerList.remove(element)

   }

   fun recover(): T? {

       return deletedItem

   }

}


by キーワードは、MutableList インターフェースの機能を innerList という名前の内部 ArrayList インスタンスに委譲するよう Kotlin に伝えます。内部 ArrayList オブジェクトに直接橋渡しするメソッドが提供されるので、ListWithTrashMutableList インターフェースのすべての機能をサポートします。さらに、独自の動作を追加することもできるようになります。

内部処理

動作の仕組みを確認してみましょう。ListWithTrash のバイトコードを逆コンパイルした Java コードを見ると、Kotlin コンパイラが実際にラッパー関数を作成していることを確認できます。このラッパー関数が、内部 ArrayList オブジェクトの対応する関数を呼び出していることもわかります。


public final class ListWithTrash implements Collection, KMutableCollection {

  @Nullable

  private Object deletedItem;

  private final List innerList;


  @Nullable

  public final Object getDeletedItem() {

     return this.deletedItem;

  }


  public final void setDeletedItem(@Nullable Object var1) {

     this.deletedItem = var1;

  }


  public boolean remove(Object element) {

     this.deletedItem = element;

     return this.innerList.remove(element);

  }


  @Nullable

  public final Object recover() {

     return this.deletedItem;

  }


  public ListWithTrash() {

     this((List)null, 1, (DefaultConstructorMarker)null);

  }


  public int getSize() {

     return this.innerList.size();

  }


  // $FF: bridge method

  public final int size() {

     return this.getSize();

  }

  //...and so on

}


注: 生成されたコードで、Kotlin コンパイラは Decorator パターンという別のデザイン パターンを使ってクラス委譲をサポートしています。Decorator パターンでは、デコレータ クラスがデコレートされるクラスと同じインターフェースを共有します。デコレータ クラスは、ターゲット クラスの内部参照を保持し、そのインターフェースで提供されるすべてのパブリック メソッドをラップ(デコレート)します。


委譲は、特定のクラスを継承できない場合に特に便利です。クラス委譲を使うと、クラスが他のクラスの階層に含まれることはなくなります。その代わり、同じインターフェースを共有し、元の型の内部オブジェクトをデコレートします。つまり、パブリック API を維持したまま、実装を簡単に入れ替えることができます。

プロパティ委譲

by キーワードを使うと、クラス委譲だけでなく、プロパティを委譲することもできます。プロパティ委譲では、デリゲートはプロパティの get 関数と set 関数の呼び出しを担当します。他のオブジェクトで getter/setter ロジックを再利用しなければならない場合、対応するフィールドだけでなく機能を簡単に拡張することができるので、この機能が非常に便利です。


次のような定義の Person クラスがあったとしましょう。


class Person(var name:String, var lastname:String)


このクラスの name プロパティには、いくつかのフォーマット要件があります。name を設定するとき、先頭の文字が大文字、他の文字が小文字になるようにします。さらに、 name を更新する場合、updateCount プロパティを自動的にインクリメントします。

この機能は、次のように実装してもいいかもしれません 。


<!-- Copyright 2019 Google LLC.

SPDX-License-Identifier: Apache-2.0 -->


class Person(name: String, var lastname: String) {

   var name: String = name

       set(value) {

           field = value.toLowerCase().capitalize()

           updateCount++

       }

   var updateCount = 0

}


これは動作しますが、要件が変わって lastname が変更されたときも updateCount をインクリメントすることになるとどうでしょうか。ロジックをコピーして貼り付け、カスタムの setter を書いてもいいかもしれませんが、両方のプロパティにまったく同じ setter を書いていることに気づくでしょう。


<!-- Copyright 2019 Google LLC.

SPDX-License-Identifier: Apache-2.0 -->


class Person(name: String, lastname: String) {

   var name: String = name

       set(value) {

           field = value.toLowerCase().capitalize()

           updateCount++

       }

   var lastname: String = lastname

       set(value) {

           field = value.toLowerCase().capitalize()

           updateCount++

       }

   var updateCount = 0

}


どちらの setter メソッドもほぼ同じということは、どちらかは不要ということです。プロパティ委譲を使うと、getter と setter をプロパティに委譲してコードを再利用できます。

クラス委譲と同じように、by を使ってプロパティを委譲します。すると、プロパティ構文を使ったときに、Kotlin はデリゲートを使うコードを生成します。


<!-- Copyright 2019 Google LLC.

SPDX-License-Identifier: Apache-2.0 -->


class Person(name: String, lastname: String) {

   var name: String by FormatDelegate()

   var lastname: String by FormatDelegate()

   var updateCount = 0

}


この変更を行うと、name プロパティと lastname プロパティが FormatDelegate クラスに委譲されます。FormatDelegate のコードを確認してみましょう。デリゲート クラスは、getter だけを委譲する場合は ReadProperty<Any?, String> を、getter と setter の両方を委譲する場合は ReadWriteProperty<Any?, String> を実装する必要があります。この例の FormatDelegate は、setter が呼び出された場合にフォーマット処理を行うので、ReadWriteProperty<Any?, String> を実装しなければなりません。


<!-- Copyright 2019 Google LLC.

SPDX-License-Identifier: Apache-2.0 -->


class FormatDelegate : ReadWriteProperty<Any?, String> {

   private var formattedString: String = ""


   override fun getValue(

       thisRef: Any?,

       property: KProperty<*>

   ): String {

       return formattedString

   }


   override fun setValue(

       thisRef: Any?,

       property: KProperty<*>,

       value: String

   ) {

       formattedString = value.toLowerCase().capitalize()

   }

}


getter 関数と setter 関数に 2 つの追加パラメータがあることに気づいた方もいらっしゃるでしょう。最初のパラメータ thisRef は、プロパティを含むオブジェクトを表します。これを使うと、オブジェクト自体にアクセスし、他のプロパティを確認したり、他のクラス関数を呼び出したりできます。2 つ目のパラメータは KProperty<*> です。これは、委譲されたプロパティについてのメタデータにアクセスするために使うことができます。

先ほどの要件を思い出してみてください。thisRef を使って updateCount プロパティにアクセスし、インクリメントしてみましょう。


<!-- Copyright 2019 Google LLC.

SPDX-License-Identifier: Apache-2.0 -->


override fun setValue(

   thisRef: Any?,

   property: KProperty<*>,

   value: String

) {

   if (thisRef is Person) {

       thisRef.updateCount++

   }

   formattedString = value.toLowerCase().capitalize()

}


内部処理

この仕組みを理解するため、逆コンパイルした Java コードを見てみます。Kotlin コンパイラは、name プロパティと lastname プロパティについての FormatDelegate オブジェクトへのプライベートな参照を保持するためのコードと、追加したロジックを含む getter/setter の両方を生成します。

さらに、委譲されるプロパティを保持する KProperty[] も作成しています。name プロパティに対して生成された getter と setter を見てみると、インスタンスはインデックス 0 に保存されています。一方、lastname プロパティはインデックス 1 に保存されています。


public final class Person {

  // $FF: synthetic field

  static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Reflection.getOrCreateKotlinClass(Person.class), "name", "getName()Ljava/lang/String;")), (KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Reflection.getOrCreateKotlinClass(Person.class), "lastname", "getlastname()Ljava/lang/String;"))};

  @NotNull

  private final FormatDelegate name$delegate;

  @NotNull

  private final FormatDelegate lastname$delegate;

  private int updateCount;


  @NotNull

  public final String getName() {

     return this.name$delegate.getValue(this, $$delegatedProperties[0]);

  }


  public final void setName(@NotNull String var1) {

     Intrinsics.checkParameterIsNotNull(var1, "<set-?>");

     this.name$delegate.setValue(this, $$delegatedProperties[0], var1);

  }

  //...

}


この仕組みによって、通常のプロパティ構文を使って任意の呼び出し元が委譲されるプロパティにアクセスできるようになっています。


person.lastname = “Smith” //

println(“Update count is $person.count”)


Kotlin は単に委譲をサポートしているだけではありません。Kotlin 標準ライブラリで組み込みの委譲も提供していますが、詳しくは別の記事で説明したいと思います。

委譲は他のオブジェクトにタスクを委譲する際に役立ち、コードの再利用性を高めます。Kotlin コンパイラは、委譲をシームレスに使えるようにコードを作成します。Kotlin は、by キーワードを使ったシンプルな構文でプロパティやクラスの委譲を行います。Kotlin コンパイラは、パブリック API を一切変更せず、委譲をサポートするために必要なすべてのコードを内部的に生成します。簡単に言えば、Kotlin は委譲に必要なボイラープレート コードをすべて生成して維持してくれます。つまり、委譲を Kotlin に委譲することができるのです。


Reviewed by Yuichi Araki - Developer Relations Team