ARCHIVES
アーカイブス
アーカイブス

DIコンテナの本当の使いどころ

翔泳社『DBマガジン』2005年12月号「アプリケーション開発 そこが知りたい」

石川智久/高橋英一郎/山本啓二
2005年12月01日
※内容は公開当時のものです

DI の自由度は諸刃の剣

近ごろ、「実プロジェクトでDIコンテナ(注1)を導入している」という話をちらほら耳にするようになりました。それと同時に、「DIコンテナを使ったプロジェクトが大変なことになっている」という話も耳にするようになりました。DIの魅力を十分に享受して低コスト、高品質を実現しているプロジェクトがある一方で「DIを導入してみたのはいいのだけれど、DIの設定ファイルが大きくなりすぎて管理しきれない」「DIを使っているのに、テスタビリティが全然向上していない」など苦労しているプロジェクトもあるようです。この差はいったいどこから来るのでしょうか。

DIは、EJBなどと比べると比較的取っ付きやすい技術ではありますが、ほかの技術同様、誤った使い方では十分に力を発揮できません。DIコンテナは非常に単純明快な技術ではありますが、そのシンプルさ故に自由度が高くさまざまな使い方ができます。そのため、無作為に利用してしまうとかえってデメリットばかりになってしまいます。自由度が高いことの代償として、利用するためにはある程度のノウハウが必要となります。このノウハウをきちんと生かしているかどうかが、DIを上手に利用できているプロジェクトとできていないプロジェクトの差となっているようです。そこで今回は、DIを安全に使うために「DIの使いどころ」と「DIのアンチパターン」を説明したいと思います。なお、サンプルコードはSpringFrameworkを前提に記述しています。

DI の使いどころ

まずは、DIの典型的な利用パターンを3つ紹介しましょう。この3つ以外にもDIを活用できる場所はありますが、DIを初めて導入する場合は、まずこの辺りを足がかりにすると良いと思います。

ServiceLocator/Factory パターンの代わり

最もオーソドックスなDIコンテナの利用方法は、ServiceLocatorパターン(注2)の代わりです。要するに、「レイヤ間の結合にDIコンテナを利用する」というものです。通常、J2EEのWebアプリケーションはプレゼンテーションレイヤ、ビジネスロジックレイヤ、インテグレーションレイヤ(注3)の3層構造で構築されます。レイヤ間はメンテナンス性や再利用性を考慮して疎結合にするのが一般的ですので、ここがDIコンテナの使いどころとなります(図1)。

図1 DIコンテナでレイヤ間を接続

ステートレスセッションBeanやO/Rマッピングフレームワークなどを利用したJ2EE Webアプリケーションでは、プレゼンテーションレイヤとビジネスロジックレイヤの間をServiceLocatorパターンもしくはJNDI(JavaNaming and Directory Interface)で、ビジネスロジックレイヤとインテグレーションレイヤの間をFactoryパターンもしくはローカル呼び出しでつなぐのが一般的です(図2)。

図2 従来のJ2EE Web アプリケーションのレイヤ接続方法

図2 従来のJ2EE Web アプリケーションのレイヤ接続方法

しかしこの方法では、2つのレイヤ間の接続方法が異なるために管理が面倒であったり、設計次第ではビジネスロジックレイヤとインテグレーションレイヤ間が密結合になってしまうといった問題がありました。ここで、レイヤ間の接続にDIコンテナを利用すると、プレゼンテーションレイヤとビジネスロジックレイヤの間もビジネスロジックレイヤとインテグレーションレイヤの間も同じDIという機構を用いて接続することができ、管理が楽になります。また、レイヤ間接続の共通基盤としてDIコンテナを採用することで、各レイヤの呼び出し方法が共通化されるため、AOP(注4)の利用も実現しやすくなります。 また、DIコンテナはServiceLocatorパターンのほかにFactoryパターンを実装する代わりとしても利用できます。DIコンテナはオブジェクトの管理機能を持っており、クラス名からオブジェクトを生成したり、生成されたオブジェクトを管理したりすることができます。この機能を利用して、DIコンテナを汎用的なFactoryクラスとして利用することができます。Factoryパターンを実装する場合、処理の一部をDIコンテナに委譲することで、高機能なFactoryパターンを非常に簡単な実装で実現できます。

Singleton パターンの代わり

DIコンテナは、Singletonパターン(注5)を実装する代わりにもなります。DIコンテナは、DIを実現するためにオブジェクトの管理機能を持っています。このオブジェクトの管理機能を利用することで、オブジェクトの数をSingletonパターンのように1つだけに制限することができます。クラスをSingletonパターンで実装しなくても、実行時にDIコンテナを利用することで同様に扱うことが可能になります(図3)。

図3 DI コンテナによるSingleton の実現

図3 DI コンテナによるSingleton の実現

Singleton パターンは容易にオブジェクトを使いまわすことができるために気軽に利用されがちですが、実はさまざまな問題を引き起こします。JavaでSingletonパターンを実装する場合、通常唯一のオブジェクトをクラス変数(static変数)として保持します。クラス変数はClassLoader(注6)オブジェクトごとに確保されるため、複数のWAR(注7)を含むEAR(注8)を作成したりしてClassLoaderオブジェクトの階層構造が複雑になっている場合や、独自開発のClassLoaderクラスを使用する場合などでは、開発者の思わぬ誤解からバグの原因となることがあります。例えば、あるWAR 内のコードで知らず知らずのうちに別のWAR 内のクラス変数にアクセスしてデータを壊してしまう、といったことが起きたりします。また、Singletonパターンではコンストラクタをprivateにすることが多いため、実装したクラスを外部で再利用しにくい、ユニットテスト(単体テスト)を行ないづらいといった問題もあります。DIコンテナによるSingletonパターンの代行では、このような問題をなくして使い勝手が良くなっています。そのため、DIコンテナを使用している環境では、クラスをSingletonパターンで実装する前に、DIコンテナに任せられないかを一度考えてみると良いでしょう。ただし、DIコンテナ上で利用するクラスはコンストラクタをpublicにする必要があります。そのため、ほかの開発者が誤ってオブジェクトを1つ以上生成するようなコーディングをしないように、何らかの対策を施す必要があります。

Properties クラスの代わり

ある程度の規模のアプリケーションを開発する場合に必要になるのが、アプリケーション固有の設定情報の管理機能です。デバッグモード用フラグ、トランザクションタイムアウト時間、対外接続用コネクタの接続先など、多種多様な設定情報がアプリケーション固有の設定情報として必要になります。これら設定情報を管理するために多くのプロジェクトで利用されているのがPropertiesクラスです。PropertiesはJ2SE( Java2 StandardEdition)に標準で用意されているクラスで、キーと値のペアを簡単に管理できます。さらにそのデータをファイル(ストリーム)から読み込んだり、書き出したりする機能もサポートしています。そのため、ファイルでアプリケーション固有の設定情報を管理するのにうってつけです。 しかし、Propertiesクラスにも弱点があります。全角文字の扱いにクセがあることと、必ずキーと値のペアでデータを保持しなければならないことです。Propertiesクラスを利用してファイルからデータを読み込むには、native2asciiというツールを利用して、ファイル内にある全角文字をすべてUnicodeエスケープ(注9)しなければなりません(注10)(図4)。

図4 Unicode エスケープ

図4 Unicode エスケープ

これが意外とくせもので、デバッグや障害対応の際などに「アプリケーション内の設定ファイルをそのまま読めない」といった困った状況を生みます。また、Propertiesクラスはキーと値のペアのみでデータを管理するため、XML のようにデータを階層構造にすることはできません。そのため、多少でも複雑な構造を持つデータを保持するためには、アプリケーション側での対応が必要となります。Propertiesクラスの代わりにXMLを使用すればこれらの問題を解決できますが、DOMやSAXによるXML処理をコーディングするのは何かと面倒です。

そこで登場するのがDIコンテナです。ほとんどのDIコンテナはXMLデータとオブジェクトをマッピングする機能を持っているので、非常に簡単にXMLデータをオブジェクト化してくれます。多くのDIコンテナではオブジェクトのフィールド型に合わせてデータ形式を変換してくれますので、boolean値やint値などの取り扱いも簡単です。オブジェクトの初期化メソッドを指定できるDIコンテナを利用すれば、設定内容のチェックも簡単に行なえます。 また、前述のとおり、DIコンテナではSingletonパターンのようにオブジェクトを使いまわすことも可能ですので、設定情報を管理するクラスをSingletonパターンで設計せずに済みます。

さらに、DIコンテナを使うことにはうれしい副作用があります。DIコンテナは、設定ファイルに記述された文字列のクラス名からオブジェクトを生成する機能を持っています。そのため、リフレクション(注11)などを利用することなく、プラグインのような機構を簡単に実現することができます。アプリケーションの中でもメンテナンスされやすい場所や再利用したい場所をこの機能を使って差し替え可能にしておくと、設定ファイルの書き換えのみで機能を変更できるようになり、メンテナンスなどがぐっと楽になります。

DIコンテナは、現在最も注目度の高い技術の1つです。今年中に正式リリース予定のJ2EE 5.0では、EJB 3.0 にDI 技術が取り入れられます。Web アプリケーションの開発や保守の効率を高めてくれる技術に間違いありませんが、使い方を誤ればかえって現場を混乱に陥れます。今回は、DI コンテナを正しく認識し、最大の効果を得るための使いどころと、よく見られる誤った使用法(アンチパターン)について解説します。

DI のアンチパターン

ここまで述べてきたとおり、DIコンテナは非常に自由度の高い技術です。そのために、「こういう使い方もできてしまうけれど、できるだけ止めておいたほうが無難」というケースもあります。ここでは、そのような「できるだけ止めておきたい」使い方を4 つ紹介します。このパターンに当てはまっているからといって、即座にダメというわけではありませんが、いったん立ち止まって「本当にこれで大丈夫か」「本当にこうしないとダメか」「もっと安全な方法はないのか」と考えてみてください。「密結合は悪である」「疎結合は善である」という信念のもと、ありとあらゆるクラスの間を疎結合にしようという人がいます(図5)。

図5 疎結合だらけ

図5 疎結合だらけ

そういう人は、可能な限りクラス間を疎結合にするため、あらゆるクラスにインターフェイスを用意し、あらゆるクラスをDIコンテナに登録します。

問題点

本来DI を使用する必要のないところ、使用するべきでないところでもお構いなしにDIを使用するために、DIコンテナに登録されるクラス数が膨大になります。その結果、不要なインターフェイスが増え、DIコンテナの設定ファイルもものすごい量になって収拾がつかなくなります。また、DIコンテナの設定ファイルに依存関係のほとんどが記述されてしまうため、いわゆるプロパティコーディングの様相を呈してきます。多くの実装クラス間にDIコンテナが介在するためにソースコードの可読性が著しく低下し、当然デバッグも困難になります。

解決策

利用コストを理解した上で、DIを利用するようにしましょう。DIが必要とされるのは、通常疎結合が必要とされるところです。そして、アプリケーション内で疎結合が必要とされる箇所はごく限られています。そのため、DIコンテナに登録されるべきクラスはアプリケーション内のごく一部のクラスのはずです。アプリケーションの規模に比べてDIコンテナに登録されているクラス数が多い場合は、本来登録されるべきではないクラスが登録されてしまっている可能性があります。そのようなときは、本当に必要なところでのみDIが利用されているかどうか、見直してください。

DIを使用するコストとメリットはきちんと計りましょう。DIを使うとコード上に現われるのはインターフェイスのみとなり、実装クラスは隠蔽されます。IDE(統合開発環境)がサポートすれば別ですが、一見して実装がどのクラスであるのかが分かり難いため、コードを読んでいると「実装はどこにあるの?」と悩むことになります。そんな実装では、DIコンテナの設定ファイルも膨大で複雑怪奇に違いありません。結果として、コードの可読性やデバッグの容易性は著しく低下します。開発者がDIのコストを意識するようになれば、自然にDIの利用は必要最低限となっていくはずです。不必要なDIの利用を避けるため、システムアーキテクチャを規定する際にDIを用いるポイントを明確に定め、想定外の利用を禁止したり、フレームワークレベルでDIコンテナを隠蔽したりしても良いでしょう。

疎結合もどき

DIでインターフェイスを介在させずにクラス間の接続を行なうケースです。実装クラス同士を直接依存させるため、インターフェイスを作らない分だけ管理するソースコードは少なくなります。

問題点

DIを使っているため一見疎結合となっているように見えますが、実はあまり疎結合になっていません。そのため、疎結合を目的としてDIを使っているようなときに、このパターンにはまってしまうとDIを使っている意味がほとんどなくなってしまいます。LIST1 をご覧ください。

LIST1「疎結合もどき」の実装(DIコンテナ「SpringFramework」を前提に記述)

■ Client.java
public class Client {
private Injectee injectee;
public void execute() {
this.injectee.doSomething();
}
public void setInjectee(Injectee injectee) {
this.injecteee = injectee;
}
}
■ InjecteeImpl.java
public class InjecteeImpl {
public void doSomething() {
// 何か処理をする
}
}
■ beans.xml(※SpringFrameworkでの記述です)
<bean  class="Client">
<property name="injectee"><ref bean="injectee"/></property>
</bean>
<bean  class="InjecteeImpl"/>

このコードでは、Clientクラスのコンパイル時に実装クラスInjecteeImplがバインドされてしまいます。そのため、InjecteeImplを別の実装に差し替えることが面倒になります(図6)。

図6 実装の差し替えに面倒が発生

図6 実装の差し替えに面倒が発生

InjecteeImplが拡張を前提として設計されていない場合は特に困難で、InjecteeImplが継承できない設計になっていたりすると、完全にお手上げになってしまいます。これではユニットテストで実装をスタブに差し替えたりすることが難しくなり、DIの魅力半減です。

解決策

InjecteeImplクラスに対して、Injecteeインターフェイスを用意します。状況によっては抽象クラスでも構いませんが、その場合もインターフェイス+抽象クラス(抽象骨格実装)にできないかを検討してみましょう。ただし、状況によっては実装クラス同士を直接依存させたくなる場合もあります。例えば、ロジックをまったくと言って良いほど持たないJavaBeansをインジェクション(注入)する場合などは実装を隠蔽する意味があまりありませんので、インターフェイスの管理工数を考えて直接依存させたほうが良いかもしれません。この辺りは状況次第ですので、状況をきちんと把握して柔軟に判断してください。

依存性ロケータ

オブジェクトを利用する側が、DIコンテナから依存性を注入されるのを待つのではなく、自らDIコンテナへ問い合わせて依存性を取得します。要するに、ServiceLocator的なDIコンテナの利用方法です。特に、Apache Struts(注12 )のActionなどのように既存のフレームワークでオブジェクト管理が行なわれるクラスでは、DIコンテナにオブジェクトを管理させることが難しいために、この方法が用いられることが多くなります。

● 問題点

LIST2は依存性ロケータの問題を抱えているコード例、図7はその動きの一部を図にしたものです。

LIST2 依存性ロケータの問題を抱えているコード

■ Client.java
public class Client {
private Injectee injectee;
public void execute() {
this.injectee.doSomething();
}
}
■ Injectee.java
public interface Injectee {
void doSomething();
}
■ InjecteeImpl.java
public class InjecteeImpl implements Injectee, BeanFactoryAware {
private BeanFactory factory;
public void doSomething() {
Injectee2 injectee2 = this.factory.getBean("Injectee2");
injectee2.doNothing();
}
public void setBeanFactory(BeanFactory factory) {
this.factory = factory;
}
}
■ Injectee2.java
public interface Injectee2 {
void doNothing();
}
■ Injectee2Impl.java
public class Injectee2Impl implements Injectee2 {
public void doNothing() {
// 何か処理をする
}
}
■ beans.xml(※SpringFrameworkでの記述です)
<bean  class="Client">
<property name="injectee"><ref bean="injectee"/></property>
</bean>
<bean  class="InjecteeImpl"/>
<bean  class="Injectee2Impl"/>
図7 依存性の逆転

図7 依存性の逆転

InjecteeImplクラスで利用されているBeanFactoryAwareインターフェイスとBeanFactoryクラスは、ともにSpringFrameworkに用意されている依存性を取得するためのインターフェイス/クラスです。これにより、Injectee 型(実装はInjecteeImpl)の変数を宣言しているClientクラスが、SpringFrameworkのインターフェイスに依存してしまっています。通常、オブジェクト利用側がDIコンテナに依存する必要はなく、完全なPOJO(注13 )としての実装が可能です。しかし、LIST2のClientクラスはSpringFrameworkに依存し、POJOとは言えなくなっています。また、ClientクラスをユニットテストするときにはSpringFrameworkが必要となり、テスタビリティも低下しています。

解決策

既存のフレームワークによりオブジェクト管理が行なわれるようなクラスの場合、そのフレームワークのオブジェクト管理機構を拡張してD I コンテナと連携させたり、Proxyパターン(注14 )を利用してフレームワークとは別にオブジェクト管理を行なう機構を作り込むことにより、この問題を解決できます。有名なフレームワークの中には、DIコンテナ側でこのような解決策が用意されているものもあります。Seasar2というDIコンテナでは、Apache StrutsのAction向けに「S2Struts」というプロダクトが用意されています。実際の開発では、このようなServiceLocator 的なDIコンテナの利用が必要となるケースも往々にしてあります。とは言うものの、やはりオブジェクトを利用するクラスからDIコンテナへの依存はないに越したことはありません。どうしてもDIコンテナへの問い合わせが必要な場合は、DIコンテナへのアクセスを請け負うラッパークラスを別に用意する(ラッピングする)と良いでしょう。DIコンテナをラッピングしておくことで、ユニットテスト時などにDIコンテナのスタブを利用したテストが可能になり、テスタビリティが向上します。

網の目依存

DIコンテナ上で管理されているオブジェクト同士を、気の向くまま思うままに関連付けします。DIコンテナ上に管理されていないければ有り得ないような依存性であっても気にせず依存させるため、図8のように依存性が網の目のようになります。

図8 網の目のような依存関係

図8 網の目のような依存関係

問題点

依存性が網の目のようになる設計は、通常「ダメな設計」として実装前に避けられます。しかし、DIを利用すると時として知らず知らずの内に依存性が網の目のようになってしまっていることがあります。その原因としては、依存関係がソースコードとは別のファイル(DIコンテナの設定ファイル)に記述されるために、依存関係が見えにくいということが挙げられます。

またそれ以外にも、DIコンテナ上にクラスを登録するためにはクラスをpublicにしなければならなくなり、Java言語仕様レベルでのアクセス制限(アクセス識別子(注15 )を利用したクラス単位のアクセス制限)がしにくいことも挙げられます。DIコンテナの管理外のクラスは、クラスのアクセス識別子やパッケージで適切にアクセス制限を行なえていても、DIコンテナに管理させるクラスは、パッケージはテキスト表記で分かりづらく、すべてのクラスがpublicになっており、気が付いたら依存関係がとんでもないことに......。網の目のようになってしまった依存性を放置した結果は想像に難くないでしょう。

解決策

無条件にオブジェクト間の依存を許してしまうと、依存関係はすぐに網の目のようになってしまいます。開発を開始する前に依存性に関する規定を定め、DIコンテナ上で管理されるオブジェクト同士の依存関係を管理できる方法を用意します。依存関係の管理方法として最も簡単なのは、図9のように注入されるInjecteeインターフェイスをpacakge privateにしてアクセス制限を行なう方法です。

図9 DIコンテナ上のクラスの最も簡単な管理方法

図9 DIコンテナ上のクラスの最も簡単な管理方法

この方法である程度DIコンテナ上の依存関係を制限することは可能ですが、設計上の制約でInjecteeインターフェイスをpackage privateにできない場合があるといった問題もあります。また、Injecteeをpackage privateにできたとしても、DIコンテナの制約上、実装クラスであるInjecteeImplはpublicにせざるを得ませんので、完全に制限することはできません。DIコンテナがコンテナの親子関係をサポートしている場合、DIコンテナのオブジェクトを複数用意して親子関係を適切に管理することで、依存関係の管理を行なうことができます。ただし、この方法ではコンテナの管理が複雑になってしまいます。オブジェクト間の依存性に関する規定を用意し、その規定にDIコンテナの設定が沿っているかチェックする機能(バリデータ)を作成するという方法もありますが、開発者が規定を覚えなくてはなりません。この問題に関しては、プロジェクトの状況に応じて上記の解決策を組み合わせる必要があるでしょう。

おわりに

「DIの使いどころ」「DIのアンチパターン」をざっと紹介しましたが、いかがでしたでしょうか。新しい技術が出るごとに「○○を導入すれば(無条件に)☆☆が良くなる」などといった幻想をどうしても持ってしまうものです。DIも同様で、「DIコンテナを導入するだけで、より簡単に、より高品質なアプリケーションを構築でき」ればどれだけ幸せなことでしょう。しかし、残念なことに、現実はなかなか甘くはありません。無思慮にDIコンテナを導入するだけでは、かえって状況を悪化させてしまうことになりかねません。実際にDIコンテナを導入してみたものの思うように行かずにがっかり、となってしまっているプロジェクトもあることでしょう。どのような技術であっても使い方を誤るととんでもない結果になってしまいます。これからDIを導入される予定がある方は、DIをどう使うのかをじっくり考えてみてください。プロジェクトでDIの使い方をきちんと共有できれば、安全にDIを利用することができ、大いに役立つでしょう。

石川智久(いしかわともひさ)

元々はテーブル設計が得意分野だったが、より概念的な方向に興味を持ちはじめ、アナリシスパターン的な世界へ徐々に移行中。一方で、アーキテクトとしてプロジェクトに参加する機会も増え、自分が「アーキテクト」なのか「モデラー」なのか分からなくなっている。中堅SIer を経て、2002 年より現職。

高橋英一郎(たかはしえいいちろう)

Java によるWeb アプリケーション開発一筋で生計を立てている。現職では商用J2EE Webアプリケーションフレームワークの開発に従事。いかにして楽にWebアプリケーションを構築するか、日夜頭を悩ませている。

山本啓二(やまもとけいじ)

関東在住の阪神ファン兼コンサルタント。死のロード前の時点まで首位キープ。死のロードは避けられませんが、プロジェクトのデスマーチは避けたいものです。近著『プログラマの「本懐」』(日経BP 社)

dbmagazine-writers@ulsystems.co.jp

  • 注1:DI(Dependency Injection)は「依存性注入」などと訳され、主にクラス間の結合を緩く(疎結合に)するための技術。DI コンテナはその実行環境。
  • 注2:サービス利用者からの要求に従って、サービスを提供するオブジェクトを返す責務を持つクラスを表わすデザインパターン。
  • 注3:RDBMS などの外部アプリケーションとの連携を行なうレイヤ。
  • 注4:アスペクト指向プログラミング(Aspect OrientedProgramming)。複数のプログラムで実行する共通の処理を別途記述しておき、それを呼び出すタイミング/条件を設定して利用する。
  • 注5:クラスごとに1つのオブジェクトしか生成されないデザインパターン。複数スレッドから利用される場合でも、同一のオブジェクトが利用される。
  • 注6:Java バイトコードからクラス情報を読み込む責務を持つJ2SE 標準のクラス(java.lang.ClassLoader)。
  • 注7:Web ARchive の略。Web アプリケーションを格納するために策定されたJar の拡張規格。
  • 注8:Enterprise ARchive の略。エンタープライズアプリケーションを格納するために策定されたJar の拡張規格。
  • 注9:"u"に続けてUnicode のコードを付記する文字の表記方法。例えば、"あ"をUnicode エスケープすると"u3042"となる。
  • 注10:native2asciiを実行しながら読み込むストリームを用意する手もありますが......。
  • 注11:クラスやメソッド、フィールドといった情報を扱い、オブジェクトの生成などの処理を動的に行なうためのAPI。例えば、"jp.co.ulsystems.Sample"といった文字列からSample クラスのオブジェクトを生成することができる。
  • 注12:Apache Project が開発を行なっているオープンソースのJ2EE Web アプリケーション用フレームワーク。公式サイトはhttp://struts.apache.org/。
  • 注13:Plain Old Java Object。EJBといった特定の仕様によらない普通のJava オブジェクトのこと。
  • 注14:クラスへのアクセスを行なう際に、そのクラスの代理となるProxy クラスを経由するパターン。
  • 注15:public、package private(識別子なしの場合)、protected、privateといったアクセスを制限するために利用される識別子。Java の言語仕様で規定されている。
アーカイブス一覧へ