保守性と拡張性を高めるためのSOLID原則とシステム設計
第10話:依存関係逆転の原則(2)依存関係の設計
(最終更新日:2024.10.14)
(絵が小さい場合はスマホを横に)
既存コードを変えずに、機能を追加できる設計を!
依存関係逆転の原則(Dependency Inversion Principle, DIP)は、ソフトウェア設計において保守性と拡張性を実現するための重要な原則だ。
この原則は、高レベルモジュールが低レベルモジュールに依存するのではなく、抽象に依存する設計を推奨する。
これにより、システムは具体的な実装に左右されず、柔軟で変更に強い構造になる。
DIPを適用することで、新しい機能やモジュールを追加する際にも既存のコードを変更せずに済み、効率的な開発と保守が可能になる。
今回は、DIPの設計方法と良い例、悪い例について、具体的なコードを通じて解説する。
1.高レベルモジュールと低レベルモジュールの依存関係の設計
本項では、依存関係逆転の原則(Dependency Inversion Principle, DIP)を実現するために、 高レベルモジュール(システムの主要なロジックや振る舞いを実行する)と低レベルモジュール(具体的な実装や外部リソースとのやり取りを行う)が どのように依存関係を持つべきかについて説明する。 この原則に従うことで、システムの保守性と拡張性を高め、柔軟なアーキテクチャを構築できる。
共通のインターフェースに依存させる
1.1 高レベルモジュールと低レベルモジュールの関係性
依存関係逆転の原則は、次の2つのルールに基づく。
- 高レベルモジュール:アプリケーションのビジネスロジックや主要な機能を担う部分。例えば、注文処理のロジックや顧客管理の機能など。
- 低レベルモジュール:DBアクセス、ファイルの読み書き、外部API呼び出しなど、具体的な処理やリソースに依存する部分。
従来の設計では、高レベルモジュールが低レベルモジュールに直接依存することが一般的だ。 このような依存関係は、低レベルモジュールの変更が高レベルモジュールに直接影響を与えるため、保守性と拡張性が低下する。
1.2 依存関係逆転の原則を適用する方法
DIPを適用するために、以下のステップを踏むことで、高レベルモジュールが低レベルモジュールに直接依存しないように設計する。
A. インターフェースを使用した依存関係の抽象化
まず、高レベルモジュールと低レベルモジュールが共通して依存できる抽象(インターフェースまたは抽象クラス)を定義する。
このインターフェースは、低レベルモジュールが提供する機能の仕様を定義し、高レベルモジュールはこの仕様に依存する。
インターフェースの定義
このインターフェースを定義することで、高レベルモジュール(OrderService)はOrderRepositoryインターフェースに依存し、
具体的な低レベルモジュール(例えば、データベースやファイル保存など)には依存しなくなる。
B. 低レベルモジュールの実装
次に、低レベルモジュールはこのインターフェースを実装します。
これにより、依存関係が低レベルモジュール側で実装され、具体的な処理内容はカプセル化される。
低レベルモジュールの実装
このように、異なる実装(データベース、ファイル保存など)を複数用意できるが、
どの実装を使うかはOrderServiceには影響しない。
C. 依存性注入(Dependency Injection, DI)によるインスタンスの注入
OrderServiceなどの高レベルモジュールでは、OrderRepositoryインターフェースを依存関係として注入することで、具体的な実装には依存しない。
これにより、インターフェースに基づいた柔軟な設計が可能になる。
高レベルモジュールの実装
OrderServiceは、具体的なDatabaseOrderRepositoryやFileOrderRepositoryのいずれかを知らず、 OrderRepositoryインターフェースにのみ依存している。 この構造により、低レベルモジュールの変更があっても、高レベルモジュールに影響を与えることはない。
1.3 依存関係逆転のメリット
A:拡張性の向上
新しい低レベルモジュール(例えば、新しいデータベースシステムや異なる保存方法)を追加する際、OrderRepositoryインターフェースを実装するだけで済む。
高レベルモジュールはそのまま機能するため、既存のコードを修正する必要がない。
B. 保守性の向上
高レベルモジュールは具体的な低レベルモジュールに依存しないため、低レベルモジュールの変更があっても、システム全体に影響を及ぼさない。
これにより、各モジュールが独立してメンテナンス可能になる。
C. テストのしやすさ
高レベルモジュールが抽象に依存しているため、テスト時にはモック(Mock)オブジェクトを簡単に差し替えることができる。
例えば、OrderRepositoryのモックを作成し、OrderServiceの振る舞いを独立してテストできるため、ユニットテストが容易になる。
テスト用のモックの導入
1.4 高レベルモジュールと低レベルモジュールの依存関係設計のベストプラクティス
インターフェースを中心に設計:
具体的なクラスの実装ではなく、インターフェースや抽象クラスを依存関係の基礎とし、抽象を介して依存を構築する。
依存性注入(Dependency Injection)を活用:
コンストラクタ、セッター、またはフレームワークを利用して、インターフェースの具体的な実装を注入する。
これにより、高レベルモジュールが直接具体的なクラスを生成するのを避けらる。
低レベルモジュールの変更を局所化:
抽象に依存することで、低レベルモジュールの変更が他のモジュールに波及しないように設計する。
これにより、変更が必要な場合でも影響範囲を限定できる。
1.5 まとめ
高レベルモジュールと低レベルモジュールの依存関係設計において、DIPを適用することで、柔軟性、保守性、拡張性が向上する。
高レベルモジュールが直接低レベルモジュールに依存せず、インターフェースや抽象クラスに依存するように設計することで、
システムは変更に対して安定し、異なる実装を容易に追加・変更できるアーキテクチャを構築できるようになる。
2.良い例と悪い例
本項では、DIPを守った設計と、守っていない設計の違いを具体的に示し、システム設計における保守性と拡張性の観点からその効果を解説する。 この対比を通じて、DIPを適用することの重要性とその効果を具体的に説明する。
2.1 悪い例:依存関係逆転の原則を守っていない設計
まず、DIPを守っていない設計の例を示す。この例では、高レベルモジュール(OrderService)が、低レベルモジュール(EmailService)に直接依存する。
悪い例
このコードの問題点は3つある。
1つ目は、OrderServiceはEmailServiceに依存しているため、他の通知サービス(例えば、SMS通知やプッシュ通知)を利用したい場合、
OrderServiceを直接修正しなければならなくなる。
2つ目は、新しい通知手段を追加する際に、高レベルモジュールであるOrderServiceを変更する必要があり、設計の柔軟性が低下する。
3つ目は、OrderServiceはEmailServiceを内部で直接生成しているため、モック(Mock)オブジェクトに置き換えられない。
その結果、OrderServiceのテストを実施する際に、実際のメール送信処理が発生する可能性がある。
2.2 良い例:依存関係逆転の原則を適用した設計
この設計では、OrderServiceがNotificationServiceというインターフェースに依存しており、具体的な通知手段(EmailServiceやSMSService)には依存していない。
良い例
このコードは先ほどの3点を全て修正している。
1つ目は、低レベルモジュール(EmailServiceやSMSService)を変更・追加しても、高レベルモジュール(OrderService)には影響を与えない。
新しい通知手段を追加する際も、OrderServiceを変更する必要はなく、新しいクラスをインターフェースに従って実装するだけで済む。
2つ目は、OrderServiceが異なる通知手段を受け入れるため、他のコンテキストでも再利用しやすくなる。
3つ目は、OrderServiceは依存性注入(Dependency Injection)を使ってNotificationServiceを受け取るため、
テスト時には簡単にモックオブジェクトを渡してテストできる。これにより、実際のメール送信処理を行わずにテストが実施できる。
モックの導入
2.3 悪い例と良い例の比較
悪い例と良い例の比較を表にまとめると、以下のようになる。
項目 | 悪い例 | 良い例 |
---|---|---|
拡張性 | 新しい機能を追加するたびにクラスを変更しなければならない | 新しい支払い方法を追加しても既存のコードを変更せずに済む |
再利用性 | 支払い方法ごとに個別のメソッドを持つため、コードの重複が発生しやすい | インターフェースで統一されたメソッドを使うことで、コードを再利用しやすい |
保守性 | クラスが巨大化し、修正が複雑になる | 各支払い方法ごとに独立したクラスで処理されるため、保守が容易 |
OCP(開放/閉鎖原則) | クラスは変更に対して閉じられていないため、変更のたびに既存のコードを編集する必要がある | 拡張に対して開かれており、既存のクラスを変更せずに新しい機能を追加できる |
ポリモーフィズム | ポリモーフィズムが使われていないため、コードが一貫性を持たない | インターフェースを使用して異なる実装を共通のインターフェースで扱える |
2.4 まとめ
悪い例では、高レベルモジュールが低レベルモジュールに直接依存しているため、システムの保守性と拡張性が低下し、
柔軟性が制限されている。また、テストも困難で、システムの変更に強くない。
良い例では、DIPに基づいて高レベルモジュールが抽象(インターフェース)に依存するよう設計されている。
この設計により、低レベルモジュールの変更や拡張が容易になり、システム全体が柔軟で保守性の高い構造になっている。
テストも容易になり、変更の影響が局所化されるため、開発と保守が効率的に行うことができる。
DIPを適用することで、システムの変更に強く、保守性と拡張性を高める設計が可能になる。
これにより、開発者は新たな機能追加や変更に対して効率的かつ安全に対応できる。
長期的に信頼性の高いソフトウェア開発が可能になる。