Vuex + Clean Architectureを頑張ってみた

なぜ

  • Vue, Vuexのアプリが大きくなると、様々なロジックが様々なところに散在する結果となったことがあったので、ロジックの集約・ロジックとライブラリ依存の実装の分離を目的として、Vuexの流れに従いながらClean Architectureを入れてみることをチャレンジしました
  • 一緒に開発する人がVueやClean Architecture初めてなこともあるので、Clean Architectureを徹底することはせず、ミニマムでClean Architectureの要素を入れました

行なったこと

Vuexの一般的データフローでActionで行われることが多い「データを外部から取ってきてstateに保存する」「ユーザ操作により追加・削除・変更されたデータをstateに保存する」という処理を以下のようにClean Architecture化しました

f:id:yukithornton:20200630222029p:plain

上記のフロー(矢印はデータの流れの向き)によって以下が実現できるようになります

  • Vuexの単方向フローを守りながら、アプリのロジックをユースケースドメインで表現できるようになる
  • ユースケースドメインはVuexというライブラリを知らずに済むので、例えばReact, Reduxに移行する場合でも変更箇所を最小限に抑えられる

StateGatewayに対してDriver層の何かを足しても良いとは思ったのですが、他のメンバーのことを考えて軽量にしたかったのと、mutaitonを呼ぶcommitを呼ぶだけの関数をこの段階で追加することによってあまり旨味が出るように思えなかったので、一旦StateGatewayだけに留めておきました

どう実現したか

Nuxt.js + Vuex + Typescriptでの実装例を紹介します

  • Vue Component, 各種Port, ItemGateway, ApiClient, ドメインクラスは特別なことはしていないので説明を省略します
  • 適当な名前としてItem~という名前を使っています

Actionの実装

Vuexの実装とClean Architectureの実装の接点となるところなので一番悩みました

  • storeを参照したりcommitを呼んだりするStateGatewayがどうしてもActionの引数であるActionContextを必要とするので、Actionが呼ばれるごとにUsecase, StateGatewayのインスタンスを生成するようにしています
    • インスタンスを無駄に生成しないようにするには、Usecaseの関数を呼ぶ時に引数としてActionContextを渡す事も考えられましたが、UsecaseがVuexに依存するのは避けたかったのでこのような実装に落ち着きました
  • Usecaseに渡すドメインインスタンスはAction内で生成するようにしました
    • Component側にあまりドメインの知識を散在させたくなかったこと、ActionとUsecaseの間にもう一つレイヤーを用意するとさらに煩雑になることから、一旦Action内にドメインインスタンスの生成するロジックを集約させることにしました
// 型宣言が冗長になってしまうを避けるため以下の1行は入れています
type Ctx = ActionContext<RootState, RootState>;

const itemGateway = new ItemGateway(new ApiClient());
const usecase = function(context: Ctx): Usecase {
  return new Usecase(
    itemGateway,
    new StateGateway(context),
  );
};

export const actions: ActionTree<RootState, RootState> = {
  load(context: Ctx): void {
    usecase(context).load();
  },
  addItem(context: Ctx, item: String): void {
    usecase(context).addItem(new ItemDomain(item));
  },
}

Usecaseの実装

ドメインの操作の詳細はドメインのクラスに実装している想定で、Usecaseではドメインを使ったデータの参照・保存の大まかな流れを記述しています

  • ItemPort: 外部からのデータの参照・保存
  • StatePort: ステートの参照と保存

この例ではitemをどの順番で表示するかをドメインロジックとして表現したいという想定をしているため、ドメインクラスの関数addItemを呼ぶようにしました

export default class Usecase {
  constructor(private itemPort: ItemPort, private statePort: StatePort) {}

  async load(): Promise<void> {
    const items = await this.itemPort.findItems();
    this.statePort.storeItems(items);
  }

  async addItem(item: ItemDomain): Promise<void> {
    const savedItem = await this.itemPort.saveItem(item);
    const items = this.statePort
        .findItems()
        .addItem(savedItem);
    this.statePort.storeItems(items);
  }
}

StateGateway

  • StateGatewayではActionContextをつかってcommitやstateに直接アクセスしています
  • ドメインインスタンスをそのままstateに入れることを避けたかったので(後述)、ドメインに内包されるデータををstateにとって最適な形に詰め直し、commitを呼ぶようにしました。逆方向の変換も同じです。(詳細な実装は省きます)
export default class StateGateway implements StatePort {
  constructor(readonly context: ActionContext<RootState, RootState>) {}

  storeItems(items: ItemListDomain): void {
    // toItemsState, toItemOrderStateはドメインからState格納用のオブジェクトに変換する
    this.context.commit('SET_ITEMS', toItemsState(items));
    this.context.commit('SET_ITEM_ORDER', toItemOrderState(items));
  }

  findItems(): ItemListDomain {
    // toItemListDomainはState格納用のオブジェクトからドメインに変換する
    return toItemListDomain(this.context.state.items.byId, this.context.state.items.order);
  }
}

Mutation

MutationではGatewayから渡されたものをそのまま入れるようにしています

export const mutations: MutationTree<RootState> = {
  SET_ITEMS: (state, items: ItemsState) => {
    state.items.byId = items;
  },
  SET_ITEM_ORDER: (state, order: ItemOrderState) => {
    state.items.order = order;
  },
}

State

  • stateは変更の単位やアクセスのしやすさを考えて正規化しています
    • Vuex用の良い記事が見つからなかったので、ReduxのNormalizing State Shapeを参考にしています
  • State用のオブジェクトを保存することで、ドメインをそのまま保存しないようにしました
    • 正規化とも同じですが、巨大なドメインが頻繁に更新されるとなんどもComponentの再レンダリングが走ってしまうからです
    • ドメインの知識がVue ComponentやGetterに散在してしまうことによって、それぞれの責務が不明瞭になることを防ぎたかったからです
  • ちなみに以下のItemsStateやItemOrderStateはクラスである必要がないのでインターフェースで宣言しました
export const state = () => ({
  items: {
    byId: {} as ItemsState,
    order: {} as ItemOrderState,
  },
});

行ってみてどうだったか

  • 単体テストが非常に書きやすくなりました
    • Cypressをつかった統合テストは導入しているものの、特にドメインロジック周りは可能な限り単体テストも書きたいと思っていました
    • ライブラリの依存を局所的にすることでテストしやすい領域が増えました
  • コードの量は圧倒的に増えたが、機能を変更したい時に変更すべき箇所が誰にでもわかるようになっているのが成果だと感じています
  • Getterの使い分け、storeに保存するか迷うようなstateの扱いなど、まだまだ考えきれていないところがあるので、様々な種類のユースケースを実装する中で設計を洗練させていきたいです

ClojureのアプリをKotlinで書き直してみた (途中経過)

※この記事は学習の途中経過を書いた記事のため、今後更新があるものとしてお読みください

なぜ

  • FPな言語からベースがOOPな言語に書き直すことで、FPとOOPの違いを噛みしめることができるのではないかという仮定
    • 設計の仕方の違いを学ぶことで、今後(ミクロでもマクロでも)設計の選択肢の幅が広がるのではないか
  • Kotlin in Actionを読んでKotlinを体系的に学んだので、その知識を元に設計・実装することでKotlinの実践力を身につけたかった
  • Clojureのアプリは業務でたまに活用する機会があるものだが私しか使っていなかったので、Kotlinであれば他の人も環境構築・機能追加がしやすいのではないかという仮定

行なっていること

  • グループの組み合わせを作成するClojureアプリgrouperをKotlinアプリkt-grouperで書き直している (現在進行形)
  • 同じ機能を持ち、同じドメインを表現するものとした (機能追加は書き直しが終わってからの予定)
  • Clojureアプリと同様、Clean Architecture, DDDを意識して設計した
  • まずはClojureのnamespaceをKotlinのパッケージorクラス、Clojureの関数をKotlinのトップレベル関数またはクラスメソッドにしてみた
    • 違和感を感じたものからOOP or Kotlinならではの表現に変更していった

アプリの簡単な説明

  • 複数のメンバーをいいかんじにグルーピングするアプリ
  • 具体的なインプットやアウトプットはgrouperのREADMEを参照
  • インプットの内訳は以下
    • Requirement: 必ず従うべき要求
      • 対象のメンバー
      • 作成するグループの数
    • Request: できれば実現したい要望
      • 各メンバーが誰と一緒にグループを組みたいか
      • 各メンバーが誰と一緒にグループを組みたくないか
      • 過去の組み合わせの結果 (過去組み合わせたメンバー同士はグルーピングを避ける形にする)
  • アウトプットはグループ分けされたメンバーと各グループがどれくらいいい感じなのかを示すスコア

FP with Clojure -> OOP with Kotlinにしてみて気づいたこと

FPでドメインサービスだと思っていたものがOOPではドメインサービスではなかった

  • Clojureアプリではドメインサービスとして表現している処理があった
    • Clojureアプリではドメインロジックを以下の抽象的な処理に分けていた
      • grouper: インプットをもとにグループの組み合わせを生成する
      • evaluator: グループがRequestをどれくらい反映しているかをスコアとして付与する
      • picker: 複数のグループの集合から何かしらの基準で一番良いとされるものを選択する
    • これらの関数のインプットは引数として渡したり、(何度も呼ばれる関数に関しては)クロージャすることで渡していたのだが、インプットもアウトプットも基本は値オブジェクトの役割を持つmapを使用していた
    • つまり私の頭の中では上記の3つの抽象的な処理は値オブジェクトを消費・生産するが自身はデータを持たない = ドメインサービスだと認識していた
  • さて、以上と同じロジックをKotlinで実装しようとすると、何が起こったか
    • 上記の3つの処理の引数や返り値に使うものとして値オブジェクトのクラスが大量にできる
    • たとえばInputといったインプット情報を全て持つ値オブジェクトができ、それをGrouperクラスの引数にするとする
    • GrouperクラスはInputクラスしか使わないので、データと振る舞いが分かれている理由はなんだっけ?むしろGrouperクラスいるんだっけ?となる
  • OOP→FPの移行であれば当たり前に気づくものではあるが、(ドメインロジックがすべてドメインサービスになる)FP→(ドメインサービス・値オブジェクト・エンティティという選択肢がある)OOPに移行するとFPで通用したドメインモデルを再考する必要がある
    • →同じドメインを表現するものでも、FPとOOPではドメインモデルの実現方法は大きく異なる
    • よって関数が主役だったFPからクラスが主役のOOPドメインモデルの組み立て方を合わせていく必要がある

振る舞いの持たせ方を注意深く考える必要がある

  • Kotlinだと振る舞いを与える場合に、クラスのメソッド・Companion Objectのメソッド・拡張関数・トップレベル関数と選択肢が多いので、注意深く表現方法を考える必要がある
  • 正解が考えられるほどまだ実践が足りないのだが、自分なりに良いと思っている分け方は以下の通り
    • 値オブジェクト・エンティティの振る舞い→クラスのメソッド
    • ドメインサービスの振る舞い→Companion Objectのメソッド > トップレベル関数 > データを持たないクラスのメソッド
      • トップレベル関数は(クラス名がない分)比較的ドキュメント性が低いor良い関数名がつけにくいという意味でドメインを表現するという意味で難易度が高い(トップレベル関数を多用するならパッケージの命名から工夫が必要だと考える)。ドメインレイヤーの外で使われる関数でありコンテキストが限られているのであれば比較的使いやすい。
      • データを持たないクラスのメソッドは、ドメインを記述するという文脈で正しくないという判断。また、ユースケース内でそのクラスを生成することになるので、単体テストでモックするのが難しくなる
      • 上記二つほどデメリットがないという意味でCompanion Objectのメソッドを一番良しとしているが、今回使っているモックライブラリでいい感じにモックできないのでモヤモヤしている
    • ドメインレイヤーの外で値オブジェクトなどに関連づけたい場合→拡張関数

FPにおける設計を学ぶ②: 実践から身に沁みた当たり前のこと

こんにちは
前回に引き続きまだまだFPを学んでいます
実際にClojureのアプリを設計して実践からFPの設計を学びました

行ったこと

  • 急な目的のために急いで作った grouper というグループ分けのアプリに簡易Clean ArchitectureとFP的な考えを取り入れて改善を試みました
  • 大まかに改善したこと
    • 一つのファイルにごちゃっとロジックが詰まっていた → Clean Architectureでドメインロジックの分離
    • 複雑すぎて実質ブラックボックスになっているグループ分けロジックの流れ → ユースケースドメインで抽象化することで大まかな流れを表現した
    • 汎用的でない関数ばかり → 上記の抽象化もあわせて、今回のアルゴリズムに特化した関数と汎用的なグループ分けロジックを表現する関数で分割した

身に沁みたこと

  • 関数を組み合わせて何もかも実現するFPは、シンプルである故に、正しく命名するといったような設計の基礎の基礎といったスキルが試されます
  • 結果として「FPであれOOPであれ良い設計の考え方は基本変わらないこと」という当たり前のことが一番身に沁みました
  • よって以下では具体的に学んだ当たり前のことを将来の自分に問いかける形式で書いていきたいと思います

設計をするときに自分に問いかけたいこと

処理の詳細を考える前に、インプットとアウトプットのデータの形をちゃんと考えましたか?

  • 関数のインプット(=引数)とアウトプット(=返り値)を見極めましょう
    • FPでは最初のデータの形(データA)と最終的に欲しいデータの形(データB)を見極めて、データAをデータBに変換する関数を少しずつ作ります(参考: Bottom Up vs Top Down Design in Clojure - Mark Bastian
    • よって最初と最後のデータの形は特に時間をとって考えましょう
  • TDDでテストを書き終えたら、一度手を止めてデータの形を考えましょう
    • そのドメインを表現するのはそのデータの形で良いのでしょうか?
      • リストである必要がないのにリストを使ったりしていませんか
      • シンプルな形になっていますか

誰もがその関数名から同じ理解を得られますか?

  • 「その関数で行うこと」 を 「関数名 (+引数名)」が表現していますか?
  • 「関数名 (+引数名)」 から 「その関数で行うこと」をズバリ導けますか?
  • 上記ができていないとき
    • 単一責任の原則を守っていますか?

正しく抽象化できていますか?

  • 関数を組み合わせた時など、抽象的な表現が必要な時があります
  • 図に表現したり、言葉遊びをするとふさわしい表現が見つかるかもしれません
  • ものごとの性質だけではなく、「そのコンテクスト内での意味」を考えてみましたか?
    • 意味を考えると、その「やりたいこと」にとって本質的に何が必要で不要なのかということにフォーカスして考えることができます

それは行いたいことを一番シンプルに行う手段ですか?

  • 様々な選択肢の中から一番シンプルで実現可能な手段選びましたか?
  • 「できること」に飛びついていませんか?
    • 言語のシンタックスやライブラリはあくまでツールの一つです
    • そのツールを使うことでシンプルさを得られるのかを今一度考えてみましょう

FPにおける設計を学ぶ①: 視聴した動画リスト

こんにちは
FPにおける設計を勉強するために多数の動画を見たので、覚書としてその概要とメモをつらつら書きます
一部Clojureに特化した内容もあります

FP入門系

Functional Design Patterns - Scott Wlaschin

  • 概要
    • FPの様々な概念や考え方を幅広く扱っている
  • メモ
    • FPの3原則: 関数はモノである / なんでもコンポジションする / 型はクラスではない
    • 共通の値・振る舞いをパラメータ化する
    • 関数だけでなく型もコンポジションする
    • 引数を1つにすることによって関数を組み合わせやすくする
    • Monad, Monoid, Functor

The Power of Composition - Scott Wlaschin

  • 概要
    • Functional Design Patternsと多少内容が被るが、関数をどう組み合わせやすくするかにフォーカスしている
  • メモ
    • 引数1つ、返り値1つの関数を目指すと関数同士の組み合わせがしやすくなる
    • 引数を1つだけにする: 事前に引数を埋め込んだ関数を生成する
    • 条件分岐が発生する関数を組み合わせたい時: 条件分岐後のアクションを表現する関数を引数で渡すと良い

Functional architecture - The pits of success - Mark Seemann

  • 概要
    • OOPでベストプラクティスとされている概念はFPにも当てはまるものがあり、そして、OOPではその実践に継続的な努力が必要だがFPでは自然と実践できるというお話
  • メモ
    • Ports and Adaptors: FPでは純粋 / 非純粋関数を分けていくと自然とレイヤーに分離される
    • Entities/Value ObjectとServices: FPではそもそも値と振る舞いが分離されている
    • Testability: 純粋関数(=理想的な関数)は定義からして他のものから独立しているのでテストがしやすい

Bottom Up vs Top Down Design in Clojure - Mark Bastian

  • 概要
    • あるボードゲームをシステムに落とし込む時にOOPとFPでどういった設計になるかということを比較している
  • メモ
    • OOP = Top Down: インターフェースなどの定義と関連付けを洗練させていく
    • FP = Bottom Up: 最初のデータの形(データA)と最終的に欲しいデータの形(データB)を見極めて、データAをデータBに変換する関数を少しずつ作っていく

設計実践編

Thirteen ways of looking at a Turtle - Scott Wlaschin

  • 概要
    • 一つのシステムを例に、13の設計パターンとそのメリットデメリットを紹介している
    • 登壇者の記事「Thirteen ways of looking at a turtle」と合わせて読むとわかりやすい
  • メモ
    • 振る舞いとデータを分離する、データをイミュータブルにする、ステートの持ち方、DIの仕方などなど

Building composable abstractions - Eric Normand

  • 概要
    • 組み合わせ可能な抽象を洗練させるプロセスをベクター描画システムを例に紹介している
  • メモ
    • ①物理的なメタファーを使う: 作りたいものを表現できる物理的なものを使って議論する
    • ②意味を構築する: 部品とその関係性の意味にフォーカスして様々なケースを考える
    • ③実装をする

教養?

Simple Made Easy by Rich Hickey

Clojure寄り

Declarative Domain Logic – Rafal Dittwald

  • 概要
    • データの中にロジックを表現するという話
    • 登壇者はこの公演で話していることをtadaというライブラリで実現しようとしている
  • メモ
    • 1つのハンドラーで行うことを大きな関数ではなく1つのmapで表現している
      • mapの中は意味ごとにキーに分けれれている: パラメータをparamというキーに、認証・バリデーションの処理はconditionに、実際に行いたい処理をeffectに入れている
    • 上記のmapをイベントとして登録し、イベントの種類とパラメータを渡すと発火できる構想

Keynote: Transparency through data by James Reeves

  • 概要
    • DSLによって予測がしやすいコード=透明性の高いコードを書きましょうという話
  • メモ
    • 透明性のあるコード=予測がしやすいコード
    • 制約を設ける(静的な型、イミュータブル、純粋関数など)と予測がしやすくなる
    • 強力なものほど、制約が少ないので、予測がしにくくなる (Rule of least power)
    • DSL(Domain Specific Language) = Least power
      • map: 純粋関数の1つの引数と返り値の対応づけができる
      • 分配束縛: 制約によって予測がしやすくなる
      • ルーティング: Compojure vs ataraxy
    • ClojureDSLを実践するには
      • ループや再帰を避ける
      • matching and destructuringや静的な構造に目を向ける
      • ネームスペース付きキーワードや様々なデータの型を活用する
      • 複雑な文法にはclojure.specを活用する

Clojure Spec Expressing Data Constraints without Types - Alex Miller

  • 概要
    • clojure.spec(データや関数の構造を説明するためのライブラリ)の概要と使い方を具体的に説明している
  • メモ
    • predicative: そのデータや関数がtrueかどうかを表現する
    • 正しいデータを自動生成するのにも使える
    • 表現した型同士を構造化できたり、組み合わせたりすることができる
    • conformで一致したデータを任意の型に変換することができる (ベクタで表現されたデータがある型の全ての制約を満たしていればその型を表現するmapに変換される)
    • 一部の関数はプロダクションの前段階で使うべきだが、外部からのデータのバリデーションやDSLへの変換などランタイムで使用できるものもある