interfaceにdefaultメソッドを追加すると設計がどう変わるか?

Java 8で導入されたdefaultメソッドは、interfaceに実装を直接持たせる機能です。
もともとJavaのinterfaceは純粋に“契約のみを定義する”ものでしたが、defaultメソッドの登場によって、インターフェースでありながら実装を含めるという設計が可能になりました。

これは柔軟性をもたらす一方で、責務の分離や構造の明示性を損なうことにもつながります。
この記事では、defaultメソッドを設計に取り入れる際のメリット・落とし穴・レビュー観点を整理します。

defaultメソッドとは何か?

public interface Printer {
    void print(String content);

    default void printWithHeader(String content) {
        System.out.println("=== HEADER ===");
        print(content);
    }
}
構文の背景

元々はJava 8のStream APIのように、既存interfaceに後方互換性を保ったまま新機能を追加するために導入された。

技術的メリット──なぜ導入されたのか?

  • 既存のinterface利用者に影響を与えず、新たな振る舞いを追加できる
  • Utility的な振る舞いをinterface側にまとめられる
  • クラス階層の肥大化を防げる(必要な分だけ追加)

OKな使い方例:完全にユーティリティである場合

public interface Timer {
    void start();

    default void logStart() {
        System.out.println("Start at: " + System.currentTimeMillis());
    }
}
  • logStart()は状態に依存しない
  • 他の実装に影響を与えない
  • 利用側がoverrideする必然性も低い

問題の所在──責務がインターフェースに混ざる

1. インターフェースの契約が“行動仕様”を持ち始める

public interface UserRepository {
    User find(String id);

    default User findOrThrow(String id) {
        User user = find(id);
        if (user == null) throw new NotFoundException();
        return user;
    }
}

→ 「nullを許容するのか?」という仕様差異が実装レベルに入り込み、interfaceに残ってしまう

2. 実装クラス側の責務が不明瞭になる

public class CachedRepository implements UserRepository {
    @Override
    public User find(String id) {
        // キャッシュロジック
    }

    // findOrThrowはdefaultのまま
}

findOrThrowの仕様にキャッシュは絡んでいない
→ 実装責務の一貫性が保てない

3. テスト対象が複雑になる

  • default実装がどこに属しているか分かりにくい
  • モック時にdefaultメソッドを差し替えられない
  • interfaceの責務が大きくなりすぎる

結局、defaultメソッドはどういうときに“アリ”か?

判断軸 OKなケース NGなケース
後方互換性維持 既存APIへの追加 新規設計にdefault前提で導入
実装不要なユーティリティ printlnなどの補助関数 データベースアクセスなどの処理
状態や副作用を持たないか 引数のみで完結する処理 状態保存やDB操作を含む処理
責務が明確に分離されているか ログ・変換・検査など ビジネスロジックを含む処理

defaultメソッドの曖昧性

UML Diagram

レビュー観点:defaultメソッドの責務混在に注意

レビュー観点

defaultメソッドをinterfaceに追加している設計を見たら、次の観点を確認してください。

defaultメソッドは“便利な妥協”ですが、設計構造を壊すほどに頼るものではありません。

結論:defaultメソッドは「互換性の例外措置」であって、「拡張の手段」ではない

Javaのinterfaceにおけるdefaultメソッドは、既存構造の壊さないための折衷策であり、
それを前提にした設計構造を取るべきではありません。

  • 状態や副作用を含む処理はクラスに明示すべき
  • 振る舞いの拡張は抽象化によって行うべき
  • interfaceはあくまで契約の定義に留める