はじめてのDependency Injection(DI)
仕事のプロジェクトがJavaになりそうな雰囲気だったので、先週、ちょこっとだけJavaの勉強していたら、DIという単語が目にとまりました。日本語に直すと「依存性注入」らしいのですが、なんだかよく分からない言葉です。Javaの世界に疎くてこれが主流なのかどうか知らないのですが、ざっと調べた感じ、使えそうな気がしました。折角なので自分の理解を深めるためにちょっと記事を書いてみようかと思います。
目的は?
最初、「依存性注入」という言葉から、依存関係を強めるような印象を抱いてしまい、そんなことしてどうするんだ?と、首をかしげてしまいました。実際はその逆で、コードから依存関係を取り除いて、必要な依存関係はコードの外(設定ファイルなど)から入れることです。つまり、「依存性を注入する」ということは、依存性が入ってないように作るから、後で依存性を注入することになるってことを言っているんでしょうね。やっぱ回りくどくて分かり難いですよ。依存性って何?
コードを書く上で依存性が無いなんてことはありません。ちょっとしたサンプルコードのようなものでない限り、main関数や1つのクラスで全ての処理を書くことはありません。なので、ここで言う依存性とは「過剰な依存性」となります。何を持って過剰なのかはケースバイケースですが、例えば、「データベースに格納する」で十分なところを「オラクルに格納する」とすると、オラクルへの依存が過剰だ、という意味で捉えてください。例題
文章ばかりでは分かり難いので、簡単な例を考えてみましょう。Hello, Worldを表示するプログラムを作成せよ。
こんな問題、
System.Console.WriteLine ("Hello, World");
これで一発なんですが、コンソールに出力とはどこにも書いてませんね。「コンソールに出力するな」とも書いてない曖昧な仕様なので、仕様が悪い言えばそれまでですが、プロジェクトが後戻りするのは、大抵こんなところからだったりします。(^^;
さて、依存性の話に戻りますと、「表示する」という要求に対して「コンソール」が過剰に依存しています。対象がコンソールだろうが、メッセージボックスだろうが対応可能にするにはどうすれば良いでしょうか?
オブジェクト指向に慣れている人でしたら、表示するというメソッドを持ったインタフェースを用意して、インタフェースに対してプログラミングすることを考えます。人によっては、具体的に「コンソールに表示して欲しい」「メッセージボックスに表示して欲しい」など、要求が発生したときに、コードを弄らないようにしたいと考えて、ファクトリクラスを用意し、設定ファイルなどを読み込んでオブジェクトを生成するように作るかも知れません。
例えば、こんな感じ。
using System; using System.Configuration; public class Program { public static void Main () { // 構成ファイルからメッセージを取得して設定 Greeting greeting = new Greeting (); greeting.Message = ConfigurationManager.AppSettings ["Message"]; // ファクトリ経由で出力先を取得して表示 IGreetingView view = GreetingViewFactory.Get (); view.Show (greeting); } }
GreetingViewFactoryのコードは恐らく想像通りだと思うので今回は省略します。
で、構成ファイルの中身はこんなの。
<?xml version="1.0" encoding="shift_jis" ?> <configuration> <appSettings> <!-- コンソールに表示 <add key="IGreetingView" value="GreetingConsoleView, GreetingConsoleView" /> --> <!-- メッセージボックスに表示 --> <add key="IGreetingView" value="GreetingMessageBoxView, GreetingMessageBoxView" /> <!-- メッセージの内容 --> <add key="Message" value="Hello, World" /> </appSettings> </configuration>
IGreetingViewが表示対象となるインタフェースでShowというメソッドを持っています。GreetingConsoleViewとGreetingMessageBoxViewが具象クラスで、構成ファイルを変更するだけで、コンソールとメッセージボックスを切り替えることができます。実はこれも依存性注入と言えます。つまり、DIとはぶっちゃけ「ファクトリクラスの豪華版」です。こう書くとガッカリする人もいそうなのでフォローしますと、大抵のDIコンテナはDI+AOP(アスペクト指向)になってて、もっと色々凄いことができます。
.NETでDI
世の中のDIコンテナはほとんどJava向けで、.NETだと選択肢が限られてきます。今回は、国産DIコンテナであるS2Container.NETを使ってみたいと思います。作成するクラス(インタフェース)は以下の通り。- Greeting : メッセージを格納する
- IGreetingView : メッセージ表示インタフェース
- GreetingConsoleView : IGreetingViewを実装したコンソール表示クラス
- GreetingMessageBoxView : IGreetingViewを実装したメッセージボックス表示クラス
- IGreetingService : アプリケーションの行うサービス
- GreetingService : サービスの実装
これらは普通の.NETの型で、Seasar用に特別なクラスを継承したりする必要はありません。
では、これらのクラスをS2Container.NETを使ってDIしてみましょう。トップダウンに順番に見ていきます。
まずは、Mainから。
using System; using Seasar.Framework.Container; using Seasar.Framework.Container.Factory; public class App { public static void Main () { // 構成ファイルのconfigPathで指定したdiconファイルを読み込む SingletonS2ContainerFactory.Init(); // S2コンテナ経由でコンポーネントを取得する IS2Container container = SingletonS2ContainerFactory.Container; IGreetingService service = (IGreetingService) container.GetComponent (typeof(IGreetingService)); service.Say (); // コンテナの後片づけ SingletonS2ContainerFactory.Destroy (); } }
中身は単純です。DIコンテナを初期化して、アプリケーションのサービスを取得してメソッドを呼び出しているだけです。DIコンテナを明示的に呼び出しているのはここだけで、残りのコードにはDIコンテナは出てきません。
DIの場合、ある意味コードと同じくらい構成ファイルが重要になってきます。なので、appDI.exe.configを見てみましょう。(アプリケーション名をappDI.exeにしています)
<?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <section name="seasar" type="Seasar.Framework.Xml.S2SectionHandler, Seasar" /> </configSections> <seasar> <assemblys> <assembly>Greeting</assembly> <assembly>GreetingConsoleView</assembly> <assembly>GreetingMessageBoxView</assembly> <assembly>GreetingService</assembly> </assemblys> <configPath>appDI.dicon</configPath> </seasar> </configuration>
まずは、seasar用にセクションを作成し、アプリケーションで使用するアセンブリを指定しています。具象クラスのアセンブリのみになっていますが、インタフェースについては具象クラスのアセンブリに関連付いているので指定する必要はありません。最後のconfigPathでappDI.diconというファイルを指定していますが、このファイルが依存性注入の記述が書かれているファイルです。
SingletonS2ContainerFactory.Init();
このタイミングで読み込まれます。
では、appDI.diconファイルを見てみましょう。
<?xml version="1.0" encoding="utf-8" ?> <!DOCTYPE components PUBLIC "-//SEASAR2.1//DTD S2Container//EN" "http://www.seasar.org/dtd/components21.dtd"> <components> <component class="Greeting" name="Greeting"> <property name="Message">"Hello, World"</property> </component> <component class="GreetingConsoleView" name="GreetingConsoleView" /> <component class="GreetingMessageBoxView" name="GreetingMessageBoxView" /> <component class="GreetingService"> <property name="Message">Greeting</property> <!-- <property name="View">GreetingConsoleView</property> --> <property name="View">GreetingMessageBoxView</property> </component> </components>
componetタグでクラスの登録を行っています。class属性にクラス名を、name属性にはコンポーネントを識別する名称を指定します。今回はclassとnameは同じ名称にしています。
<component class="Greeting" name="Greeting"> <property name="Message">"Hello, World"</property> </component>
ここで注目なのが、propertyです。DIコンテナはGreetingオブジェクト生成後、MessageプロパティにHello, Worldを設定してオブジェクトを返します。また、DIコンテナがサポートするオブジェクトの初期化方法はプロパティの他に、コンストラクタや初期化メソッドなどを指定することも可能です。個人的には、これがDIコンテナで嬉しいことの1つです。再利用の妨げになる原因の一つにオブジェクトを如何に生成するかがあります。デフォルトコンストラクタだけで初期化可能なオブジェクトだけなら問題になりませんが、幾つかの手順を踏まなければならない場合、ファクトリクラスを用意したとしても、都度、コンパイルが必要になることが多いです。DIコンテナを使えば、多くのケースでコンパイル無しにアセンブリを入れ替えることができます。逆に言うと、DIコンテナに作業を押しつけるような作り方をするようにしなければなりませんが、最初のウチは戸惑いそうです。
次に表示関連のクラスです。
<component class="GreetingConsoleView" name="GreetingConsoleView" /> <component class="GreetingMessageBoxView" name="GreetingMessageBoxView" />
この2つはデフォルトコンストラクタを呼ぶだけなので、特別な記述はありません。
最後に処理本体を担当するサービスクラスです。
<component class="GreetingService"> <property name="Message">Greeting</property> <!-- <property name="View">GreetingConsoleView</property> --> <property name="View">GreetingMessageBoxView</property> </component>
MessageプロパティにGreetingを指定していますが、これはcomponentのname属性の値を指定しています。これによってHello, Worldが設定されたGreetingオブジェクトがプロパティに設定されます。Viewプロパティにはメッセージボックス用のオブジェクトを設定しています。これをコンソール用に切り替えると、コンソールにメッセージが表示されます。
DIコンテナの構成ファイルの中身が分かったところで、Mainメソッドをの続きを見ていきます。
IGreetingService service = (IGreetingService) container.GetComponent (typeof(IGreetingService));
DIコンテナにtypeof(IGreetingService)指定して、IGreetingServiceを実装するクラスのインスタンスを取得します。先ほどの設定ファイルで見たとおり、GreetingServiceのインスタンスが作成され、MessageプロパティにGreetingのインスタンス(Hello, Worldが設定済み)が設定され、ViewプロパティにはGreetingMessageBoxViewのインスタンスが設定されます。
IGreetingServiceインタフェース
public interface IGreetingService { void Say (); }
GreetingServiceクラス
public class GreetingService : IGreetingService { Greeting greeting; IGreetingView view; public Greeting Message { get { return greeting; } set { greeting = value; } } public IGreetingView View { get { return view; } set { view = value; } } public void Say () { view.Show (greeting); } }
view.ShowでメッセージボックスにHello, Worldが表示されます。
残りのクラス/インタフェースについても見てみましょう。
Greetingクラス
public class Greeting { string message; public string Message { get { return message; } set { message = value; } } }
IGreetingViewインタフェース
public interface IGreetingView { void Show (Greeting greeting); }
GreetingConsoleViewクラス
using System; public class GreetingConsoleView : IGreetingView { public void Show (Greeting greeting) { Console.WriteLine (greeting.Message); } }
GreetingMessageBoxViewクラス
using System; using System.Windows.Forms; public class GreetingMessageBoxView : IGreetingView { public void Show (Greeting greeting) { MessageBox.Show (greeting.Message); } }
取りあえず、DIコンテナのさわりでした。Seasar.NETは他にも、S2Dao.NET(O/Rマッパー)とか便利なのが色々ありますので、機会があったら紹介したいと思います。っというか、アスペクト指向によるDAOの自動生成や、トランザクションなどのサンプルは用意したんですが、記事を書く時間が取れなくて・・・(^^;