SOLIDの原則を心で理解する - 依存性逆転の原則
この記事は、「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