インターフェース分離とは何か
インターフェース分離(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");
}
}
JavaSimplePrinter は「印刷だけできればよい」のに、
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 さえあれば何でもできる」状態になっているときも、インターフェース分離の出番です。
コントローラごとに「本当に必要な操作」だけをまとめた小さなインターフェースを定義し、
それを実装する形にすると、依存がだいぶマシになります。
インターフェース分離がもたらすメリット(重要なまとめ)
インターフェースを分離すると、次のような良さが出てきます。
クラスは「自分が本当に使うメソッドだけ」を実装すればよくなる。
変更が入っても、本当に関係のあるクラスだけが影響を受ける。
テストがしやすくなる(小さいインターフェースごとにフェイク実装を作れる)。
読むときに「このインターフェースは何をさせたいのか」が明確になる。
逆に、大きなインターフェースをそのままにしておくと、
「使わないメソッドのせいで、関係ない変更に巻き込まれる」というツラさが続きます。
インターフェースを設計するときは、
「このインターフェースを使う側から見て、全部のメソッドが本当に必要か?」
と自問してみてください。
もし「一部のクライアントはこの半分しか使わないな」と感じたら、
そこがインターフェース分離の候補です。
