はじめに
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パターン
のインターフェースで、具象クラスにはComposite
とLeaf
が存在します。
public interface Component {
String getName();
int getPoint();
List<Component> getChildren();
boolean isLeaf();
}
このインターフェースに、条件に合致するコンポーネントを一覧で返すfind
メソッドを追加したいとしましょう。このメソッドは、具象クラスに依存しない共通処理として記述できるので、defaultメソッドとして実装します。
default List<Component> find(Predicate<Component> p) {
return getChildren().stream().filter(p).collect(Collectors.toList());
}
このように、具象クラスが実際に何であるかに関わらず、Component
インターフェースを正しく実装していれば普遍的に成り立つような共通的な振る舞いをdefaultメソッドで実装すべきなのです。
例外的なケース
例外的に(他の抽象メソッドを呼び出さないことを)許容できるケースがあります。それは、デフォルト実装を提供する抽象基底クラスを作る代わりにインターフェースだけで済ませるケースです。
具体例で見てみましょう。先程のコンポジット構造を走査するVisitorパターン
を導入するとします。
public interface Visitor {
void visit(Composite composite);
void visit(Leaf leaf);
}
public interface Component {
// ...
void accept(Visitor visitor);
}
以下は具体的なVisitor実装クラスの例です。
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メソッドだけをオーバーライドすればよい)
public abstract class BaseVisitor implements Visitor {
@Override
public void visit(Composite composite) {
// 空実装
}
@Override
public void visit(Leaf leaf) {
// 空実装
}
}
インターフェースのdefaultメソッドを使えば、上の抽象基底クラスは不要となります。
public interface Visitor {
default void visit(Composite composite) {}
default void visit(Leaf leaf) {}
}
このような使い方は、デフォルト実装を提供するという意味で、defaultメソッドの本来の意図から外れないのではないかと思います。
まとめ
インターフェースのdefaultメソッドは、デフォルト実装を持つという点を除けば、他の抽象メソッドと同様に捉えてオブジェクト指向設計らしく使うのがよいでしょう。