覚えたら書く

IT関係のデベロッパとして日々覚えたことを書き残したいです。twitter: @yyoshikaw

Kotlin - 移譲

Java(オブジェクト指向言語)を用いて大規模なシステムを設計・開発した場合に、実装の継承 によってシステムが壊れていくことが多々あります。

共通化という名目(DRYの達成)のために、実装クラスを継承してしまっていると、
クラスを拡張し、いくつかのメソッドをオーバーライドするときに、拡張元の基底クラスの実装の詳細に依存するようになり、極端な密結合が発生します。

対象のシステムが発展する中で、基底クラスの実装内容が変更されたり、新しいメソッドが追加されたりすると
継承した側の子クラスのコードが正しく動かなくなる事があります。

実装クラスの継承を行うと、コードを共通化して綺麗に整理できた気分になれるので、陥りがちな状況な気がします・・・。
対象システムの改修作業のために、後から参画したメンバーやコードを保守するメンバーが痛い目に逢います。
後任者からは、「結局、親クラスの親クラスまで見ないとコードの意味わからないじゃん、影響範囲わからないじゃん。」とかいう声が上がる事でしょう。


Kotlin では、上記のような問題への対処のために、デフォルトでクラスは final の状態(継承不可)となっています。
拡張性を考えて設計されたクラスのみを継承可能としており、あえて open という修飾子を指定する必要があります。


クラスを拡張したいケースでは、実装の継承を行う代わりに、
拡張元のクラスと同じインターフェースを実装して、元の実装クラスをフィールドに持つという方法が取られます。
拡張が必要ないメソッドはそのまま元の実装クラスに処理を転送、拡張が必要なメソッドは自前の実装でオーバーライドする
というやり方になります。

ただし、Java でこのアプローチを行なった場合には元の実装クラスへ処理を転送する大量のボイラープレートコードが発生します。

java.util.List を実装したクラスを作成して、フィールドに持ったArrayList のインスタンスに処理を転送しようとすると以下のようなコードになってしまいます。

public class ExArrayList<String> implements List<String> {

    // 処理の転送先となる実装クラス
    private final List<String> internalList = new ArrayList<>();

    // 拡張したメソッド
    @Override
    public boolean add(String string) {
        System.out.println("Try add " + string);
        return internalList.add(string);
    }

    @Override
    public int size() {
        return internalList.size();
    }

    @Override
    public boolean isEmpty() {
        return internalList.isEmpty();
    }

    @Override
    public boolean contains(Object o) {
        return internalList.contains(o);
    }

    @Override
    public Iterator<String> iterator() {
        return internalList.iterator();
    }

    @Override
    public Object[] toArray() {
        return internalList.toArray();
    }

    @Override
    public boolean remove(Object o) {
        return internalList.remove(o);
    }

    @Override
    public void replaceAll(UnaryOperator<String> operator) {
        internalList.replaceAll(operator);
    }

    @Override
    public void sort(Comparator<? super String> c) {
        internalList.sort(c);
    }

    @Override
    public void clear() {
        internalList.clear();
    }

    @Override
    public boolean equals(Object o) {
        return internalList.equals(o);
    }

    @Override
    public int hashCode() {
        return internalList.hashCode();
    }

    @Override
    public String get(int index) {
        return internalList.get(index);
    }

    @Override
    public String set(int index, String element) {
        return internalList.set(index, element);
    }

    @Override
    public void add(int index, String element) {
        internalList.add(index, element);
    }

    @Override
    public String remove(int index) {
        return internalList.remove(index);
    }

   // 以下省略
}


ほとんどの場面で、実装の継承をするよりもより良い方法とはなりますが、移譲(delegation)というものを Java が言語的にサポートしていないために、
ボイラープレートコードが生まれてしまうという状況に陥ってしまいます。


Kotin では、この 移譲 を第一級言語機能としてサポートしています。
インターフェースを実装している場合、 by キーワード を利用する事によってインターフェースの実装を別のオブジェクトに移譲できます。


Kotlin でさきほどの実装と同様のクラスの宣言は以下のように記述できます。
自分で拡張したい add メソッド以外は登場しておらず、ボイラープレートコードが激減しています。

class ExArrayList<String>(val innerList: MutableList<String> = ArrayList()): MutableList<String> by innerList {

    override fun add(element: String): Boolean {
        println("Try add $element")
        return innerList.add(element)
    }

}

このクラスを実行するサンプルコードで実行してみると

val list = ExArrayList<Any>()
list.add("Kotlin")
list.add("Java")

println("$list : List size = ${list.size}")

実行結果は以下の通りです。addメソッドが予定通り拡張されていることが分かります。

Try add Kotlin
Try add Java
net.yyuki.sandbox.list2.ExArrayList@59a6e353 : List size = 2


by キーワードはあくまで対象インターフェースを転送してくれるだけのようなので、それとは関係ない toString などはオーバーライドされていないようです。

toString までオーバーライドまでしたければ、データクラス として宣言しちゃえばいいわけですが、こんなことしていいのかは良く分かってないです笑

data class ExArrayList<String>(val innerList: MutableList<String> = ArrayList()): MutableList<String> by innerList {

    override fun add(element: String): Boolean {
        println("Try add $element")
        return innerList.add(element)
    }

}

こうすると toString の結果も意味のある文字列となります

Try add Kotlin
Try add Java
ExArrayList(innerList=[Kotlin, Java]) : List size = 2


まとめ

"継承よりコンポジションを選ぶ" という記述が「Effective Java」に存在していますが、残念ながらJavaでは移譲(delegate)は、第一級言語機能となっていません。
そのためか、Javaで作られたシステムでは、実装の継承 というものが氾濫していることが少なくありません。

Kotlin では by キーワードによって簡単に移譲を実現できます。これによって、クラス間の不要な密結合が生まれにくくなると思われます。



関連エントリ