🐲

SOLIDの原則を心で理解する - 依存性逆転の原則

2024/07/14に公開

この記事は、「SOLIDの原則を心で理解する」に関するシリーズの締めくくりとして、その最後の原則であり、重要な「依存性逆転の原則(DIP: Dependency Inversion Principle)」について解説します。

これはおそらく5つの原則の中で最も重要なものであり、単一責任の原則(Single Responsibility Principle)リスコフの置換原則(Liskov Substitution Principle)とともに、とても優れたアーキテクチャパターンである基礎を形成しています。

高レベルのモジュールと低レベルのモジュールについての説明は以下の通りです。

高レベルのモジュール

これらのモジュールは通常、ビジネスロジックやアプリケーションのオーケストレーション(調整)を含みます。複雑なタスクの調整やビジネスルールの適用を担当することが多いです。

低レベルのモジュール

これらのモジュールは特定の機能や実装の詳細を提供します。例えば、データベースアクセスのためのクラスや外部サービス、フレームワークなどが含まれます。

依存性逆転の原則(DIP)を適用することにより、高レベルのモジュールが直接低レベルのモジュールに依存しないようにします。代わりに、これらのモジュールは、低レベルのモジュールが提供する必要な機能を表現する抽象(インターフェースや抽象クラス)に依存します。

つまり、具体的な実装の詳細は低レベルのモジュールに委ねられ、一方で高レベルのモジュールは抽象にのみ依存して相互作用するということです。

SOLID原則の他の例と同様に、ここでの目的はアプリケーションの異なる部分間の結合を減らし、システムをより柔軟にすることです。抽象が維持される限り、実装の詳細を変更しても高レベルのモジュールに影響を与えないためです。

依存性逆転の原則(DIP)の違反例

依存性逆転の原則(DIP)が適用されていない例を考えてみましょう。
例では、通知サービス (NotificationService) が電子メールを送信するクラス (EmailSender) に直接依存しています。

class EmailService {
  sendEmail(to: string, subject: string, message: string) {
    // 高度で難解なコード
  }
}

class NotificationService {
  private emailService: EmailService

  constructor() {
    this.emailService = new EmailService()
  }

  sendNotification(user: User, message: string) {
    const userEmail = user.getEmail()
    this.emailService.sendEmail(userEmail, 'Notification', message)
  }
}

この例では、NotificationServiceクラスがEmailServiceクラスに直接依存しています。

これらのクラスは強く結合されており、高レベルのモジュールであるNotificationServiceが低レベルのモジュールであるEmailServiceに直接依存しています。

言い換えれば、これは依存性逆転の原則(DIP)に対する明らかな違反(🚨👮✋)のケースです。
なぜなら、NotificationServiceが抽象ではなく具体的な実装に依存しているからです。

例の何が問題なのか?🤔

このコード例にはいくつかの問題があります。主な問題は以下の3つです。

高結合度

EmailServiceクラスに直接依存しているため、NotificationServiceクラスはこの特定の実装に強く結びついています。これは、メールの送信方法を変更する(たとえば、別のメールプロバイダーを使用する、またはログ機能を追加する)場合に、NotificationServiceクラス自体を修正しなければならないことを意味し、システム全体に予期せぬ影響を与える可能性があります。

テストの難しさ

NotificationServiceクラスがEmailServiceに直接依存しているため、独立してテストするのが難しくなります。テスト時には、EmailServiceを簡単にダミーのテスト実装に置き換えて、メール送信の振る舞いをシミュレーションすることができません。

依存性の隠蔽

クラス内で依存関係を直接インスタンス化することは、カプセル化の原則を破ります。クラスは通常、自分の動作に責任を持つべきであり、依存関係の生成方法については考慮すべきではありません。依存関係を注入することで、依存関係の生成を使用から分離し、コードのモジュール性と保守性を向上させます。

依存性逆転の原則(DIP)の適用

前述の問題を解決し、依存性逆転の原則(DIP)を遵守するために、EmailService(および他の実装)に対する抽象を導入することができます。以下はその具体的な方法です。

interface NotificationSender {
  send(to: string, subject: string, message: string): void
}

class EmailService implements NotificationSender {
  send(to: string, subject: string, message: string) {
    // 高度で難解なコード
  }
}

class NotificationService {
  private readonly notificationSender: NotificationSender

  constructor(sender: NotificationSender) {
    // 依存関係が注入されている
    this.notificationSender = sender
  }

  sendNotification(user: User, message: string) {
    const userEmail = user.getEmail()
    this.notificationSender.send(userEmail, 'Notification', message)
  }
}

NotificationServiceは具体的な実装EmailServiceではなく、抽象 NotificationSenderに依存しています。

このようにすることで、コードはより柔軟になり、EmailServiceを別の実装に置き換えることが容易になります。
例えば、SMSやプッシュ通知を送る実装に置き換えることができます。

まとめ

依存性逆転の原則(DIP)はソフトウェア設計において重要な要素です。具体的な実装ではなく抽象に依存することを推奨することで、コンポーネント間の結合を緩め、柔軟性と拡張性を高めます。
このアプローチは複雑性を軽減し、保守を容易にし、コードの再利用性を促進します。

Discussion