制御反転(Inversion of Control) を理解するには、まずソフトウェア設計の重要な思想である依存性逆転原則(Dependency Inversion Principle) を理解することが必要だと思います。
依存性逆転原則(Dependency Inversion Principle) とは何でしょうか?例えば、車を設計するとします。最初に車輪を設計し、その車輪の大きさに基づいてシャーシを設計します。その後、シャーシに基づいて車体を設計し、最後は車体に基づいて車全体を設計します。ここで「依存関係」が発生しています:車は車体に依存し、車体はシャーシに依存し、シャーシは車輪に依存しています。
このような設計は一見問題なさそうですが、メンテナビリティが非常に低いです。例えば、設計が完了した後、上司が市場の需要に応じて車輪のサイズを大きくするように言ってきたとします。この場合、非常に困った状況です。なぜなら、車輪のサイズに基づいてシャーシを設計し、シャーシに基づいて車体を設計し、車体に基づいて車全体を設計しているからです。車輪のサイズを変更すると、シャーシの設計を変更しなければならず、シャーシの設計が変更されると、車体の設計も変更しなければならず、同様に車全体の設計も変更しなければならないため、ほぼすべての設計を変更しなければならないのです。
今度は別のアプローチを取ります。まず、車の全体像を設計し、その後車体を設計し、車体に基づいてシャーシを設計し、最後はシャーシに基づいて車輪を設計します。この方法では、依存関係が逆になります:車輪はシャーシに依存し、シャーシは車体に依存し、車体は車全体に依存します。
このように、上司が車輪の設計変更を要求した場合、私たちは車輪の設計のみを変更すれば済みます。底盤や車体、車全体の設計を変更する必要はありません。
ここで疑問を持つ方もいるかもしれませんが、第二の方法で依存性を逆転させても、底盤を設計した後で車輪を変更する必要がある場合、底盤も変更する必要があるでしょう。
ただし、ここでは概念的な例を挙げているだけです。この方法により、底盤がさまざまなサイズの車輪に柔軟に対応できるようになったと考えることができます。
これが依存性逆転原則です。この原則では、元々の上位層が下位層に依存していた関係を逆転させ、下位層が上位層へ依存するようにします。上位層は必要なものを決定し、下位層はそれを実現しますが、上位層は下位層の実装の詳細については関知しません。これにより、前述のような「一部を変更すれば全体を変更する必要がある」という問題が回避されます。
制御反転(Inversion of Control) は、依存性逆転原則の一種であり、コード設計のアプローチです。具体的な実装方法は依存性注入(Dependency Injection) と呼ばれます。これらの概念に初めて触れると、かなり混乱するかもしれません。要するに、これらの概念の関係はおおよそ以下のようになります。
これらの概念を理解するために、前の車の例を使いますが、今回はコードに置き換えます。車、車体、シャシー、タイヤの4つのクラスを定義します。その後、車を初期化し、最後にその車を走らせます。
このようにすると、上記の最初の例と同じく、上位の構造が下位の構造に依存します。すなわち、各クラスのコンストラクタは、下位のコードのコンストラクタを直接呼び出します。例えば、タイヤ(Tire)クラスの変更が必要で、そのサイズを静的ではなく動的にしたい場合、常に30であるのではなく変更が必要です。次のように変更します:
タイヤの定義を変更したため、プログラム全体が正常的な動作するように、以下の変更が必要です。
ここからわかるように、タイヤのコンストラクタを変更するだけで、この設計では上位のすべてのクラスのコンストラクタを変更する必要があります!ソフトウェアエンジニアリングでは、このような設計はほとんど保守不能です。実際のプロジェクトでは、あるクラスが数千のクラスの下位になることもありますが、そのクラスを変更するたびにそれを依存するすべてのクラスを変更しなければならないなら、ソフトウェアの保守コストはあまりにも高くなります。
したがって、私たちは制御反転(IoC) を行う必要があります。つまり、上層が下層を制御するのです、下層が上層を制御するのではありません。ここで依存性注入(Dependency Injection) という方法を使ってこれを実現します。依存性注入とは、下層のクラスを上層のクラスのパラメータとして渡し、上層のクラスが下層のクラスを「制御」する仕組みを実現することです。ここでは、コンストラクタを通じた依存性注入の方法 を使って、車のクラス定義を書き直します:
ここでは、2つの重要なポイントが言及されています:
制御反転(IoC): 上層が下層を制御するのです
依存性注入(Dependency Injection): 下層のクラスを上層のクラスのパラメータとして渡し、上層のクラスが下層のクラスを「制御」する仕組みを実現することです
ここではXML配置ファイル方式で実現します。
ここでタイヤのサイズを動的にするために、同様にシステム全体が順調的な実行するように、次のような変更が必要です。
見ましたか?ここではタイヤクラスだけを変更すればよく、他の上位クラスを一切変更する必要はありません。 これは明らかに保守しやすいコードです。それだけでなく、実際のプロジェクトでは、このような設計パターンは異なるグループの協力や単体テスト にも有利です。例えば、この4つのクラスをそれぞれ異なるグループが開発する場合、インターフェースを定義するだけで、4つの異なるグループが互いに制約を受けずに同時に開発を進めることができます。また、単体テストの場合、Carクラスの単体テストを書くには、FrameworkクラスをMockしてCarに渡すだけでよく、Framework, Bottom, Tireをすべて新たに作成してからCarを構築する必要はありません。
ここではコンストラクタ注入 を用いて依存性注入を行っていますが、実際には他に2つの方法もあります。Setter注入とインターフェース注入です。ここでは詳細は述べませんが、基本的な考え方は同じで、すべて制御反転を実現するためのものです。
ここまで見ていると、あなたは制御反転(IoC)と依存性注入(DI)について理解しているはずですね。それでは、IoCコンテナとは何でしょうか? 実際、先ほどの例で車のクラスを初期化する部分は、IoCコンテナが行う作業です。
当然、依存性注入を採用すると、初期化の過程で多くの new を避けることは避けられません。ここでIoCコンテナがこの問題を解決します。このコンテナは、自動的にコードを初期化し、上位クラス初期化ごとに大量のコードを手動で書く必要がなく、一つConfiguration(XMLまたはコード、ここにXMLで使用される)を管理するだけで済みます。これがIoCコンテナを導入する最初の利点です。
IoCコンテナの第2の利点は、インスタンスの作成時にその詳細を知る必要がないことです。上記の例では、自分で車のインスタンスを手動で作成するとき、下位から上位に向かってnewします。
このプロセスでは、Car / Framework / Bottom / Tire クラスのコンストラクタがどのように定義されているかを理解し、ステップバイステップでnewしたり、インジェクションしたりする必要があります。
IoCコンテナはこの作業を逆に行います。最上位から依存関係を探し始め、最下位に到達した後、ステップバイステップで上に向かってnewしていきます(深さ優先探索のようなものです)。
ここでIoCコンテナは具体的なインスタンス作成の詳細を隠すことができ、私たちが見るところでは工場のように見えます。
私たちは工場の顧客のようです。工場にCarのインスタンスをリクエストするだけで、それがConfigに基づいてCarのインスタンスを作成してくれます。私たちはそのCarのインスタンスがどのようにして作成されるのかを全く気にする必要はありません。
実際のプロジェクトでは、10年前に作成されたServiceクラスがあり、その下に数百のクラスが存在することがあります。新しく作成したAPIがこのServiceをインスタンス化する必要がある場合、数百のクラスのコンストラクタを理解することはできませんよね? このような問題をIoCコンテナの特性が完璧に解決します。このアーキテクチャでは、クラスを書く際に対応するConfigファイルを書く必要があるため、昔のServiceクラスを初期化する必要がある場合、前任者がすでにConfigファイルを作成しているので、必要な場所で直接このServiceを注入するだけです。これにより、プロジェクトの保守性が大幅に向上し、開発の難易度が低減します。
以上は、わたくしがIoC(Inversion of Control、制御反転)とDI(Dependency Injection、依存性注入)について理解していることです。ご質問や異論があれば、コメントをお願いします。