読者です 読者をやめる 読者になる 読者になる

かとじゅんの技術日誌

技術の話をするところ

今さらデザインパターン Factoryパターン編

どうもです。今日は暖かいので親子でピクニックに行ってきました。

で、はい。本題ですが。

今回はFactoryパターン編。別名SimpleFactoryとも言われています。
GoFではFactoryMethodパターンが紹介されていますが、ここではシンプルなFactoryパターンを紹介します。(FactoryMethodパターンは次回のエントリで紹介します)
というか、DIコンテナベースの話になっているので、DIコンテナ上でのFactoryパターンの使い方の説明になっています。。。

まずは事例から

いつものごとく、ダイちゃんのところのサンプルを改編!やはり今回もDIコンテナを前提に考えます。

現状のBusinessLogicは、MySQLPostgreSQL用にそれぞれ別々のコンポーネントを用意して使うようにdiconファイルに定義されています。これはこれで正解だと思います。
でも、今回は一つのBusinessLogicで実行時に必要に応じてMySQLPostgreSQLの両方に対応するにはどうたらよいかを考えてみます。

BusinessLogicクラス単体だけを見た場合、Conveterはdiconファイル上で静的に決定されています。これはシステムに対する要件が静的に決まるのであればFactoryは必要ありません。必要なConveterをDIするだけでOKです。
ですが、実行時にConverterを選ばないといけない場合(たとえば、ユーザがUI上から生成するDBMSの種類を変更したなど)は、dicon上で静的にマッピングしていたのでは不可能です。

現状のBusinessLogic

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);
	}
}

で、MySQLPostgreSQLが実行時に変化する場合のロジックを考えてみましょう。
doBusinessメソッドの引数でConveterのモードを指定できるようにしましょう。こうすることで実行時指定できるようにします。(便宜上、converterModeは文字列ですが、本来ならenumなどがよいかと思います)

public class BusinessLogic {

// 省略

	public void doBusiness(String conveterMode, Table table) {

// 省略

	}
}

次にそのConverterのモードに応じたConverterをSeasar2から取得するFactoryクラスを作りましょう。
はい。以下です。createメソッドの引数の内容によってコンテナから取得するコンポーネントを変化させます。
diconファイルに定義してください。(DIコンテナ内で管理する前提なので、createメソッドはクラスメソッド化していません)

import org.seasar.framework.container.S2Container;

public class ConverterFactory {

	private S2Container s2Container;

	public void setS2Container(S2Container container) {
		s2Container = container;
	}

	public Converter create(String mode) {
		if ("MYSQL".equals(mode)) {
			return (Converter) this.s2Container.getComponent("mySQLConverter");
		} else if ("POSTGRESQL".equals(mode)) {
			return (Converter) this.s2Container
					.getComponent("postgreSQLConverter");
		}
		return null;
	}

}

引き続きBusinessLogicのほうですが、以下が最終版。コンストラクタインジェクションでConverterFactoryをDIするようにします。このようにすれば実行時動的にコンポーネントを切り替えれます。
余談ですが,上記のcreateメソッドのif文がなんか美しくないなぁと思うかもしれませんが,まぁ この程度なら実用的に問題ないと思います.if文を使わないですむ方法がFactoryMethodパターンなんですよね.それは次回で紹介します.

public class BusinessLogic {

	private ConverterFactory converterFactory;

	public BusinessLogic(ConverterFactory converterFactory) {
		this.converterFactory = converterFactory;
	}

	public void doBusiness(String conveterMode, Table table) {
		Converter converter = converterFactory.create(conveterMode);
		String sql = converter.convert(table);
		System.out.println(sql);
	}
}

diconファイルは以下のような感じ。BusinessLogicで作るコンポーネントは一種類。
ConverterFactoryをコンストラクタインジェクションします。

    <component name="conveterFactory" class="jp.xet.tutorial.s2.ConverterFactory"/>
    <component class="jp.xet.tutorial.s2.BusinessLogic">
        <arg>conveterFactory</arg>
    </component>

まとめ

DIコンテナが登場してFactoryの出番が少なくなったとはいえ、上記のように動的にコンポーネントを選択しなければならない場合は、やはりFactoryの出番となります。
本来、ConverterFactory#createメソッド内で各Converterクラスの生成を行うところを、DIコンテナからgetComponentしてます。これはコンテナで対象のコンポーネントをどのようなインスタンス管理をするかで、動きが変わってきます。Converterをsingletonで管理している場合は、毎回同一のインスタンスが返されます。厳密にはFactoryは生成することが責務なので、prototypeで各Conveterクラスを管理させたほうがよいかもしれません。
prototypeにしてもsetterインジェクションで各ConverterをDIしてしまうと、createメソッドは毎回同じインスタンスを返してしまいますので、createメソッド内でgetComponentする必要があります。

というか、私の場合は基本的にはそのようにするようにしています。

<component class="org.seasar.framework.container.autoregister.FileSystemComponentAutoRegister">
	<property name="instanceDef">@org.seasar.framework.container.deployer.InstanceDefFactory@PROTOTYPE</property>
	<initMethod name="addClassPattern">
		<arg>"jp.xet.tutorial.s2.converter"</arg>
		<arg>".*"</arg>
	</initMethod>
</component>

以上、ご参考までに。