30
29

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

インターフェースのdefaultメソッド、ちゃんと使えていますか?

Posted at

はじめに

Java 8でインターフェースのdefaultメソッドが導入され、Java 9からはインターフェースにprivateメソッドも定義できるようになりました。
便利な機能なのですが、誤った使い方で濫用するとアプリケーションの保守性を下げてしまいます。では、どのような使い方が望ましいのでしょうか?

良くない例

public interface BadUsage {

    void someMethod();

    // (1) ミックスインや実装継承を目的としたユーティリティ的なもの
    default String normalize(String str) {
        return str.toUpperCase().trim();
    }

    // (2) 他のオブジェクトに依存した処理
    default int getSomeValue() {
        return Singleton.getInstance().getValue();
    }
}

上記サンプルコードの(1)は、インターフェースを実装するクラスに対してミックスイン的に機能を追加したり、実装クラス側で共通的に利用できるユーティリティ的な機能を提供する用途でdefaultメソッドを使っています。
継承よりも移譲というのはオブジェクト指向の鉄則です。このような使い方は避け、ユーティリティクラスやヘルパークラスを使うのがよいでしょう。
(2)も用途としては(1)に近いのですが、さらに問題なのは、他のオブジェクトに依存した処理を記述していることです。この例ではシングルトンオブジェクトにアクセスしていますが、他にも以下のような処理が例として挙げられます。

  • ファイルやネットワークなどのIO
  • スレッドへのアクセス
  • DIコンテナへのアクセス(SpringのApplicationContextなど)

このような処理を書いてしまうと、ユニットテストも大変困難になります。そもそもインターフェースが具象クラスに依存してはいけません。

良くない使い方の見分け方

インターフェースのdefaultメソッドはpublicであり、抽象メソッドと同様に利用者に対して公開される操作です。つまりはdefaultメソッドも、インターフェースが果たすべき責務の一部であるべきです。
オブジェクト指向におけるオブジェクトデータ振る舞いをまとめたものですから、振る舞いを表すインスタンスメソッドは、オブジェクトのデータ(インスタンス変数)へのアクセスが発生するはずです。

もちろんインターフェース自体にインスタンス変数は持てませんから、defaultメソッドは、他の抽象メソッドを呼び出すことで間接的に実装クラスのインスタンス変数へアクセスするのです。
逆に言うと、他の抽象メソッドを呼び出さないdefaultメソッドには疑問を持つべきです。

正しい使用例

以下のようなインターフェースがあったとします。Compositeパターンのインターフェースで、具象クラスにはCompositeLeafが存在します。

Component.java
public interface Component {

    String getName();

    int getPoint();

    List<Component> getChildren();

    boolean isLeaf();

}

このインターフェースに、条件に合致するコンポーネントを一覧で返すfindメソッドを追加したいとしましょう。このメソッドは、具象クラスに依存しない共通処理として記述できるので、defaultメソッドとして実装します。

Component.java
    default List<Component> find(Predicate<Component> p) {
        return getChildren().stream().filter(p).collect(Collectors.toList());
    }

このように、具象クラスが実際に何であるかに関わらず、Componentインターフェースを正しく実装していれば普遍的に成り立つような共通的な振る舞いをdefaultメソッドで実装すべきなのです。

例外的なケース

例外的に(他の抽象メソッドを呼び出さないことを)許容できるケースがあります。それは、デフォルト実装を提供する抽象基底クラスを作る代わりにインターフェースだけで済ませるケースです。
具体例で見てみましょう。先程のコンポジット構造を走査するVisitorパターンを導入するとします。

Visitor.java
public interface Visitor {

    void visit(Composite composite);

    void visit(Leaf leaf);
}
Component.java
public interface Component {
    // ...
    void accept(Visitor visitor);
}

以下は具体的なVisitor実装クラスの例です。

LeafCounterVisitor.java
public class LeafCounterVisitor implements Visitor {

    private int count = 0;

    @Override
    public void visit(Composite composite) {
        // Compositeノードには関心がないので、空実装
    }

    @Override
    public void visit(Leaf leaf) {
        count ++;
    }

    public int getCount() {
        return count;
    }
}

Visitorにとって関心のないノードへの訪問(visit)は、上記のように空実装になります。今回はノードが2種類しかないのですが、もし10個あったとすると、全てのvisitメソッドをオーバーライドして空実装を入れるのはなかなか億劫な作業です。
その場合、以下のように予め空実装を用意した抽象基底クラスを用意しておき、このクラスを継承するという手段があります。(派生クラスでは、関心のあるvisitメソッドだけをオーバーライドすればよい)

BaseVisitor.java
public abstract class BaseVisitor implements Visitor {

    @Override
    public void visit(Composite composite) {
        // 空実装
    }

    @Override
    public void visit(Leaf leaf) {
        // 空実装
    }
}

インターフェースのdefaultメソッドを使えば、上の抽象基底クラスは不要となります。

Visitor.java(修正版)
public interface Visitor {

    default void visit(Composite composite) {}

    default void visit(Leaf leaf) {}
}

このような使い方は、デフォルト実装を提供するという意味で、defaultメソッドの本来の意図から外れないのではないかと思います。

まとめ

インターフェースのdefaultメソッドは、デフォルト実装を持つという点を除けば、他の抽象メソッドと同様に捉えてオブジェクト指向設計らしく使うのがよいでしょう。

30
29
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
29

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?