設計原則 レッスン3

インターフェース分離と依存性逆転(ISP/DIP)

肥大化したインターフェースを分割するISPと、上位モジュールが抽象に依存するDIPの実践方法を学ぶ

インターフェース分離の原則(ISP)とは

インターフェース分離の原則(Interface Segregation Principle)は、 「クライアントは、自分が使わないメソッドへの依存を強制されるべきではない」という原則です。 1つの巨大なインターフェースにあらゆるメソッドを詰め込むと、 一部の実装しか必要としないクラスにも不要なメソッドの実装を強制してしまいます。

// 悪い例: 印刷・スキャン・FAXすべてを1つのインターフェースに詰め込む
interface MultiFunctionDevice {
  print(doc): void
  scan(doc): void
  fax(doc): void
}

// シンプルなプリンターなのに、使わないメソッドの実装を強制される
class SimplePrinter implements MultiFunctionDevice {
  print(doc) { /* 印刷処理 */ }
  scan(doc) { throw new Error("この機種はスキャンに対応していません") }
  fax(doc) { throw new Error("この機種はFAXに対応していません") }
}
// 良い例: 機能ごとにインターフェースを分割する
interface Printer {
  print(doc): void
}

interface Scanner {
  scan(doc): void
}

interface FaxMachine {
  fax(doc): void
}

// 必要なインターフェースだけを実装すればよい
class SimplePrinter implements Printer {
  print(doc) { /* 印刷処理 */ }
}

class OfficeMultiDevice implements Printer, Scanner, FaxMachine {
  print(doc) { /* 印刷処理 */ }
  scan(doc) { /* スキャン処理 */ }
  fax(doc) { /* FAX処理 */ }
}

ポイント

「使わないメソッドを呼ばれたら例外を投げる」実装が必要になっている場合は、 ISP違反のサインです。インターフェースを利用側の必要に応じて分割しましょう。

依存性逆転の原則(DIP)とは

依存性逆転の原則(Dependency Inversion Principle)は、 「上位モジュールは下位モジュールの実装の詳細に依存すべきではなく、 両者とも抽象(インターフェース)に依存すべき」という原則です。

// 悪い例: 上位モジュール(NotificationService)が具体的な実装に直接依存している
class EmailSender {
  send(message) { /* メール送信の実装 */ }
}

class NotificationService {
  private emailSender = new EmailSender()

  notify(message) {
    this.emailSender.send(message)
    // SMS通知を追加したくなると、このクラス自体を書き換える必要が出てくる
  }
}
// 良い例: 上位・下位の両方が抽象(インターフェース)に依存する
interface MessageSender {
  send(message): void
}

class EmailSender implements MessageSender {
  send(message) { /* メール送信の実装 */ }
}

class SmsSender implements MessageSender {
  send(message) { /* SMS送信の実装 */ }
}

class NotificationService {
  // 具体的な実装ではなく、インターフェースに依存する
  constructor(private sender: MessageSender) {}

  notify(message) {
    this.sender.send(message)
  }
}

// 呼び出し側で具体的な実装を注入する(依存性注入)
const service = new NotificationService(new SmsSender())

DIPと依存性注入(DI)の関係

DIPは「何に依存すべきか」という設計上の方針であり、依存性注入(Dependency Injection)は その方針を実現するための具体的な手段の1つです。 クラスの内部で具体的な実装をnewするのではなく、コンストラクタや関数の引数として 外部から渡す(注入する)ことで、DIPが求める「抽象への依存」を実現します。

DIPを守るメリット

  • • 実装を差し替えやすくなる(テスト時にモックを注入できる)
  • • 上位モジュールが下位モジュールの変更の影響を受けにくくなる
  • • 新しい実装(通知手段など)の追加がOCPの形で行いやすくなる

注意点

  • • すべてのクラスをインターフェース化すると、かえって間接参照が増え読みにくくなる
  • • 実装が1つしかなく変化する見込みもない箇所まで抽象化する必要はない

ポイント

  • • ISPは、クライアントに使わないメソッドへの依存を強制しないよう、インターフェースを分割する原則
  • • 「使わないメソッドで例外を投げる」実装はISP違反のサイン
  • • DIPは、上位モジュール・下位モジュールの両方が具体的な実装ではなく抽象に依存すべきという原則
  • • 依存性注入(DI)は、DIPを実現するための具体的な手段の1つ
  • • 変化する見込みのない箇所まで過度に抽象化すると、かえって読みにくくなる

確認クイズ

1 / 3

ISP違反が起きているサインとして代表的なものはどれか?