2008年6月18日水曜日

DIのお話を書いてみる(Guice)

まず。この物語はフィクションであり このエントリ上のコードはあくまでもチュートリアル用のコードであり、実用性に関しては(ry

という訳で、id:happy_ryoのリクエストにお応えして、id:daisuke-mが書いたDIのお話のパクリです。Guiceを使ってみます。

ポリモーフィズムの例をもうちっと実用的に書いてみた。 - 都元ダイスケ IT-PRESSの続きとして書かれた、「DIのお話を書いてみる」をGuiceで書いたものです。必ずしもネタ元と同じ展開になるとは限りません...というか、Seasar2とGuiceでは機能が違うから同じ展開は難しいのです。が、「DIとはなんぞや?」を説明する、という目的とサンプルアプリケーションは同じです。

まずはいきなりMain...ではなくBusinessLogicを見てみますか。

本家のdaisuke-mの展開だとまずMainになっているが、こちらはBusinessLogicから。
import com.google.inject.Inject;

public class BusinessLogic {
    private Converter converter;

    @Inject
    public BusinessLogic(Converter converter) {
        this.converter = converter;
    }

    public void doBusiness(Table table) {
        String sql = converter.convert(table);
        System.out.println(sql);
    }
}
元々のBusinessLogic.javaとの差分は2行だけ。
  • まず1行目の「import com.google.inject.Inject;」は、GuiceのAnnotation. 単なるimport。
  • で、本題はコンストラクタを修飾している「@Inject」。これのために先ほどのimportがある。今回は「DI」つまり「Dependency(依存) Injection(注入)」の話であって、その「Inject」を行うための修飾となる。
    BusinessLogicが「依存」しているConverterのインスタンスを「うまい具合に設定してね」という事をGuiceに伝えるための表現。

ではMain見てみますか。

import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;

public class Main {
    public static void main(String[] args) {
        Table table = new Table("T_HOGE");
        table.columns.add(new Column("ID", "integer"));
        table.columns.add(new Column("CONTENTS", "string"));

        // Guiceの設定
        Injector injector = Guice.createInjector(new AbstractModule() {
            @Override protected void configure() {
                bind(Converter.class).to(MySQLConverter.class);
            }
        });
        BusinessLogic business = injector.getInstance(BusinessLogic.class);
        business.doBusiness(table);
    }
}
main()メソッド内のtableがらみの3行はネタ元のソースだが「Guiceの設定」から下がこのサイトオリジナルのもの。
  • 「Guice.createInjector()」で「Injector」のインスタンスを生成している。こいつこそが「DIContainerの本体」みたいなものとなる。
  • 「Injector」のインスタンスを生成する際に、AbstractModuleを継承した匿名Classを引数に渡している。これが「DIContainerへの設定情報」となる。GuiceはXmlとか使わず、Javaで設定するのだ。
  • その設定方法として、「bind().to()」という表現方法で「Converter」という型には「MySQLConverter」というclassを割り当ててね、という設定をしている。めっさ簡単に単純化して言うと「Converter.classをKeyにMySQLConverter.classを設定」しているカンジ。Mapみたいなイメージでも構わない。
  • BusinessLogicのインスタンスを作成する時は、自分でnewせずにInjectorのインスタンスに対して「getInstance()」でBusinessLogicのインスタンスを要求してる。すると、BusinessLogicを生成しようとしたGuiceが、先ほどの「@Inject」という修飾を見つけて「Converter」のインスタンスをInjectしなきゃ!となり、その際には「ConverterにはMySQLConverter」という設定を見て「MySQLConverter」のインスタンスをBusinessLogicにInjectしてくれるのだ。

ちょっと話がそれるけど…BusinessLogicを少し修正

少しBusinessLogicを修正してみる。こちらの方が「Inject」されてる!と感じやすいかも?しれない。
import com.google.inject.Inject;

public class BusinessLogic {
    @Inject
    private Converter converter;

    public void doBusiness(Table table) {
        String sql = converter.convert(table);
        System.out.println(sql);
    }
}
Converterを引数にしたコンストラクタを削除して、private で宣言しているfieldに対して「@Inject」で修飾してみた。これでも、Mainはそのままでも動作する。privateとかおかまいなしにGuiceがInjectしてくれるためだ。
なお、この修正を行った後で、Mainクラス内のBusinessLogicのインスタンス生成をGuiceのInjectorに依頼するのではなく、自分自身でnewしてみよう。つまり「BusinessLogic business = injector.getInstance(BusinessLogic.class);」を「BusinessLogic business = new BusinessLogic();」とする。この場合は以下のようなExceptionが発生する。
Exception in thread "main" java.lang.NullPointerException
    at BusinessLogic.doBusiness(BusinessLogic.java:8)
    at Main.main(Main.java:19)
いわゆる、ぬるぽですな。そりゃそうです、BusinessLogic#Converterはコンストラクタでも受け取らないし、setterすら無い、「設定できないはずのfield」となっているわけですから。
この無理矢理っぽい動作を見ると「誰かが不可視なはずのfieldに何かを注入しやがっている!」という事を実感しやすいかもしれん、と思っただけです。

あれ?ひとつのInterfaceに対してひとつの実装の型を指定できるだけ?

ネタ元では、「mysql」「postgresql」という名前でConverterの実装を切り分けているようですね。では、Guiceでも同じ事をやってみよう。Guiceの設定部分を次のように修正する。

// Guiceの設定
Injector injector = Guice.createInjector(new AbstractModule() {
    @Override
    protected void configure() {
        bind(Converter.class).annotatedWith(Names.named("mysql")).to(
                MySQLConverter.class);
        bind(Converter.class).annotatedWith(Names.named("postgresql")).to(
                PostgreSQLConverter.class);
    }
});

bind().annotatedWith().to()、という流れ。「"mysql"という修飾があるConverterの実装としてMySQLConverter、"postgresql"という修飾があるConverterの実装としてPostgreSQLConverter」という設定をしている。見たまんまだ。で、これを使う側のBusinessLogicのソースは以下のようになる。

import com.google.inject.Inject;
import com.google.inject.name.Named;

public class BusinessLogic {
    @Inject
    @Named("postgresql")
    private Converter converter;

    public void doBusiness(Table table) {
        String sql = converter.convert(table);
        System.out.println(sql);
    }
}

...あれ?これだと"mysql","postgresql"を選択するときに毎回ソースを触らないといけないな…。
ま、今回はこれでいいとしよう(他の方法がわかんないしw)。GuiceでのDIの簡単な説明についてはこんなところで。

コメントを投稿