Java | オブジェクト指向:インターフェース分離

Java Java
スポンサーリンク

インターフェース分離とは何か

インターフェース分離(Interface Segregation Principle, ISP)は、
「インターフェースは、小さくて、はっきりした役割ごとに分けなさい」
という原則です。

もっと噛み砕くと、

ひとつの大きなインターフェースに「あれもこれも」押し込まない。
クライアント(使う側)に「使わないメソッド」まで実装させない。

という考え方です。

あるクラスが「不要なメソッドのために、わざわざ変な実装を書かされている」なら、
そのインターフェースはデカすぎて、分離すべきサインです。


なぜインターフェースを分ける必要があるのか(重要)

インターフェースが大きくなると、次のような問題が起きます。

関係のない機能まで同じ契約に入ってくる。
一部だけ使いたいクラスも、全部のメソッドを実装しないといけない。
誰かがインターフェースにメソッドを追加すると、関係ないクラスまで修正が必要になる。

つまり「変更の影響範囲が無駄に広がる」のです。

インターフェース分離のゴールは、
「それぞれのクライアントが、本当に必要としているメソッドだけを持ったインターフェースに依存できる状態」にすること。
必要な役割だけを小さく分けることで、クラス同士の結合を弱くし、変更に強くします。


悪い例:デカすぎるインターフェース

まずは、分離できていない「悪い例」から見てみます。

interface Machine {
    void print(String content);
    void scan();
    void fax(String number);
}
Java

一見それっぽく見えますが、問題は「役割が混ざっている」ことです。
とにかく全部 “Machine” に押し込んでしまっています。

例えば、次のクラスを考えます。

final class SimplePrinter implements Machine {
    @Override
    public void print(String content) {
        System.out.println("PRINT: " + content);
    }

    @Override
    public void scan() {
        // この機械はスキャン機能を持たないので、どうする?
        throw new UnsupportedOperationException("scan not supported");
    }

    @Override
    public void fax(String number) {
        // FAX も無い…
        throw new UnsupportedOperationException("fax not supported");
    }
}
Java

SimplePrinter は「印刷だけできればよい」のに、
Machine を実装したせいで scan と fax も無理やり実装させられています。

「UnsupportedOperationException を投げる」という “ダミー実装” を書かされている時点で、
そのインターフェースは役割過多であり、分離できていないサインです。


良い例:役割ごとにインターフェースを分割する(重要な深掘り)

先ほどの Machine の例を、役割ごとに分けてみます。

interface Printer {
    void print(String content);
}

interface Scanner {
    void scan();
}

interface Fax {
    void fax(String number);
}
Java

これで、「印刷」「スキャン」「FAX」という三つの役割に分けました。

印刷だけできるプリンターは Printer だけを実装します。

final class SimplePrinter implements Printer {
    @Override
    public void print(String content) {
        System.out.println("PRINT: " + content);
    }
}
Java

複合機なら、必要な役割を複数 implements すればよいです。

final class MultiFunctionMachine implements Printer, Scanner, Fax {
    @Override
    public void print(String content) { /* 実装 */ }

    @Override
    public void scan() { /* 実装 */ }

    @Override
    public void fax(String number) { /* 実装 */ }
}
Java

こうすると、

印刷だけ使うクライアントは Printer にだけ依存すればよい。
スキャンだけ使うクライアントは Scanner にだけ依存すればよい。

「使わないメソッドにまで引きずられない」状態を作れます。

これがインターフェース分離の本質です。
クライアントごとに、必要な契約だけを切り出してあげるイメージです。


クライアント視点でインターフェースを考える

インターフェース分離が難しくなるのは、「提供側の都合だけでインターフェースを作ってしまう」時です。
大事なのは「誰がこのインターフェースを使うのか?」というクライアント視点です。

例えば「ユーザを扱うサービス」があるとします。

interface UserService {
    User findById(String id);
    void create(User user);
    void delete(String id);
    void exportAllToCsv(java.io.OutputStream out);
}
Java

これも一見普通ですが、「exportAllToCsv」はかなり特殊な責務です。
ユーザの登録や削除だけしたいコードまで、このメソッドを持つ UserService に依存する必要はありません。

そこで、こう分けてみます。

interface UserReader {
    User findById(String id);
}

interface UserWriter {
    void create(User user);
    void delete(String id);
}

interface UserExporter {
    void exportAllToCsv(java.io.OutputStream out);
}
Java

これに対する実装は、1 クラスでも複数クラスでも構いません。

final class UserService implements UserReader, UserWriter, UserExporter {
    @Override public User findById(String id) { /* … */ }
    @Override public void create(User user) { /* … */ }
    @Override public void delete(String id) { /* … */ }
    @Override public void exportAllToCsv(java.io.OutputStream out) { /* … */ }
}
Java

読み取りだけ必要なクライアントは UserReader にだけ依存すればよく、
書き込みだけ必要なクライアントは UserWriter にだけ依存すれば済みます。

「クライアントごとに、必要な操作セットをインターフェースとして切り出す」
これがインターフェース分離を考えるときの一番大事な視点です。


実務でよくある「違反パターン」とその直し方

なんでも入りの Repository インターフェース

よくあるのが「全部入り Repository」です。

interface UserRepository {
    User findById(String id);
    java.util.List<User> findAll();
    void save(User user);
    void delete(String id);
    void exportCsv(java.io.OutputStream out);
}
Java

シンプルに見えますが、「exportCsv」はかなり特殊です。
ほとんどのクライアントは、読み書きだけできればよいはずです。

分離するなら、例えばこうです。

interface UserQueryRepository {
    User findById(String id);
    java.util.List<User> findAll();
}

interface UserCommandRepository {
    void save(User user);
    void delete(String id);
}

interface UserExportRepository {
    void exportCsv(java.io.OutputStream out);
}
Java

実装クラスは 1 つでもかまいませんが、依存側が「自分に関係ないメソッド」を見ずに済むようになります。

コントローラから見える「サービスがデカすぎる」パターン

サービスインターフェースが巨大で、コントローラが「UserService さえあれば何でもできる」状態になっているときも、インターフェース分離の出番です。

コントローラごとに「本当に必要な操作」だけをまとめた小さなインターフェースを定義し、
それを実装する形にすると、依存がだいぶマシになります。


インターフェース分離がもたらすメリット(重要なまとめ)

インターフェースを分離すると、次のような良さが出てきます。

クラスは「自分が本当に使うメソッドだけ」を実装すればよくなる。
変更が入っても、本当に関係のあるクラスだけが影響を受ける。
テストがしやすくなる(小さいインターフェースごとにフェイク実装を作れる)。
読むときに「このインターフェースは何をさせたいのか」が明確になる。

逆に、大きなインターフェースをそのままにしておくと、
「使わないメソッドのせいで、関係ない変更に巻き込まれる」というツラさが続きます。

インターフェースを設計するときは、
「このインターフェースを使う側から見て、全部のメソッドが本当に必要か?」
と自問してみてください。

もし「一部のクライアントはこの半分しか使わないな」と感じたら、
そこがインターフェース分離の候補です。

タイトルとURLをコピーしました