LINEヤフー Tech Blog

LINEヤフー株式会社のサービスを支える、技術・開発文化を発信しています。

コード品質向上のテクニック:第28回 制約にも相続税

こんにちは。コミュニケーションアプリ「LINE」のモバイルクライアントを開発している石川です。

この記事は、毎週木曜の定期連載 “Weekly Report” 共有の第 28 回です。Weekly Report については、第 1 回の記事を参照してください。

制約にも相続税

Kotlin では Array<Int>List<Int> よりも、IntArray (Java の int[] に相当) の方が、boxing が不要といった理由で効率的になることが多いです。ただし、IntArray は可変である上に、List のユーティリティをそのまま使うことができません。そこで、ある開発者が以下のように ImmutableIntList というクラスを作りました。このクラスは、List のインターフェースをもつ IntArray のラッパーで、不変であることを保証できます。(注: Kotlin の List は Java の List とは異なり、setadd といったメソッドは持ちません。)

class ImmutableIntList(vararg values: Int): List<Int> {
    // It's safe to assign `values` because `vararg` does not keep the reference to the `*array` argument.
    private val valueArray : IntArray = values

    override operator fun get(index: Int): Int = valueArray[index]

    override val size: Int = valueArray.size

    ... snip
}

そして、別の開発者が ImmutableIntList の様々なバリエーションを作りたいと考えたとします。例えば、要素がソートされていることを保証するような、ImmutableSortedIntList といったものです。以下の実装では ImmutableIntList を継承可能 (open class) にし、その子クラスとして ImmutableSortedIntList を定義しています。

open class ImmutableIntList(vararg values: Int): List<Int> {
    
    protected val valueArray : IntArray = values

    override operator fun get(index: Int): Int = valueArray[index]

    ... snip
}

class ImmutableSortedIntList(vararg values: Int) :
    ImmutableIntList(*values.sorted().toIntArray())

class AnotherImmutableIntList(...): ImmutableIntList() {
    ... // may touch `valueArray` here.
}

この構造に何か問題はありますか?

不変性への相続税

不変であることを保証したい場合は、継承不能にしたほうがよいです。あるクラスで Immutable と名付けても、継承可能な場合は子クラスで可変にできてしまいます。

特に ImmutableIntList は汎用的で広く使われるような名前がつけられているため、思わぬパッケージやモジュールで「誤った継承がされる」可能性があります。たとえば、以下のように不変性を破る子クラス、見知らぬどこかで作られてしまうかもしれません。

class MutableIntList(...): ImmutableIntList(...) {
    operator fun set(index: Int, value: Int) {
        valueArray[index] = value
    }

    ...
}

val mutableIntList = MutableIntList(...)

// Although this is an `ImmutableIntList`, it's actually mutable
// because it's modifiable by accessing `mutableIntList`.
val immutableIntList: ImmutableIntList = mutableIntList

これを防ごうとして valueArrayprivate にしても、不変性を維持するためには不十分です。get 関数がまだオーバーライド可能であるため、valueArray を無視した実装ができてしまいます。

class IntListWithMutableSuffix(...): ImmutableIntList(...) {

    var suffix: Int = 0

    override operator fun get(index: Int): Int =
        if (index != super.size) super[get] else suffix

    ...
}

不変性に限った話ではないですが、基本的・汎用的なクラスを継承可能にする場合、どのメンバーをオーバーライド可能にするかは慎重に検討する必要があります。また、継承可能にするのではなく、継承の関係にない独立した型を定義することも検討してください。

不変性と継承関係

一般に、可変なオブジェクトのクラスと不変なオブジェクトのクラスは、互いに継承関係を持たないようにすべきです。

  • 可変が不変を継承する場合: 先述のように不変性の制約が破られる。
  • 不変が可変を継承する場合: 変更するメソッド (add/set など) が不変なオブジェクトのクラスからも呼び出せる必要があるため、実行時エラーが必須になる (例: Guava の ImmutableList)。

可変と不変の共通の親クラスが必要な場合は、「読み取り専用 (read-only, unmodifiable)」として作るのが良いでしょう。(Kotlin の List もそのような継承構造になっています。)

  • 可変が読み取り専用を継承する場合:問題なし。親クラスの「メソッド」は子クラスのメソッドのサブセットになっている。(子クラスだけが set/add などを持ち、親クラスだけで有効なメソッドはない)
  • 不変が読み取り専用を継承する場合:問題なし。親クラスの「制約」は子クラスの制約のサブセットになっている。(子クラスだけが不変というを持ち、親クラスだけの制約はない)

一言まとめ

不変性を保証したい場合、継承不能にしたほうがよい。

キーワード: immutability, inheritance, override

コード品質向上のテクニックの他の記事を読む

コード品質向上のテクニックの記事一覧