依存性逆転の原則
オブジェクト指向設計において、依存性逆転の原則、または依存関係逆転の原則[1](dependency inversion principle) とはソフトウエアモジュールを疎結合に保つための特定の形式を指す用語。この原則に従うとソフトウェアの振る舞いを定義する上位レベルのモジュールから下位レベルモジュールへの従来の依存関係は逆転し、結果として下位レベルモジュールの実装の詳細から上位レベルモジュールを独立に保つことができるようになる。この原則で述べられていることは以下の2つである:[2]
- A. 上位レベルのモジュールは下位レベルのモジュールに依存すべきではない。両方とも抽象(abstractions)に依存すべきである。
- B. 抽象は詳細に依存してはならない。詳細が抽象に依存すべきである。
この上位レベル、下位レベルの両方とも抽象に依存しなければならないと言う要求により、この設計原理は一部の人々のオブジェクト指向プログラミングに対する考え方を反転させる。[3]
この原理について述べた上記のポイント A と B の背景にある考えは、上位モジュールと下位モジュールの相互作用を設計する際に、これらの関係が抽象的な相互作用として捉えられるべきであるということであり、これによって上位モジュールだけでなく下位レベルモジュールも設計的な影響を受けることになる。下位モジュールは相互作用を念頭に設計されるべきで、その結果自身を使用するためのインターフェースについて変更を行う必要が生じる可能性がある。多くの場合、相互作用を抽象的な概念として捉えることにより追加のコーディングパターンを導入せずにコンポーネント間の結合を減らすことができ、より軽量で実装への依存が少ない総合作用スキーマを用いるだけで済むようになる。
2つのモジュールの間で見出された抽象的な相互関係が一般性を持ち、かつ一般化が意味を持つとき、この設計原理は以下の依存性の逆転コーディングパターンを導く。
従来のレイヤーパターン
伝統的なアプリケーションアーキテクチャーにおいて、下位レベルコンポーネント(e.g. Utility Layer)はより複雑なシステムの構築を可能にする上位レベルコンポーネント(e.g. Policy Layer)によって使用される形で設計がおこなわれる。この方法では上位レベルコンポーネントは直接下位レベルコンポーネントに依存する。この低レベルコンポーネントへの依存は、上位レベルコンポーネントの再利用の機会を制限してしまう。[2]
依存性逆転パターンの目指すところは、抽象レイヤーを導入することによってこの高度に結合した状態を回避し、上位の policy layer の再利用性を高めることにある。
依存性逆転パターン
抽象レイヤーを加える事により、上位レベルレイヤーと下位レベルレイヤーの両方とも top から bottom に向かう従来の依存関係を減らす事ができるが、"反転" の概念は下位レベルレイヤーが上位レベルレイヤーに依存することを意味しない。両方のレイヤーは上位レベルレイヤーが要求する振る舞いを表現した抽象に依存すべきである。
依存性逆転の直接のアプリケーションでは、抽象は上位/policy レイヤーによって所有される。このアーキテクチャーでは上位/policyコンポーネントと下位サービスを規定する抽象レイヤーを同一のパッケージとして扱う。低レベルレイヤーはこれらの抽象クラスやインターフェースを継承して生成される。[2]
依存性とオーナーシップの逆転は上位/policyレイヤーの再利用性を高め、上位レイヤーは他の低レベルサービスを利用することができるようになる。もし低レベルレイヤーがクローズドなコンポーネントであったり、アプリケーションが既存のサービスを再利用する必要がある場合、サービスと抽象レイヤーの間を仲介するAdapterを設けるのが一般的である。
依存性逆転パターンの一般化
多くのプロジェクトにおいて依存性逆転の原則とパターンは一般化されるべき概念であると考えられる。これには少なくとも以下の2つの理由がある。
- 優れた考えの原則をコーディングパターンとみなすほうがよりシンプルである。ひとたび抽象クラスやインターフェースが実装されるとプログラマーは「自分は抽象化のための仕事をした」と言うかもしれない。
- 多くのユニットテストツールがモックを作成するためにインターフェースに依存しているため、クラス間のジェネリックなインターフェースを利用することは(モジュール間に限った話ではなく一般的にも)ルールになっている。
もし、インターフェースのみに依存するモック作成ツールを使用している場合、一般化された依存性逆転パターンが必要になることがあるが、これには大きな欠点がある。
- クラスに対して単純にインターフェースを実装するだけでは不十分であり、一般的に結合を減らすことにはならない。相互作用に対しての潜在的な抽象化を考える事が唯一結合を減らす設計につながり得る。
- ジェネリックなインターフェースをプロジェクト内の全ての箇所で実装してしまうと、理解してメンテナンスをするのが非常に難しくなる。ソースコードを読む人は全てのステップにおいて「このインターフェースの他の実装はなんだろうか」と自問することになり、そしてその答えはほとんど「モックだけ」ということになりかねない。
- インタフェースの一般化はより "plumbing code" である事が要求され、一般的に依存性注入フレームワークに依存するファクトリーなどが特にそれにあたる。
- インターフェースの一般化はプログラミング言語の利用も制限する。
一般化における制約
依存性逆転のパターン(DIP)を達成するためにインターフェースが存在することは、オブジェクト指向プログラムにおいて他の設計上の制約をもたらす:
- クラス内の全てのメンバー変数はインターフェース、もしくは抽象でなくてはならない。
- 全ての具象クラスパッケージはインターフェース、もしくは抽象クラスパッケージを通してのみ結合されなければならない。
- 具象クラスを派生してはならない。
- 既に実装済みのメソッドをオーバーライドしてはならない。[2]
- 全ての変数のインスタンス化においてFactory MethodもしくはFactory パターンのような生成に関するパターンの実装が必要になる。もしくはDIフレームワークが必要になる。
インターフェースモック化の制約
継承ベースのモック化ツールを使用した場合でも以下の制約が生じる。
- 外部公開されている静的メンバーも体系的に依存性性注入されるべきだが、そのための実装はかなり難しい。
- テスト可能な全てのモジュールはインターフェースの実装、もしくは抽象定義のオーバーライドを行う必要がある。
将来的な方向性
原則は考えるための方法であり、パターンは問題解決のための共通手段である。コーディングパターンはプログラミング言語に欠如している機能であるとみなされるかもしれない。
- プログラミング言語は少なくとも2つの方向で、その使用においてより正確に、より強力になる方向に進化を続けるだろう。ひとつは使用条件の強化(事前、事後、そして不変条件)、もう一つは状態ベースのインターフェースである。これらは多くの状況において、より強力な依存性逆転の応用の促進し、潜在的に単純化に寄与するだろう。
- 静的メンバーや非仮想メンバーの置き換えの問題を解決するために、今やより多くのモック化ツールがコード注入を用いるようになっている。プログラミング言語は "mocking-conpatible" なバイトコードを生成するように今後進化するかもしれない。 一つの方向性は非仮想メンバーの使用を制限するというもの。そしてもう一つは、少なくともテストを行う状況においては、非継承ベースのモック化が可能なバイトコードを生成するというものだ。
実装
DIPの2つの一般的な実装では、さまざまな意味でよく似た論理アーキテクチャを使用する。
直接的な実装において、 plicy クラスと service 抽象クラスは一つのライブラリーにパッケージ化される。この実装では上位コンポーネントと下位コンポーネントは別々のパッケージ/ライブラリとして配布される。上位レベルコンポーネントによって要求されるされる振る舞い/サービスを定義したインターフェースは上位レベルコンポーネントによって所有され、上位レベルコンポーネントと同じライブラリの中に存在する。
上位レベルコンポーネントのインターフェースは下位レベルコンポーネントによって実装されるため、コンパイル時に下位レベルコンポーネントが上位レベルコンポーネントに依存する事が必要になる。よって従来の依存関係は逆転する。
Figures 1 and 2 では同じ機能を実現したコードを表現している。しかし、 Figure 2 ではインターフェースは依存性を逆転させるために使用されている。policy コードの再利用性を最大化したり、循環依存を排除するために依存の方向は選択することができる。
このバージョンの DIP では、下位レイヤーコンポーネントが上位レベルレイヤーのインターフェース/抽象に依存しているため、下位レベルレイヤーの再利用は困難になる。 この実装はその変わりに伝統的な to-to-bottom の依存関係を反対の bottom-to-top へと逆転させる。
抽象コンポーネントをライブラリやパッケージから独立させて置くと、より柔軟性が増す。
全てのレイヤーを分離して個別のパッケージに置くことでどのレイヤーの再利用性も向上し、ロバストネス性とモビリティーを得ることができる.[2]
例
家系モジュール
ある家系システムでは人々の間の関係を第一レベル関係のグラフとして表現するかもしれません(父親/息子, 父親/娘, 母親/息子, 母親/娘, 夫/妻, 妻/夫...)。これは非常に効率的です(そして拡張も可能: 元夫/元妻, 法的保護者...)。
しかし、上位レベルモジュールでは家系をブラウズするためのより簡単な方法が必要になるかもしれません: 人には、子供、父親、母親、兄弟と姉妹(異母兄弟を含む含まないも合わせて)、祖父、祖母、伯父、伯母、従兄弟... などがいるかもしれません。
家系モジュールの使用法に応じて、共通の関係を明確で直接的な属性として(グラフを隠して)表示することは上位レベルモジュールと家系モジュールの間の結合を遥かに軽くでき、モジュールの使用になんの影響も与えることなく内部表現を完全に変更することを可能にします。またそれによって、家系モジュールに対し兄弟、姉妹(異母兄弟であるかどうか)の正確な定義を埋め込む事が可能になります... またそれは単一責務の法則を強制することにも繋がります。
最終的に、もし最初の一般化された拡張可能なグラフアプローチが一番拡張可能に思えるのであれば、家系モジュールの利用は更に特殊化及び単純化された関係の実装がアプリケーションに対して十分であることを示しているかもしれませんし、より効率的なシステムを作成する手助けになるかもしれません。
この例でのモジュール間の相互関係の抽象化は下位レベルモジュールのインターフェースの単純化だけでなく、より単純化された実装につながるかもしれません。
リモートファイルサーバークライアント
リモートファイルサーバー(FTP, cloud strage ...)に対するクライアントを実装しなければならないケースを想像してください。あなたはそのクライアントに抽象インターフェースを持たせる事を考えるでしょう。
- Connection/Disconnection (a connection persistence layer may be needed)
- Folder/tags creation/rename/delete/list interface
- File creation/replacement/rename/delete/read interface
- File searching
- Concurrent replacement or delete resolution
- File history management ...
ローカルファイル、リモートファイルの両方が同じ抽象インターフェースを提供する場合、ローカルファイルと完全に実装された依存性逆転パターンを使用するどんな上位レベルモジュールもローカルとリモートの区別なくファイルにアクセスすることが可能になります。
ローカルディスクは一般的にフォルダーを使用します、リモートストレージもフォルダーを使用するかもしれません(もしくはタグのみ、フォルダーとタグの両方など). もし可能であればそれらを統一する方法を決めておく必要があります。
リモートファイルに対しては、作成と置換のみを行う必要があるかもしれません(ローカルファイルに比べてリモートファイルのランダムアップデートはあまりに遅く、実装も難しくなる可能性があるので、リモートファイルの更新は意味をなさない)。リモートファイルに対して部分的な読み込みと書き込みを行う必要があるかもしれません(少なくともリモートファイルモジュールの内部では通信中断後のダウンロードとアップロードの再開をできるようにする必要がある)。しかし、ローカルキャッシュを利用している場合を除き、ランダムリードは適しません。
ファイル検索は "pluggable" であるかもしれません: ファイル検索は OS、特にタグや全文検索に依存し、異なるシステムでも実装する事ができます。(OS に組み込まれていたり、別に入手する事も可能) 並行実行される置換または削除の検出は、他の抽象インタフェースに影響を与える可能性があります。
概念的なそれぞれのインターフェースに対してリモートファイルサーバーを設計するときは、上位レベルモジュールが要求するサービスのレベル (必ずしも全てが要求されるわけではない)を良く考える必要があります。そして、どのようにリモートファイルサーバーの機能を実装するかだけでなく、アプリケーション内に既に存在するファイルサービス(ローカルファイル、既にあるクラウドクライアントなど)と新たしいリモートファイルサーバークライアントの間で整合を取るかについても同様に考える必要があります。
必要なインターフェースの設計が完了したら、リモートファイルサーバークライアントはこれらのインターフェースを実装すべきです。
また、既に存在するローカルファイルに対しての機能 (例えばファイル更新など)について制限を加えたい場合、同じ抽象インタフェースを提供するローカルまたは他の既存のリモートファイルアクセスモジュール用のアダプタを記述する必要があるかもしれません。また、コンピュータ上に構成されている利用可能なファイル互換システムを検索するために独自のファイルアクセス列挙子を記述しておく必要もあります。
これらが完了すると、アプリケーションはドキュメントをローカルとリモートに透過的に保存する事が可能になります。さらに簡単に言うと、新しいファイルアクセスインターフェースを使用している上位レベルモジュールは使用の際にローカルとリモートファイルにアクセルするシナリオを意識する必要がなくなり、再利用性が向上します。
注意: 多くの OS がこの手の機能を実装し始めているため、新規実装のクライアントをこの既に存在している抽象モデルへ適用するのは控えた方が良いかもしれません。
この例では、モジュールを抽象インターフェースのセットとして考え、このインターフェースのセットに他のモジュールを適合させることで、様々なファイルストレージシステムに対し、共通のインターフェースを提供する事ができます。
Model View Controller
UI とアプリケーションレイヤーパッケージは主に具象クラスを含んでいて、コントローラーは抽象/インターフェース型を含んでいます。また UI は ICustomerHandler のインスタンスを保持し、全てのパッケージは物理的に分離されていて。アプリケーションレイヤーには Page クラスが使用する具象クラスの実装が存在しています。これらのインターフェースのインスタンスは (コントローラーと同じパッケージに存在するかもしれない)Factory によって動的に生成されます。具象タイプである Page と CustomerHandler はお互いに依存してはらなず、両方とも ICustomerHandler に依存します。
これらの直接的な効果は、UI が直接 ApplicatonLayer や、ICustomerHandler を実装したどの具象パッケージも参照する必要がない事です。コンクリートクラスはリフレクションを使用してロードされます。またどの時点であっても、具象実装は UI クラスに変更を及ぼさずに他の具象実装に差し替える事ができます。他の興味深い可能性は Page クラスが ICustomerHander のメソッドに引数として渡す事ができるインターフェース IPageViewer を実装していると言う事で、これによってt具象実装は具象的な依存なしに UI と通信する事ができます。何故なら両者はインターフェースでリンクされているからです。
関連するパターン
依存性逆転の原則の適用はアダプターパターンの一例と見ることもできます。例えば、上位レベルクラスが自身が依存する抽象への固有のアダプターインターフェースを定義するような場合がそれに当たります。アダプティーの実装はまたアダプターインターフェスに依存しますが(もちろん、このインターフェースを実装しているので)独自の下位レベルモジュール内のコードを用いて実装を行うこともできます。上位レベルモジュールは、アダプティーとその下位レベルモジュールによって実装されたインターフェースへのポリモーフィックな関数を呼び出すことによってアダプターインターフェースを介して間接的に下位レベルモジュールを利用するため、上位レベルモジュールが下位レベルモジュールに依存するといったことはありません。
Plugin, Service Locator, or Dependency Injection などの様々なパターンは上位レベルコンポーネントに対する選択された下位レベルコンポーネントの "run-time provisioning" を容易にする目的で導入されます。
History
The dependency inversion principle was postulated by Robert C. Martin and described in several publications including the paper Object Oriented Design Quality Metrics: an analysis of dependencies,[4] an article appearing in the C++ Report in May 1996 entitled The Dependency Inversion Principle,[5] and the books Agile Software Development, Principles, Patterns, and Practices, and Agile Principles, Patterns, and Practices in C#.
脚注
- ^ ロバート・C・マーチン『アジャイルソフトウェア開発の奥義 第2版』SBクリエイティブ、163頁。ISBN 978-4-7973-4778-4。
- ^ a b c d e Martin, Robert C. (2003). Agile Software Development, Principles, Patterns, and Practices. Prentice Hall. pp. 127–131. ISBN 978-0135974445
- ^ Freeman, Eric; Freeman, Elisabeth; Kathy, Sierra; Bert, Bates (2004). Hendrickson, Mike; Loukides, Mike. eds (paperback). Head First Design Patterns. 1. O'REILLY. ISBN 978-0-596-00712-6 2012年6月21日閲覧。.
- ^ Martin, Robert C.. “Object Oriented Design Quality Metrics: An analysis of dependencies”. 2016年10月15日閲覧。
- ^ Martin, Robert C. (1996年5月). “The Dependency Inversion Principle”. C++ Report. 2011年7月14日時点のオリジナルよりアーカイブ。 Template:Cite webの呼び出しエラー:引数 accessdate は必須です。
関連項目
- Adapter パターン
- 依存性の注入
- 契約プログラミング
- インタフェース (情報技術)
- 制御の反転
- プラグイン
- Service locator pattern
- SOLID:the "D" in "SOLID" stands for the dependency inversion principle
外部リンク
- Object Oriented Design Quality Metrics: an analysis of dependencies Robert C. Martin, C++ Report, Sept/Oct 1995
- The Dependency Inversion Principle, Robert C. Martin, C++ Report, May 1996
- Examining the Dependency Inversion Principle, Derek Greer
- DIP in the Wild, Brett L. Schuchert, May 2013
- IoC Container for Unity3D – part 2