実装 - Hexagonal Redux -


Reduxのヘキサゴナルアーキテクチャ

Reduxの様なイベント駆動アーキテクチャでDDDの恩恵を受けるため、ヘキサゴナルアーキテクチャを採用するモデリングの考察です。疎結合なドメインモデル同士は、時に外部ドメインで扱う横断的関心事を参照する必要が出てきます。

Redux は SingleStore のため、全部入りのエンタープライズモデルと捉えられがちですが、それを構成する Reducer と対の Model が、マルチドメインを構成する単体のドメインモデルだと捉えています。先日投稿したモジュールの構成で、ヘキサゴナルアーキテクチャを採用する準備は出来ています。用語のマッピングは以下の通りです。

DomainModel = immutable.Record
DomainEvent = ReduxAction
Port(in) = ReduxReducer
Port(out) = ReduxStore
Adapter = Saga(Container/Provider)

上記マッピングでIDDD本を読むと、腑に落ちるところが多いと個人的に感じています。本稿で取り上げる内容は、saga が横断的関心事の参照を解決する責務を担っているという点です。

ドメインイベントと呼ばれるものは ReduxDDD には存在しないと筆者は考えており、代わりにドメインアクションと呼んでいます(普通のReduxアクションと全く同じ)

【IDDD本で示されているヘキサゴナルアーキテクチャ】

サブドメインの種類

  • 汎用サブドメイン
  • 支援サブドメイン
  • コアドメイン

汎用的に利用出来るものは「汎用サブドメイン」で、上流のドメインにあたり、純度が高いです。

汎用サブドメインのモデルをマッピングし、独自の問題領域を解決する中流のドメインが「支援サブドメイン」です。ここより下流は Saga が何らかの形で関与してきます(後述)

この2種類のサブドメインを利用してもなお、解決できない問題を取り扱うサブドメインが「コアドメイン」にあたり、特定のエンドポイントでしか利用しないようなアプリケーションモデルを扱います。コアドメインモデルは粒度に応じて複数に分割されることもあります。

境界付けられたコンテキストの結合

汎用サブドメインモデルは、継承することで育てますが、他のサブドメインからは独立しています。支援サブドメインやコアドメインは、複数の他サブドメインモデルを、自身のドメインモデルへとマッピングする必要があります。では、Modelはどこで・どの様に結合するのが適切でしょうか?

【ドメインクライアントで結合させる?】

props で渡される Model インスタンス を ViewComponent 上で合成するという結合例です。これはクライアントに副作用が含まれ、ドメインがクライアントに依存しているという点から、そのロジックに依存する箇所が多くなってきた場合辛くなります。できる限りこの方法は避けたいところです。

【サブドメインがAPIを公開する?】

サブドメインが外部サブドメインに向けてリモートプロシージャコール(RPC)を公開することも検討できます。しかしドメイン内で結合が完結する場合は、この手法よりも、ドメインアクションを受けたアダプターで結合する方が柔軟でしょう。

【アダプターで結合する?】

react-redux が提供する様なドメインアクションを受ける Provider もアダプターの役割ですが、View向けのアダプターのため、ここで取り扱うべきではありません。以上の観点から長期プロセスとして常駐しているアダプターの saga が適切という解に向かいます。

ドメイン層構築までのおさらい

複数のサブドメインモデルに、一つずつ reducer という input port を繋ぎ、store という observable な一枚の大きな「膜」で全てのサブドメインモデルを覆います。

entry.js
combineReducers({
  user: UserReducer(new UserModel()), // 汎用サブドメインモデル
  todos: TodosReducer(new TodosModel()), // 支援サブドメインモデル
  auth: AuthReducer(new AuthModel()), // 支援サブドメインモデル
  app: AppReducer(new AppModel()) // コアドメインモデル
})

下流サブドメインがドメインアクションを saga で購読し、task内でコンテキストマッピングすることで、自身のドメインモデルへと値を変換していきます。

rootSaga.js
export default function * rootSaga () {
  yield fork(todosSaga) // 支援サブドメイン業務
  yield fork(authSaga) // 支援サブドメイン業務
  yield fork(appSaga) // コアドメイン業務
}

sagaMiddleware.run(rootSaga)
appSaga.js
import { types as UserActionTypes } from '~/path/to/user/reduxBoilerPlate'
import { types as TodosActionTypes } from '~/path/to/todos/reduxBoilerPlate'
import { creators as AppActionCreators } from '~/path/to/app/reduxBoilerPlate'

export default function * appSaga () {
  while (true) {
    // 上流サブドメインアクションを購読
    yield take(action => action.type === UserActionTypes.updateUser || TodosActionTypes.updateTodo)
    // 上流サブドメインモデルを参照
    const { user, todos } = yield select()
    const userName = user.getUserName()
    const filteredTodos = todos.getFilteredTodos()
    // 自身のドメインモデルエンティティへ変換
    yield put(AppActionCreators.setSomeMappedValue(userName, filteredTodos))
  }
}

これが筆者の考える Hexagonal Redux のアウトラインです。新たなドメインが介入してくると、既存のドメインが適度な粒度に分裂し、各々の業務が減ることが想定されます。同時に、境界付けられたコンテキストをマッピングする saga task が増えるでしょう。必ずしもsagaである必要はありませんが、Hexagonal Redux において疎結合なモデルを繋ぐのはアダプター(サービス層)のはずです。

MVVMに見られる Observable な VM よりも手順が多いですが、ドメインモデルが膨大な量になった時、モデル間マッピングの制御が柔軟に変更できる様に思います。