読者です 読者をやめる 読者になる 読者になる

算譜王におれはなる!!!!

偏りはあると思うけど情報技術全般についてマイペースに書くよ。

KotlinのClass Delegationについて

KotlinにはClass Delegationという機能があります。 公式ドキュメントはこちら

これの使い道について書きたいと思います。

そもそもClass Delegationって?

Delegation、つまり委譲の仕組みです。

例えばこんな感じ。

// 挨拶する人のインタフェース
trait Greeter {
    fun greet()
}

// Greeterの実装クラス(というかオブジェクト)
object JapaneseGreeter: Greeter {
    override fun greet() {
        println("こんにちは")
    }
}

// ここでClass Delegationを使っている
class Person(greeter: Greeter): Greeter by greeter

fun main(args: Array<String>) {
    val person = Person(JapaneseGreeter)

    // personはgreet()メソッドを呼び出せる
    person.greet() // => こんにちは
    
    // personはGreeterのサブタイプ
    val greeter: Greeter = person
}

Personクラスは: GreeterによりGreeterトレイトを実装しています。が、Personにはgreetメソッドの定義はありません。: Greeter by greeterと指定することでコンストラクタで受け取るgreeterに処理が委譲されるのです。 なるほど、書き方はわかった。じゃあ何に役立つのでしょうか。

「継承よりコンポジションを選ぶ」

Effective Java第2版の項目16には「継承よりコンポジションを選ぶ」という教えが掲げられています。 具象クラス(実装)を継承して、いわゆる差分プログラミングで楽をする誘惑に駆られたことのある人は多いと思います。 しかし実装の継承は落とし穴があります。 継承は実装のためではなく、型のために行うべきです。 詳細はこの記事がわかりやすいです。

qiita.com

さて、KotlinのClass Delegationの話に戻ります。 先に示したコードで、PersonGreeterのサブタイプです。 Greeterはインタフェースなので実装の継承ではなく型の継承です。 そしてPersonには実装の記述はありませんが動作します。 実装はコンストラクタで渡されるgreeterに委ねられます。 言いたいことは、実装ではなく型のみの継承を行い、実装の継承に付き物の落とし穴を回避しつつも差分プログラミングのような手軽さをClass Delegationは実現してくれる、ということです。

具体例

同じ題材を使います。 挿入された回数を記憶するSetを作りたいと思います。

上記記事ではHashSet、すなわち具象クラスを継承することで期待通りに動かないクラスを作ってしまっています。 そこでスーパクラスの実装に影響を受けないように、インタフェースのサブタイプとなり具象クラスのインスタンスへ委譲する(上記記事では「転送」という言葉が使われていますが)ような方法を採っています。フィールドに具象クラスのインスタンスを保持し、インタフェースが提供する抽象メソッドをすべて実装して、独自の振る舞いを持たない場合には単に具象クラスのインスタンスへそのまま処理を流す、、という感じになります。

このよくあるパターンをClass Delegationを使うと簡単に記述できます。実際のコードはこうです。

class InstrumentedSet<E>(private val s: MutableSet<E>) : MutableSet<E> by s {

    var addCount: Int = 0
        private set
 
    override fun add(e: E): Boolean {
        addCount++
        return s.add(e)
    }
 
    override fun addAll(c: Collection<E>): Boolean {
        addCount += c.size()
        return s.addAll(c)
    }
}

fun main(args: Array<String>) {
    val instrumentedSet = InstrumentedSet(java.util.HashSet<String>())
 
    instrumentedSet.add("hoge")
    instrumentedSet.add("fuga")
    instrumentedSet.addAll(listOf("a", "b", "c"))
    println(instrumentedSet.addCount) // => 5
 
    // InstrumentedSetで定義していない実装は委譲先の実装が使われる
    println(instrumentedSet.contains("hoge")) // => true
}

InstrumentedSetがオーバライドしているメソッドは2つだけです。 その他にもSetが提供する抽象メソッドはたくさんありますが、具象クラスのインスタンスsに委譲するだけならわざわざ記述する必要はありません。

このように「型の継承をしつつ、実装は外部からインジェクトする」ようなパターンのための機能がClass Delegationです。