redux-aggregate x TypeScript
- ドメインモデル設計 -

redux-aggregate とは

先日「まぼろし」さん主催の参加者全員LT会に参加させていただきました。その際の発表で、自身が npm で配布している redux-aggregate をご紹介。こちらの記事でも先行して紹介しており、redux-aggregate は少ない手順で TypeScript Redux アプリケーションを実装することが出来るヘルパーモジュールです。

ここまでの資料では 「状態変更の手続きを減らす・隅々まで型安全である」 という利点を挙げました。

本稿ではもう少し掘り込んだ内容として、従来の Redux では難しかったデザインパターンの実装が可能になることを紹介します。

状態変更から Redux の知識を排除


従来の reducer は二つの責務を抱えていました。

  • 単一Store で発生する Action の「選り分け」
  • 状態を変更する処理

私はこの責務混雑が reducer の unitテスト や 型定義の煩雑さに繋がっていると考えています。redux-aggregate が要求している、受け取った状態を immutable に変更するだけの mitation 純関数。そこに Redux の知識は一片もありません。

Counter.ts
const initialState = => ({ count: 0 })

function increment(state) {
  return { ...state, count: state.count + 1 }
}
function setCount (state, value) {
  return { ...state, count: value }
}
export const Mutations = {
  increment,
  setCount
}

この責務の線引きを行なっている点も、redux-aggregte の特徴です。redux の知識は、store.ts に全て格納されることになります。

実行時に依存性を注入する


いかなる Reduxアプリケーションも、実行時に createStore を実行し、その store を依存として注入しなければ、利用者である middleware / view は機能しません。

従来のモジュールシステムにより import される 各種ボイラープレートは、単一ファイルに閉じられた static class member とも捉えることが出来ます。reducer は実行時に store に注入されるにも関わらず、その振る舞いは実行前に決まっていました。

動的な reducer が強力な理由はこちらのサンプルにある通りで、状態変更定義や state スキーマが同じものであっても、その同期トランザクション同士は干渉しません。各集約のコンテキスト・依存性の注入は実行時に決定することが出来ます。ボイラープレートが静的なものではなく、動的なものであるべきだと考えいているのは、DXのためだけでは無いということです。以下はREADME に記載しているコードです。

Store.ts
export interface StoreST {
  counter1: CounterST
  counter2: CounterST
}

// ______________________________________________________

export const Counter1 = createAggregate(CounterMT, 'counter1/')
export const Counter2 = createAggregate(CounterMT, 'counter2/')

// ______________________________________________________

function storeFactory<R extends ReducersMapObject>(reducer: R): Store<StoreST> {
  return createStore(
    combineReducers(reducer),
    composeWithDevTools()
  )
}

export const store = storeFactory({
  counter1: Counter1.reducerFactory(
    CounterModel({
      name: 'COUNTER_1',
      count: 0,
      bgColor: '#ccc'
    })
  ),
  counter2: Counter2.reducerFactory(
    CounterModel({
      name: 'COUNTER_2',
      count: 10,
      bgColor: '#eee'
    })
  )
})

継承で実装するユースケース層


継承(mixin)を用いて、関数型でありながらオブジェクト指向のデザインパターンを輸入することが出来ます。こちらのサンプルがそれを指しており、TodosPresent ドメインモデルは Todos ドメインモデルを継承したユースケース層です。

フラグによるフィルタリング(プレゼンテーション向けに用意されたエンティティ)は、このサブクラスとも呼べるレイヤーでのみ扱うことが出来ます。Queries という概念を、同一ファイルで定義することを提唱しているのも、このためです。

TodosPresent.ts
import { TodosST, TodosQR, TodosMT, TodosModel } from './todos'
import { TodoST } from './todo'

// ______________________________________________________
//
// @ Model

export interface TodosPresentST extends TodosST { // State スキーマの継承
  showAll: boolean
}
export const TodosPresentModel: Modeler<TodosPresentST> = injects => ({
  ...TodosModel(), // State エンティティの継承
  showAll: true,
  ...injects
})

// ______________________________________________________
//
// @ Queries

function getDoingItems (state: TodosPresentST): TodoST[] {
   // file private method
  return state.items.filter(item => !item.done)
}
function getVisibleItems (state: TodosPresentST): TodoST[] {
   // public method
  return state.showAll ? state.items : getDoingItems(state)
}
export const TodosPresentQR = {
  ...TodosQR, // Queriesの継承
  getVisibleItems,
}

// ______________________________________________________
//
// @ Mutations

function toggleShowAll (state: TodosPresentST): TodosPresentST {
  return { ...state, showAll: !state.showAll }
}
export const TodosPresentMT = {
  ...TodosMT, // Mutationsの継承
  toggleShowAll
}

注意点として、それは全ドメインモデルに共通した振る舞いやエンティティを持たせる、ということとイコールではありません。「各ドメインが持つべき知識と振る舞いの凝集化」は、設計者に委ねられることになります。

おわりに


動的ボイラープレートのパフォーマンスや実行時のCPU負荷はどうなのか?という疑問につきましては、筆者の確認した範囲(類似のジェネレータを prod コードで利用してきた経験)から影響はありません。コードベースに大きく関わるところなので、気になる方は各位調査の上でご利用ください。総転送量も加味すると、むしろ良いケースもあるのではと考えています。

今回、npm で配布するに至ったのは、静的型付けの問題を克服出来たからに他なりません。TypeScript は進化の速度が速く、ある日今まで不可能だったことが可能になる期待値が高いです。動向を追いながら利用方法を考え続けること、Generics の定義に慣れることで、開発効率を上げる一助になるのではないでしょうか。是非、挑戦してみてください。