DIのお話を書いてみる。

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

という訳で、id:happy_ryoのリクエストにお応えして、DIのお話。Seasar2を使ってみます。

ポリモーフィズムの例をもうちっと実用的に書いてみた。 - 都元ダイスケ IT-PRESSの続きです。

まずはいきなりMain見てみますか。

import org.seasar.framework.container.S2Container;
import org.seasar.framework.container.factory.SingletonS2ContainerFactory;

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"));
        
        SingletonS2ContainerFactory.init();
        S2Container container = SingletonS2ContainerFactory.getContainer();
        
        BusinessLogic business = (BusinessLogic) container.getComponent("business");
        business.doBusiness(table);
        
        container.destroy();
    }
}

何やら知らないクラスが出てきました。

DIコンテナ

S2Containerというクラスが「DIコンテナ」と呼ばれるものです。簡単に「インスタンス(≒コンポーネント)の管理人」と思って下さい*1

インスタンス管理というのは、新しいインスタンスを作ったり(newしたり)、保持したり、破棄したりする事。インスタンスが幾つ作られているのか、誰が持って居るのか、まだ生きている(GCされない)必要があるのか、という事を意識する事。

通常、インスタンス管理は、自分が書いているプログラム自身が行います。DIコンテナは、この作業を代行してくれるもの。コンテナに「このクラスのインスタンスくれー」って言うと、くれるんです。

そんなん、単純に自分でnewすりゃいいじゃん、と思うかもしれない。だけど例えばsingleton。プログラム中で1回しかnewしちゃいけなくて、使う時は全部同じインスタンスを使わなければいけない、という状態だ。1回だけnewしたインスタンスを誰が持つ? そのインスタンスが欲しい時はどうやって手にいれる? もう既にnewされているのか、1回もnewされていないのか、覚えておく役目はどのクラス?

一応、singletonパターンという名前でパターン化されているので、この件に関しては「パターンを知っている人」には負担の無い例だが、もっと複雑なインスタンス管理をしなければいけない状況だってふつーーにあり得る。

こういった状況だと、単純にnewする訳にはいかない。この煩雑な管理を任せることができるのが、DIコンテナ。

話が逸れたので戻します。

コード中の「container.getComponent("business");」という記述。これは「businessっていう名前のついたインスタンスをくれー」っていうコードです。

じゃあ、コンポーネントを定義(この名前は、このインスタンスの事だよーって宣言)しているのはドコ? というと、コード外の設定ファイルに書かれてる。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN"
	"http://www.seasar.org/dtd/components24.dtd">
<components>
	<component name="business" class="BusinessLogic" />
	<component name="mysql" class="MySQLConverter" />
	<component name="postgresql" class="PostgreSQLConverter" />
</components>

こんな感じで、3つのクラスのインスタンスにそれぞれ名前を付けています。コンテナは、このファイルを読み込む事によって、名前とインスタンスの関連づけをしています。ちなみにapp.diconというファイル名で、デフォルトパッケージに置いておきます。

依存性

ところで(といってまた話が逸れて行きますw)。依存って何だ? 抽象的説明より具体例のが分かりやすいかな。今の例で言えば「BusinessLogicクラスはConverterに依存している」のです。ConverterはBusinessLogicが居なくても動くけど、BusinessLogicはConverterが居ないと動かない。これが依存。

先ほどの例では、businessコンポーネントを取得して使ったけど、こいつはConverterに依存しているから、Converterが無いと動かないはず。Converterは2種類あるのに、どちらが使われたんだろう? そもそも、Converterは、どうやってBusinessLogicに組み込まれたんだろう?

ということで、BusinessLogicクラスを見てみる。

import org.seasar.framework.container.annotation.tiger.Binding;

public class BusinessLogic {
	
    @Binding("mysql")
    private Converter converter;

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

おお、なんか書いてある。この「@Binding("mysql")」というアノテーションは「このフィールドにmysqlって名前のコンポーネントを入れとけよ」っていう、コンテナに対する指示。

そうするとコンテナは「businessくれ」と言われた時に、このフィールドにmysqlを代入(=injection)した上で、インスタンスをくれるのです。

Converterが代入される。dependencyがinjectionされる。すなわち、dependency injectionでDI。

インスタンス管理もさることながら、依存性の管理もしてくれる。これが、DIコンテナの代表的なお仕事。

さて、これにて。

前のエントリで書いていたConverterFactoryの出番がなくなり、晴れてif文が消えてなくなりました!! めでたしめでたし。
と言われて納得した方は、悪徳商法新興宗教の類に注意して生きてくださいw

そんなん、Mainをどう変えたってmysql版でしか動かないんだから、if文消えるの当たり前やんw

というわけで、もう少しいじってみる。

まず、BusinessLogicからBindingアノテーションを取り払って、前のエントリと同様の状態に戻す。コンストラクタがある状態ね。

public class BusinessLogic {

    private Converter converter;

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

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

で、app.diconをちょっと書き換えます。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN"
	"http://www.seasar.org/dtd/components24.dtd">
<components>
	<component name="mysqlBusiness" class="BusinessLogic">
		<arg>mysql</arg>
	</component>
	<component name="postgresqlBusiness" class="BusinessLogic">
		<arg>postgresql</arg>
	</component>
	<component name="mysql" class="MySQLConverter" />
	<component name="postgresql" class="PostgreSQLConverter" />
</components>

なんか、ってのが出てきた。これは「コンストラクタに与える引数」を表してる。つまり、

  • mysqlBusinessコンポーネントってのは、コンストラクタにmysqlコンポーネントを与えてnewしたもの。
  • postgresqlBusinessコンポーネントってのは、コンストラクタにpostgresqlコンポーネントを与えてnewしたもの。

この上で「container.getComponent("mysqlBusiness");」とやると、「MySQLConverterが注入されたBusinessLogic」をもらえるのだ。

これで、さっきの詐欺的状況から抜け出しましたね。if文が消え去り、与える文字列によって動作が変わるコードが出来上がりました。args辺りから文字列を引っ張ってくれば、立派に両方の動作をこなします。

AutoBinding

以上で、DIについての基本的な解説は終了。ここからはちょっと余談で、Seasar2の「AutoBinding機能」について。

例えば、app.diconをこんな風に書き換えてみる。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN"
	"http://www.seasar.org/dtd/components24.dtd">
<components>
	<component name="mysqlBusiness" class="BusinessLogic" />
	<component name="mysql" class="MySQLConverter" />
</components>

コンテナにMySQLConverterだけを登録(PostgreSQLConverterをコンテナに登録しない)して、かつ、args指定を省略する。これでプログラムを走らせたらどうなるでしょう?

コンテナは「BusinessLogicのコンストラクタに何を与えていいか分からない」状態に見えます。

普通に考えれば、例外投げて終了するか、強引にnullを渡してnewするか。だがこれがSeasarの凄いところ。以下、コンテナの思考パターン。

  • mysqlBusinessをくれって言われたからBusinessLogicをnewしよう*2
  • んーー、コンストラクタに引数がある。なんか食わせなきゃnewできん。
  • 与えなきゃいけない型はConverterか。
  • お、mysqlコンポーネントはConverterを実装してるぞ。
  • こいつを食わせとけばいっか。

という感じで、よしなにDIしてインスタンスをくれる。これすげー。

ただし。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN"
	"http://www.seasar.org/dtd/components24.dtd">
<components>
	<component name="mysqlBusiness" class="BusinessLogic" />
	<component name="mysql" class="MySQLConverter" />
	<component name="postgresql" class="PostgreSQLConverter" />
</components>

app.diconがこうだった場合。「mysqlとpostgresqlは、どっちもConverterを実装してるぞ…。さすがにどっち食わせりゃいいかわかんねえよ。」ということで

org.seasar.framework.container.TooManyRegistrationRuntimeException: [ESSR0045]interface Converterに複数のコンポーネント(MySQLConverter, PostgreSQLConverter)が登録されています

となってしまいます。

以上! Seasar2を使ったDIの解説でした。

追記

以上の例ではデフォルトパッケージを使用したので、app.diconののclass属性にはクラス名のみが記述されていますが、パッケージをしっかり指定した場合は、下記のようにそのクラスのFQCNを記述する必要がありますのでご注意を。

<component name="postgresql" class="jp.xet.tutorial.di.PostgreSQLConverter" />

追記2

で、よく見ると、これってStrategyパターンが出来上がっている。Strategyパターンで言うところの「ContextはBusinessLogic」、「StrategyはConverter」、「ConctereStrategyがMySQLConverterやPostgreSQLConverter」にあたります。

*1:他にも仕事はありますが、後述

*2:実際には、app.diconでインスタンス管理方式を指定していないので、newはinit時に行われている。正確には、init時に「mysqlBusinessがコンポーネント登録されたから、インスタンスを作っておかなきゃ」となる。