Web制作、Web開発の歩き方

保守性と拡張性を高めるためのSOLID原則とシステム設計

第9話:依存関係逆転の原則(1)依存関係逆転の原則とは

(最終更新日:2024.10.10)

SOLID原則とシステム設計
この記事は4分で読めます!
(絵が小さい場合はスマホを横に)

依存関係逆転の原則で保守性と拡張性を実現する

依存関係逆転の原則(Dependency Inversion Principle, DIP)は、システム設計において保守性と拡張性を高めるための重要なアプローチだ。 この原則では、高レベルモジュールと低レベルモジュールが直接依存するのではなく、共通の抽象に依存するよう設計する。 これにより、低レベルモジュールの変更が高レベルモジュールに影響を与えず、新たな機能の追加や既存の機能変更が容易になる。 システム全体の柔軟性が向上し、長期的な保守が効率的に行えるアーキテクチャを構築できる。


1.依存関係逆転の原則とは

依存関係逆転の原則(Dependency Inversion Principle, DIP)は、オブジェクト指向設計におけるSOLID原則の一つで、 特に依存関係の構築方法に焦点を当てた設計原則だ。 この原則は、高レベルモジュールが低レベルモジュールに依存するのではなく、抽象に依存すべきであると示す。 これにより、システムの保守性と拡張性が向上し、変更に強い設計が実現される。


1.1 依存関係逆転の原則の基本的な定義
依存関係逆転の原則は、次の2つのルールに基づく。

  1. 高レベルモジュールは低レベルモジュールに依存してはならない。両者は抽象に依存すべきである。
  2. 抽象は詳細に依存してはならない。詳細が抽象に依存すべきである。

これを守ることで、システム全体の構造が柔軟になり、変更に対して安定した設計が可能となる。 高レベルモジュールとは、システムの主要なロジックや振る舞いを定義する部分のことだ。 低レベルモジュールは、具体的な実装やハードウェア依存の部分になる。 この原則に従い、高レベルモジュールが低レベルモジュールに直接依存しないようにする。

1.2 依存関係逆転の原則が必要な理由
従来の設計方法では、 高レベルモジュール(ビジネスロジック)と低レベルモジュール(データベースアクセスやAPI呼び出し)には強い結びつきがある。 この結びつきにより、低レベルモジュールの変更(データベースの種類を変更するなど)が高レベルモジュールに影響を与える。 このような依存関係が生じると、以下の問題が発生する。

  1. 保守性の低下: 低レベルモジュールの変更が高レベルモジュールに影響を及ぼし、変更箇所が広範囲にわたる可能性がある
  2. 拡張性の欠如: 新しいモジュールや機能を追加する際に、既存のコードを大幅に変更しなければならなくなる場合がある

依存関係逆転の原則を適用することで、これらの問題を解決し、システムの保守性と拡張性を向上させることができる。

分割されたインターフェース

1.3 具体例で見る依存関係逆転の原則
A:依存関係逆転の原則を適用しない場合(悪い例)
以下は、依存関係逆転の原則を適用していない設計の例だ。 高レベルモジュールであるOrderServiceクラスが、低レベルモジュールであるEmailServiceに直接依存している。

悪い例

このコードの問題点は、高レベルモジュール(OrderService)が低レベルモジュール(EmailService)に直接依存しているため、 EmailServiceの変更がOrderServiceに影響を与える。例えば、別のメールサービス(SMS通知やプッシュ通知など)に変更したい場合、 高レベルモジュールを変更しなければならず、保守性と拡張性が低くなる。

B. 依存関係逆転の原則を適用した場合(良い例)
依存関係逆転の原則に基づいて設計をリファクタリングし、 インターフェースを導入することで、OrderServiceが抽象(インターフェース)に依存するように変更する。

良い例

このコードの良い点は、抽象に依存(OrderServiceはNotificationServiceというインターフェースに依存)しており、 具体的なEmailServiceには依存しない。これにより、異なる通知サービス(例えば、SMS通知など)に容易に切り替えることができる。 つまり、低レベルモジュール(EmailServiceやSMSService)の実装を追加・変更しても、高レベルモジュールのOrderServiceには影響を与えない。

C. 別の通知サービス(SMS通知)の追加
別の通知サービスを追加する際、NotificationServiceインターフェースを実装するだけで、OrderServiceには変更を加えずに拡張できる。 このように、インターフェースを介して依存関係を抽象化することで、システムの保守性が大幅に向上し、拡張も簡単に行える。

SMS通知の追加拡張

1.4 依存関係逆転の原則を守る設計のベストプラクティス
インターフェースや抽象クラスを活用:
高レベルモジュールが直接低レベルモジュールに依存しないよう、インターフェースや抽象クラスを設計に導入する。

依存性注入(Dependency Injection)を使用:
コンストラクタやセッターを使って依存関係を注入することで、クラスが具体的な実装に依存しないようにする。

モジュール間の結合度を下げる:
高レベルと低レベルのモジュールが疎結合になるように設計し、変更が他のモジュールに影響しないようにする。

1.5 まとめ
依存関係逆転の原則(DIP)は、高レベルモジュールが低レベルモジュールに依存するのではなく、 両者が抽象に依存することで、保守性と拡張性を実現する。 この原則を守ることで、システム全体が柔軟で変更に強い構造になり、新しいモジュールの追加や既存モジュールの変更が容易になる。 また、設計がシンプルで一貫性のあるものとなり、将来的なメンテナンスがしやすくなる。

2.具体は抽象に依存する考え方

「具体は抽象に依存する考え方」は、DIPの中心的な概念で、システムの設計を柔軟かつ拡張可能にするために重要だ。 この考え方は、システムの高レベルモジュールと低レベルモジュールがどのように依存するべきかを示し、抽象度を利用して依存関係を管理する。 具体的か抽象的かは、一般的なもので言うと、下記のような例が挙げられる。食べ物がより抽象的で、寿司が料理名なのでやや具体的、 さらに鉄火巻やアナゴ握りは更に具体的になる。寿司は食べ物に依存し、鉄火巻やアナゴ握りは寿司に依存するという関係性が望ましい。 鉄火巻やアナゴ握りに依存してしまうと、他の寿司ネタが作れないからだ。

具体が抽象に依存する一般例

2.1 具体は抽象に依存するとは
従来の設計では、高レベルのモジュール(ビジネスロジック)は、 低レベルのモジュール(データベースアクセス、外部サービス、ファイルシステム)に直接依存していた。 しかし、こうした直接的な依存関係があると、低レベルモジュールの変更が高レベルモジュールに影響を与えてしまい、保守性が低下する。

そこで、DIPでは「高レベルモジュールも低レベルモジュールも、抽象(インターフェースまたは抽象クラス)に依存すべきであり、 具体的な実装(クラス)に依存してはならない」としている。 これにより、具体的な実装の変更が抽象に隠蔽され、システム全体の変更に強い構造が実現される。

2.2 具体が抽象に依存する設計のメリット
保守性の向上:
低レベルモジュールの変更が高レベルモジュールに直接影響を与えないため、保守が容易になる。

拡張性の確保:
新しい具体的な実装(例:新たなDB、別の通知サービス)を追加する際、抽象に依存しているため、 既存の高レベルモジュールに変更を加えずに済む。

テストの容易さ:
具体的な依存が抽象に隠蔽されているため、テスト時にモック(Mock)やスタブ(Stub)などの代替実装を簡単に挿入できる。

2.3 具体が抽象に依存する設計(悪い例)
具体が抽象に依存する設計の考え方を、コード例を通じて説明する。

例: 依存関係逆転の原則に従っていない設計
以下の例では、OrderServiceがDatabaseServiceという具体的な実装に直接依存している。

悪い例

2.4 具体が抽象に依存する設計(良い例)
依存関係逆転の原則に基づいて、OrderServiceが抽象(インターフェース)に依存するように設計を変更しました。 OrderServiceはOrderRepositoryというインターフェース(抽象)に依存しており、具体的な実装(DatabaseOrderRepository)には依存していない。 そして、例えば、ファイルシステムに保存する新しい実装やクラウドに保存する実装を追加する際、OrderServiceのコードを変更する必要はない。 新しいクラスをOrderRepositoryインターフェースに従って実装するだけで、システムに拡張が可能になる。

良い例

2.5 新しい実装の追加(ファイルシステムへの保存)
上記のクラスを利用して、新たな機能の実装を追加した例を紹介する。 新しい実装を追加した場合でも、OrderServiceは既に抽象に依存しているため、変更を加えることなく、この新しい実装を利用できる。

新しい実装の追加

2.5 依存関係逆転の原則を活用するためのベストプラクティス
インターフェースや抽象クラスを活用:
高レベルモジュールと低レベルモジュールが依存するものをインターフェースまたは抽象クラスとして定義し、具体的な実装がこれらに従う形で設計する。

依存性注入(Dependency Injection, DI)を使用:
依存性注入(DI)を活用して、クラスが必要とする依存オブジェクトを外部から注入することで、 クラス内部で具体的な実装を生成しないようにする。

疎結合を促進する設計:
具体的なクラス間の結合度を下げ、各モジュールが独立して動作できるように設計することで、拡張性と保守性を高める。

2.6 まとめ
「具体は抽象に依存する」という考え方は、依存関係逆転の原則の根幹だ。 この原則を守ることで、システム全体が変更に強く、保守性と拡張性が高まる。 具体的なクラス(低レベルモジュール)が直接依存するのではなく、共通の抽象(インターフェースや抽象クラス)を通じて依存関係を管理することで、 設計が柔軟かつ拡張可能になる。 また、抽象化によってテストも容易になり、モジュールごとの変更が他のモジュールに影響を与えることなく、システムの安定性が向上する。