コードで学ぶドメイン駆動設計入門 〜エンティティとバリューオブジェクト編〜 - じゅんいち☆かとうの技術日誌
コードで学ぶドメイン駆動設計入門 〜振る舞いとサービス編〜 - じゅんいち☆かとうの技術日誌
と続いて、このエントリはファクトリ編を解説します。
ドメインオブジェクトのライフサイクル
これまで紹介したエンティティやバリューオブジェクト、サービス(以下、これらをドメインオブジェクトと呼ぶことにします)はライフサイクルを持っています。
ライフサイクルと聞くと、newされてからGCされるまでの間を想像します。しかし、一般的なアプリケーションでは、実際はVMのライフサイクルを超えるライフサイクルを扱うことが多いと思います。たとえば、テキストエディタで編集したデータを永続化する場合などです。テキストエディタのデータは単なるバイト配列と捉えるのではなく、ドメインモデルとして捉えることができます。新規にテキストを編集する際にドメインオブジェクトが生成されて、保存する時はファイルなどに永続化されるわけです。
そのような問題を解決するためにファクトリやリポジトリがあります。それではコードを使いながら一つずつ解説してみます。
ぬあっと、全部書こうかと思ったのですが、エントリが肥大化したのでファクトリだけ書きますw
ファクトリ
ファクトリは、デザインパターンの本にも登場する重要なパターンですが、ライフサイクルの始まり、つまりドメインオブジェクトの生成にフォーカスして、複雑なドメインオブジェクトの生成やそれらの内部構造をカプセル化することが目的です。
現実の世界では、モノ自身がモノを作るということがないのですが、言語の機能として車自身が車を製造するということが可能になっています。
Car car = new Car(); // 車が車自身を製造?
無論、言語としてはできて当然なのですが、現実世界と少し違うことが行われているという違和感ですw
CarFactory carFactory = new CarFactory();
Car car = carFactory.newCar();
このように車の工場が車を製造するという方が、直感的に分かりやすいという見方があります。この程度の単純なオブジェクトであれば、生成に手間取らないので、わざわざファクトリを導入する必要もないかもしれません。
しかし、車が保持するエンジンやタイヤなどの内部のオブジェクトも生成しなければならない場合はどうでしょうか? 以下のコードでイメージしてみてください。
Engine engine = new Engine(...); List<Tire> tires = new ArrayList<Tire>(); tires.add(new Tire(...)); Car car = new Car(engine, tires); car.goTo(store);
Carを利用する側(クライアント側)が実現したいことは「お店に行くこと」なのに、内部のオブジェクトを生成まで強いられるのは本末転倒なのです。しかし、Carの立場ではエンジン(Engine)やタイヤ(Tire)は生成時に必要なオブジェクトなので、クライアント側では初期化に必要なオブジェクトもあわせて用意する必要があります。つまるところ、Carの内部の構造が露呈しているわけで、クライアントを不必要に手間取らせコードを複雑にするだけです。このような問題を解決できるのがファクトリです。CarFactoryは複雑な生成処理をカプセル化します。*1
Car car = carFactory.newCarForShopping(); // 買い物に適切なエンジンとタイヤをセットした車を生成する
car.goTo(store);
schema-generatorのファクトリ
ここでまたschema-generatorのファクトリの例を紹介します。
ActionFactoryImpl(ファクトリ)です。
/** * {@link ActionFactory}の実装クラス。 */ public class ActionFactoryImpl implements ActionFactory { static final String SQL = "SQL:"; static final String ECHO = "ECHO:"; /** * {@inheritDoc} * * @param command 先頭にSQL:かECHO:の文字列が含まれるアクションの文字列。 */ @Override public Action<?> newAction(String command) { Validate.notNull(command); Validate.notEmpty(command); Action<?> action = null; if (command.startsWith(ECHO)) { action = new EchoActionImpl(command.substring(ECHO.length())); } else if (command.startsWith(SQL)) { action = new SqlActionImpl(command.substring(SQL.length())); } return action; } }
Carの例ほど、難しいことはやっていませんが、設定ファイル上で指定されているアクション用の文字列には、ECHO:やSQL:から始まるプレフィックスが含まています。それを見分けて、適切なアクションの実装を生成して返します。このAPIのクライアント側は具体的な実装を意識する必要がありません。
ActionFactory actionFactory = new ActionFactoryImpl(); Action<?> action = actionFactory.newAction("ECHO:Hello, World!"); // 内部でEchoActionImplのインスタンスを生成 action.execute(context); // クライアント側はEchoActionImplであろうが、なかろうがそのアクションを実行するだけ。
ActionsFactoryImpl(ファクトリ)はバリューオブジェクトのファクトリですが、エンティティのためのファクトリもあります。
このファクトリでは殆ど何もやってませんが、先ほどと同じように実装を隠蔽するためにファクトリを用いています。複数の実装を認めるのであれば、ファクトリを用意しておいたほうがよいでしょう。逆にいうとDataSourceは複数の実装も認めず、生成もそれほど難しいものではないので、ファクトリを用意していません。
/** * {@link ActionsFactory}の実装クラス。 */ public class ActionsFactoryImpl implements ActionsFactory { @Override public Actions newActions(String identity, DataSource dataSource, DataSourceConnectService dataSourceConnectService, List<Action<?>> actions) { return new ActionsImpl(identity, dataSource, dataSourceConnectService, actions); } }
汎用的なファクトリインターフェイスとその実装を考えてみる
schema-generatorの例では、ActionFactory#newActionやActionsFactory#newActionsなどと、ドメインオブジェクトに特化したシグニチャになっています。これはこれで問題ないのですが、もっと汎用的なインターフェイスとして定義したい場合、以下のようなものがよいと思います。*2
/** * {@link Entity}のためのファクトリ。 */ public interface EntityFactory<T extends Entity<T>> { /** * ファクトリの状態に基づいて {@link Entity}のインスタンスを生成する。 * * <p>エンティティの識別子は自動生成される。</p> * * @return 新しい {@link Entity}のインスタンス */ T create(); /** * ファクトリの状態に基づいて {@link Entity}のインスタンスを生成する。 * * @param identifier エンティティの識別子 * @return 新しい {@link Entity}のインスタンス * @throws IllegalArgumentException 引数{@code identifier}に{@code null}を与えた場合 */ T create(EntityIdentifier<T> identifier); }
それでは、このファクトリのインターフェイスで、Employeeのファクトリを実装してみましょう。
/** * {@link Employee}のためのファクトリ実装。 */ public class EmployeeFactory implements EntityFactory<Employee>{ private PersonName name; /** * インスタンスを生成する。 * * @param name {@link PersonName} */ public EmployeeFactory(PersonName name){ this.name = name; } @Override public Employee create() { // EntityIdentifierBuilderは、バリューオブジェクトのビルダー。詳しくは後述。 EntityIdentifier<Employee> identifier = new EntityIdentifierBuilder<Employee>(Employee.class).build(); return build(identifier); } @Override public Employee create(EntityIdentifier<Employee> identifier) { return new Employee(identifier, name); } }
使い方はこんな感じ。
Employee employee = new EmployeeFactory(personName).create();
エンティティの場合はほとんどが可変オブジェクトなので、属性の数が多くなっても不変条件を満たすためにコンストラクタの引数が増えるということはないはずなので、このような使い方で問題ないと思います。
次は、バリューオブジェクトのファクトリを検討します。これまでのエントリで紹介したPersonNameで考えます。BigDecimalもバリューオブジェクトですが、以下のようなファクトリメソッドを持っています。
BigDecimal bd = BigDecimal.valueOf(1L);
PersonNameでも以下のようなファクトリメソッドを持つのもひとつの手段です。
PersonName personName = PersonName.of("Junichi","Kato");
少し脱線しますが、バリューオブジェクトは、基本的に状態が変化しない不変オブジェクトです。この性質はオブジェクトをいろいろなところで共有するには好都合です。
// personNameには同一インスタンスで状態を変える機能がないので、不変。 PersonName personName = new PersonName("Junichi","Kato");
しかし、上記のpersonNameの一部の属性を変更したい場合はどうしたらよいでしょうか。たとえば、KatoをKATOのように大文字に変更したい場合です。
PersonName personName1 = new PersonName("Junichi","Kato"); PersonName personName2 = new PersonName(personName1.getFirstName(), personName1.getLastName().toUpperCase());
若干面倒ですw まぁ、この程度なら問題ないですが、引数の多い、つまり属性が多いバリューオブジェクト(大きいバリューオブジェクト)は一部の属性を書き換えるのが手間です。この問題は、以下のようなファクトリを実装しても単純に解決できる問題ではありません。*3
// こんなファクトリを作ったとしても全く効果がない。 public class PersonNameFactory { public PersonName newPerson(String firstName, String lastName){ retunr new Person(fistName, lastName); } } PersonName personName1 = personNameFactory.newPersonName("Junichi","Kato"); PersonName personName2 = personNameFactory.newPersonName(personName1.getFirstName(), personName1.getLastName().toUpperCase());
では、不変オブジェクトの唯一の欠点を埋めるにはどうしたらよいのでしょうか。その時に使えるのがビルダーパターンです。こちらもJiemamyプロジェクトに、非常に便利なValueObjectBuilderという成果物があります。以下に示します。
/** * {@link ValueObject}のインスタンスを生成するビルダー。 * * @param <T> ビルド対象のインスタンスの型 * @param <S> このビルダークラスの型 */ public abstract class ValueObjectBuilder<T extends ValueObject, S extends ValueObjectBuilder<T, S>> { List<BuilderConfigurator<S>> configurators = new ArrayList<BuilderConfigurator<S>>(); /** * ビルダの設定に基づき、引数の{@link ValueObject}の内容を変更した新しいインスタンスを生成する。 * * @param vo 状態を引用する{@link ValueObject} * @return vo の内容に対して、このビルダの設定を上書きした{@link ValueObject}の新しいインスタンス */ public T apply(T vo) { S builder = newInstance(); apply(vo, builder); for (BuilderConfigurator<S> configurator : configurators) { builder.addConfigurator(configurator); } return builder.build(); } /** * ビルダの設定に基づいて{@link ValueObject}の新しいインスタンスを生成する。 * * @return {@link ValueObject}の新しいインスタンス */ public T build() { for (BuilderConfigurator<S> configurator : configurators) { configurator.configure(getThis()); } return createValueObject(); } /** * {@link BuilderConfigurator}を追加する。 * * @param configurator {@link BuilderConfigurator} */ protected void addConfigurator(BuilderConfigurator<S> configurator) { configurators.add(configurator); } /** * 引数のビルダに対して、引数の{@link ValueObject}の内容を適用する。 * * @param vo 状態を引用する{@link ValueObject} * @param builder ビルダ */ protected abstract void apply(T vo, S builder); /** * ビルダの設定に基づいて{@link ValueObject}の新しいインスタンスを生成する。 * * <p> * {@link ValueObjectBuilder#build}内でこのビルダに追加された{@link BuilderConfigurator}を全て実行した後に、このメソッドが呼ばれる。<br> * その為、このビルダに対する変更を行うロジックはこのメソッド内に記述せず、目的となる{@link ValueObject}を生成し返すロジックを記述することが望まれる。 * </p> * * @return {@link ValueObject}の新しいインスタンス */ protected abstract T createValueObject(); /** * このビルダークラスのインスタンスを返す。 * * @return このビルダークラスのインスタンス。 */ protected abstract S getThis(); /** * このビルダークラスの新しいインスタンスを返す。 * * @return このビルダークラスの新しいインスタンス。 */ protected abstract S newInstance(); /** * {@link ValueObjectBuilder#build()}内で順次実行されるビルダの設定を定義するインタフェース。 * * @param <S> 設定対象ビルダーの型 */ public static interface BuilderConfigurator<S> { /** * {@link ValueObjectBuilder#build()}内で呼ばれる際に実行するビルドアクションを定義する。 * * @param builder ビルダーインスタンス */ void configure(S builder); } }
それでは、このビルダーを継承したPersonName用のビルダーを以下に示します。長いですが辛抱をw
/** * {@link PersonName}のためのビルダ実装。 */ public class PersonNameBuilder extends ValueObjectBuilder<PersonName, PersonNameBuilder> { private String firstName; private String lastName; /** * {@link PersonName}に与える名前をこのビルダに設定する。 * * @param firstName 名前 * @return {@link PersonNameBuilder} */ public PersonNameBuilder withFirstName(final String firstName) { addConfigurator(new BuilderConfigurator<PersonNameBuilder>() { @Override public void configure(PersonNameBuilder builder) { builder.firstName = firstName; } }); return getThis(); } /** * {@link PersonName}に与える苗字をこのビルダに設定する。 * * @param lastName 苗字 * @return {@link PersonNameBuilder} */ public PersonNameBuilder withLastName(final String lastName){ addConfigurator(new BuilderConfigurator<PersonNameBuilder>() { @Override public void configure(PersonNameBuilder builder) { builder.lastName = lastName; } }); return getThis(); } @Override protected void apply(PersonName vo, PersonNameBuilder builder) { builder.withFirstName(vo.getFirstName()); builder.withFirstName(vo.getLastName()); } @Override protected PersonName createValueObject() { return new PersonName(firstName,lastName); } @Override protected PersonNameBuilder getThis() { return this; } @Override protected PersonNameBuilder newInstance() { return new PersonNameBuilder(); } }
PersonNameBuilderを使い方はこんな感じです。
新しくインスタンスを作る場合は、こちら。メソッドチェインで楽に呼べます。
PersonName personName1 = new PersonNameBuilder() .withFirstName("Junichi") .withLastName("Kato").build(); assertThat(personName1.getFirstName(), is("Junichi")); assertThat(personName1.getLastName(), is("Kato"));
既存のインスタンスの一部を変更して新しいインスタンスを作る場合はこちら。personName1.lastNameを大文字に書き換えています。このぐらいの規模のバリューオブジェクトの場合、効果はあまり感じませんが、大きいバリューオブジェクトの場合は効果が絶大で、ValueObjectBuilderの目玉機能だと思います。
PersonName personName2 = new PersonNameBuilder() .withLastName(personName1.getLastName().toUpperCase()) .apply(personName1); assertThat(personName2.getFirstName(), is("Junichi")); assertThat(personName2.getLastName(), is("KATO"));
ファクトリが不要な条件
ここまでファクトリの必要性を説いていますが、ファクトリが不要な場合もあります。
DDD Quickly日本語版では、以下の条件が示されています。
- オブジェクトの作成処理が複雑でない。
- オブジェクトの作成処理内で他のオブジェクトを作成しない。必要な属性はすべてコンストラクタで設定する。
- クライアントはオブジェクトの実装に興味がある。例えば、ストラテジパターンを使って処理を選択したい。
- クラスは具体的な型であり、その型から派生しているクラスはない。したがって具体的な実装を選ばなくていい。
判断基準はいろいろありますが、ファクトリメソッド→生成に適したオブジェクトにファクトリメソッド→スタンドアロンなファクトリクラスを設ける流れがよいようです。
あわせて読みたい
コードで学ぶドメイン駆動設計入門 〜エンティティとバリューオブジェクト編〜 - じゅんいち☆かとうの技術日誌
コードで学ぶドメイン駆動設計入門 〜振る舞いとサービス編〜 - じゅんいち☆かとうの技術日誌
コードで学ぶドメイン駆動設計入門 〜リポジトリ編〜 - じゅんいち☆かとうの技術日誌
コードで学ぶドメイン駆動設計入門 〜アグリゲート編〜 - じゅんいち☆かとうの技術日誌