プログラミング初心者がアーキテクトっぽく語る

見苦しい記事も多数あるとは思いますが訂正しつつブログと共に成長していければと思います

Dependency Injection vs Abstract Factory - Clean Architectureのオブジェクト生成

Clean ArchitectureではController、UseCase、Presenter、Viewなどのオブジェクトを生成する必要があります。

ControllerはUseCase、Presenter、Viewオブジェクトをフィールドに保持して利用します。

これらオブジェクト群の生成処理をどのようにするべきかよく悩みます。

ControllerへConstructor Injectionするのが一番シンプルですがユースケースが増えてきたときにコードが汚くなりがちです。

Abstract Factoryパターンを使うと個々のクラスはすっきりしますが、クラスや依存関係が増えて無駄にややこしくなったように感じることもあります。

またFactoryをMain側で使うか、Controller側で使うかで悩むこともあります。

悩んだ結果、「根っこクラスは多少、汚くても許されるだろう」という言い訳で適当なところで諦めます。

そうして私が放置した根っこクラスをいつの間にか誰かがきれいにしてくれていたこともありました。

いつまでも迷惑ばかりかけてられないので今回、まじめに検討してみることにしました。


Dependency Injection方式

この方式ではMainがUseCase、Presenter、Viewオブジェクトを「new」で生成します。 その後、これらのオブジェクトをControllerにConstructor Injectionで渡します。

上のUML図を見るとオブジェクトのLifecycle管理は下位層のMainが行い、Controllerは処理に集中しています。 きれいな役割分担だと思います。

Mainから複数の矢印が上位層に扇形に延びています。 依存関係の方向はきれいですが数が多いです。 「Mainあるある」で流せるか、許容できないか、好みが分かれそうです。

総じて、エンタープライズシステムの基幹部分(根っこ)っぽい構造だなと個人的には感じます。

この方式は一番単純な実現方式です。 一番単純なのでTDDでコーディングすると最初にこの形になると思います。

// Main.java
//   - Constructor Injectionする
Gateway gw = new GatewayImpl();
UseCase useCase = new UseCaseImpl(gw);
Presenter presenter = new PresenterImpl();
View view = new ViewImpl();
Controller controller = new ControllerImpl(useCase, presenter, view);

ユースケースが増えて「Controller、UseCase、Presenter、View」のセットが増えた場合、同じようなコードを書き足していくことになります。

// Main.java
//   - ユースケースが増えたとき
Gateway gw1 = new UseCase1GatewayImpl();
UseCase useCase1 = new UseCase1UseCaseImpl(gw1);
Presenter presenter1 = new UseCase1PresenterImpl();
View view1 = new UseCase1ViewImpl();
Controller controller1 = new UseCase1ControllerImpl(useCase, presenter, view);
routes.put(path, controller1);

Gateway gw2 = new UseCase2GatewayImpl();
UseCase useCase2 = new UseCase2UseCaseImpl(gw2);
Presenter presenter2 = new UseCase2PresenterImpl();
View view2 = new UseCase2ViewImpl();
Controller controller2 = new UseCase2ControllerImpl(useCase, presenter, view);
routes.put(path, controller2);

<ユースケースの数、繰り返し>

似たようなコードの繰り返しが増えてMainが無駄に長くなってしまうのが難点です。


Abstract Factoryパターン方式1

この方式ではMainがController、UseCase、Presenter、ViewオブジェクトをFactoryで生成します。

UML図を見るとMainがFactoryを利用している以外は「Dependency Injection方式」と同じです。 これだけだとメリットがわかりにくいですね。

実際、以下のようにユースケースが1つだけだとメリットを感じないでしょう。

// Main.java
//  - Abstract Factoryを使う
Factory factory = FactoryImpl.getFactory();
Gateway gw = factory.makeGateway();
UseCase useCase = factory.makeUseCase(gw);
Presenter presenter = factory.makePresenter();
View view = factory.makeView();
Controller controller = factory.makeController(useCase, presenter, view);

しかし、ユースケースの数が増えてくると有難みが出てきます。 ユースケースが増えても多態性を活用してMainをきれいに保つことができるからです。

例えば下の例のようにforループを利用して冗長なオブジェクト生成コードを排除することができます。

// Main.java
//  - Abstract Factoryを利用して複数のユースケースに対応する
factories.add(new UseCase1Factory.getFactory());
factories.add(new UseCase2Factory.getFactory());
factories.add(new UseCase3Factory.getFactory());
<ユースケースの数繰り返す>

for (Factory factory : factories) {
  Gateway gw = factory.makeGateway();
  UseCase useCase = factory.makeUseCase(gw);
  Presenter presenter = factory.makePresenter();
  View view = factory.makeView();
  Controller controller = factory.makeController(useCase, presenter, view);
  routes.put(path, controller);
}

上述のサンプルコード前半のFactory生成処理部分を他のクラスへ移したり、Reflectionを利用したりすればMainをCloseすることも可能です。 以降、ユースケースを追加してもMainを変更する必要はありません。 Open Closed Principleです。

Mainにswitch/if文などの条件分岐がある場合も、多態性で置き換えてMainをCloseできる可能性が生まれます。

ユースケースが多いときや、今後ユースケースが増えていく可能性が高いときに適していると思います。


Abstract Factoryパターン方式2

この方式ではMainはControllerにFactoryオブジェクトをConstructor Injectionで渡します。 ControllerはUseCase、Presenter、ViewオブジェクトをFactoryを使って生成します。

UML図を見るとこれまでの方式と明らかに違います。

まず下位層から上位層へ伸びる矢印がすっきりしました。

次にUseCase、Presenter、ViewオブジェクトのLifecycle管理をControllerが行っています。 Controller側/上位側の責務が多くなったように見て取れます。 一般的なエンタープライスシステムの基幹部分でもありそうですが、上位層が大きなライブラリやフレームワーク、下位層がユーザアプリのような場合にも使えそうです。

次にコードを見てみましょう。

// Main.java
//  - ControllerにFactoryオブジェクトをConstructor Injectionで渡す
Controller controller1 = UseCase1ControllerImpl(new UseCase1Factory.getFactory());
routes.put(path, controller1);

Controller controller2 = UseCase2ControllerImpl(new UseCase2Factory.getFactory());
routes.put(path2, controller2);
<ユースケースの数繰り返す>
// ControllerImpl.java
//   - Factoryを利用して各種オブジェクトを生成する
private Gateway gw;
private UseCase useCase;
private Presenter presenter;
private View view;
public ControllerImpl(Factory factory) {
  this.gw = factory.makeGateway();
  this.useCase = factory.makeUseCase(this.gw);
  this.presenter = factory.makePresenter();
  this.view = factory.makeView();
}

オブジェクト生成処理の大半をControllerに任せたのでDependency Injection方式と比較するとMainがすっきりしています。 しかし「Abstract Factoryパターン方式1」と比較するとMainに冗長な処理が多いです。 このままでもいい気がしますが、折角なのでMainにもう一手間加えた例も考えてみましょう。

// Main.java
//  - 冗長な繰り返しを排除
//  - ControllerFactoryMapsはController実装クラスとFactory実装クラスの組み合わせを定義したList
//  - getObjectはReflectionでオブジェクトを作成するprivateメソッド
for ( map : ControllerFactoryMaps ) {
  Factory factory = (Factory) getObject(map.factory);
  Controller controller = (Controller) getObject(map.controller, factory);
  routes.put(path, controller);
}

これで今後ユースケースが増えてもMainをきれいに保つことができるでしょう。 でも、Reflectionは好みが分かれそうですし、ユースケースが少ない場合は過剰設計になるかもしれません。 Mainでここまでやるかはケースバイケースかなと思います。

全体の構造に目を向けてみましょう。

Controllerがオブジェクトのライフサイクルを管理できる点も活かせそうです。 上の例ではControllerのConstructorでオブジェクトを生成していますが、リクエストごとに生成することもできます。 そうした場合、CommandパターンのようにUseCaseオブジェクトに状態をもたせ、Undoやバッチ処理などを実現することも可能です。 リクエスト処理が終わればオブジェクトが消えるのでメモリリークの心配も少なくなるでしょう。

マルチスレッド環境ではControllerのみスレッド化して、UseCase、Presenter、ViewオブジェクトはRequestごとにスレッド内で生成すれば競合の心配が少なくなります。


使い所

Dependency Injection方式

  • TDDでは最初、これになる
  • ユースケースの数がとても少ないときはこのままでもよい

Abstract Factoryパターン方式1

Abstract Factoryパターン方式2

  • 上位層がライブラリ、下位層がユーザアプリのとき
  • システムの基幹部分を開発しており、Controllerが使うオブジェクトのLifecycleをControllerに管理させる要件があるとき