はじめに
この記事は、SOLID原則のまとめ の続きです。
今回は、SOLID原則のひとつである Interface Segregation Principle(インターフェース分離の原則)について、歴史的背景から実務での適用パターンまで、コード例を交えながら解説します。
Interface Segregation Principle(ISP)とは
ISP は 「クライアントが利用しないメソッドへの依存を強制してはならない」 という原則です。
つまり、1つの大きなインターフェースを作るのではなく、クライアントが必要とするメソッドだけを持つ小さなインターフェースに分割すべきということです。このような分割されたインターフェースは ロールインターフェース(Role Interface) とも呼ばれます。
歴史的背景: Xerox の事例
ISP は Robert C. Martin が Xerox のプリンターシステム開発のコンサルティング中に定式化した原則です。
当時の Xerox のソフトウェアでは、1つの巨大な Job クラスが印刷・ステープル・FAX・スケジューリングなど、あらゆる処理を担当していました。この「太ったクラス(Fat Class)」のせいで、どんなに小さな変更でも再デプロイに1時間かかるようになり、開発がほぼ不可能な状態に陥っていました。
Martin の解決策は、Job クラスのインターフェースを StapleJob・PrintJob・FaxJob などのクライアントごとのインターフェースに分離することでした。Job クラスはこれらすべてを実装しますが、各クライアントは自分が必要とするインターフェースだけに依存します。
ISP に違反した例
例1: 太ったインターフェース(Fat Interface)
動物を表すインターフェースを考えます。
<?php
interface Animal {
public function eat();
public function fly();
public function swim();
}
class Dog implements Animal {
public function eat() {
echo "犬がご飯を食べる\n";
}
public function fly() {
// 犬は飛べないが、実装を強制される
throw new Exception('犬は飛べません');
}
public function swim() {
echo "犬が泳ぐ\n";
}
}
class Eagle implements Animal {
public function eat() {
echo "鷹がご飯を食べる\n";
}
public function fly() {
echo "鷹が飛ぶ\n";
}
public function swim() {
// 鷹は泳がないが、実装を強制される
throw new Exception('鷹は泳げません');
}
}
Animal インターフェースが eat・fly・swim のすべてを要求するため、飛べない Dog や泳がない Eagle にも不要なメソッドの実装が強制されています。これが ISP 違反です。
例2: 複合機のインターフェース
実務でよくある例として、複合機のインターフェースがあります。
<?php
interface MultiFunctionDevice {
public function print(string $document);
public function scan(string $document): string;
public function fax(string $document, string $number);
}
// 印刷しかできないプリンター
class SimplePrinter implements MultiFunctionDevice {
public function print(string $document) {
echo "印刷: {$document}\n";
}
public function scan(string $document): string {
throw new Exception('スキャン機能は非対応です');
}
public function fax(string $document, string $number) {
throw new Exception('FAX機能は非対応です');
}
}
SimplePrinter は印刷しかできないのに、scan と fax の実装を強制されています。これは Xerox の事例と同じ問題構造です。
ISP に準拠した例
例1の改善: ロールインターフェースで分離
インターフェースを能力ごとに分離します。
<?php
interface Eatable {
public function eat();
}
interface Flyable {
public function fly();
}
interface Swimmable {
public function swim();
}
class Dog implements Eatable, Swimmable {
public function eat() {
echo "犬がご飯を食べる\n";
}
public function swim() {
echo "犬が泳ぐ\n";
}
}
class Eagle implements Eatable, Flyable {
public function eat() {
echo "鷹がご飯を食べる\n";
}
public function fly() {
echo "鷹が飛ぶ\n";
}
}
class Duck implements Eatable, Flyable, Swimmable {
public function eat() {
echo "アヒルがご飯を食べる\n";
}
public function fly() {
echo "アヒルが飛ぶ\n";
}
public function swim() {
echo "アヒルが泳ぐ\n";
}
}
各クラスは自分が実際に持つ能力のインターフェースだけを実装すれば良くなりました。
例2の改善: 機能ごとにインターフェースを分離
<?php
interface Printable {
public function print(string $document);
}
interface Scannable {
public function scan(string $document): string;
}
interface Faxable {
public function fax(string $document, string $number);
}
// 印刷のみ
class SimplePrinter implements Printable {
public function print(string $document) {
echo "印刷: {$document}\n";
}
}
// 全機能対応の複合機
class AllInOnePrinter implements Printable, Scannable, Faxable {
public function print(string $document) {
echo "印刷: {$document}\n";
}
public function scan(string $document): string {
return "スキャン結果: {$document}";
}
public function fax(string $document, string $number) {
echo "FAX送信: {$document} → {$number}\n";
}
}
SimplePrinter は Printable だけを実装し、不要な scan や fax の実装は不要になりました。クライアント側も、印刷だけが必要なら Printable 型で受け取れば十分です。
ISP 違反のサイン
以下のような兆候が見られたら、ISP 違反を疑ってみてください。
- メソッドの空実装や
throw new NotImplementedException()がある - インターフェースのメソッド数が多く、実装クラスの多くが一部のメソッドしか使わない
- インターフェースを変更すると、関係ない実装クラスにまで影響が及ぶ
- 1つのインターフェースが異なる関心事のメソッドを混在させている
ISP と SRP の関係
ISP と SRP(単一責務の原則)は密接に関連しています。
- SRP はクラスの責務を分離する原則
- ISP はインターフェースの責務を分離する原則
どちらも「関心事の分離」という同じ目標に向かっていますが、異なる視点からアプローチしています。ISP に違反している太ったインターフェースは、多くの場合 SRP にも違反しています。
Adapter パターンによる既存コードへの適用
既存のコードが太ったインターフェースに依存している場合、すべてを書き直すことは現実的ではないかもしれません。このような場合、Adapter パターンを使って段階的に ISP を適用できます。
<?php
// 既存の太ったインターフェースを持つクラス
class LegacyMailer {
public function send(string $to, string $subject, string $body) { /* ... */ }
public function receive(): array { /* ... */ }
public function delete(int $id) { /* ... */ }
public function search(string $query): array { /* ... */ }
}
// 送信だけが必要なクライアント向けのインターフェース
interface MailSender {
public function send(string $to, string $subject, string $body);
}
// Adapter で分離
class MailSenderAdapter implements MailSender {
public function __construct(private LegacyMailer $mailer) {}
public function send(string $to, string $subject, string $body) {
$this->mailer->send($to, $subject, $body);
}
}
既存の LegacyMailer を変更せずに、クライアントが必要とする最小限のインターフェースを提供できます。
まとめ
ISP は「インターフェースは小さく、具体的に」という原則です。太ったインターフェース(Fat Interface)を避け、クライアントが本当に必要とするメソッドだけを持つロールインターフェースに分割することで、不要な依存を減らし、変更に強い設計を実現できます。既存コードに対しても、Adapter パターンを使って段階的に適用可能です。