Vuex + Clean Architectureを頑張ってみた
なぜ
- Vue, Vuexのアプリが大きくなると、様々なロジックが様々なところに散在する結果となったことがあったので、ロジックの集約・ロジックとライブラリ依存の実装の分離を目的として、Vuexの流れに従いながらClean Architectureを入れてみることをチャレンジしました
- 一緒に開発する人がVueやClean Architecture初めてなこともあるので、Clean Architectureを徹底することはせず、ミニマムでClean Architectureの要素を入れました
行なったこと
Vuexの一般的データフローでActionで行われることが多い「データを外部から取ってきてstateに保存する」「ユーザ操作により追加・削除・変更されたデータをstateに保存する」という処理を以下のようにClean Architecture化しました
上記のフロー(矢印はデータの流れの向き)によって以下が実現できるようになります
- 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内で生成するようにしました
// 型宣言が冗長になってしまうを避けるため以下の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用のオブジェクトを保存することで、ドメインをそのまま保存しないようにしました
- ちなみに以下のItemsStateやItemOrderStateはクラスである必要がないのでインターフェースで宣言しました
export const state = () => ({ items: { byId: {} as ItemsState, order: {} as ItemOrderState, }, });
行ってみてどうだったか
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: できれば実現したい要望
- 各メンバーが誰と一緒にグループを組みたいか
- 各メンバーが誰と一緒にグループを組みたくないか
- 過去の組み合わせの結果 (過去組み合わせたメンバー同士はグルーピングを避ける形にする)
- Requirement: 必ず従うべき要求
- アウトプットはグループ分けされたメンバーと各グループがどれくらいいい感じなのかを示すスコア
FP with Clojure -> OOP with Kotlinにしてみて気づいたこと
FPでドメインサービスだと思っていたものがOOPではドメインサービスではなかった
- Clojureアプリではドメインサービスとして表現している処理があった
- Clojureアプリではドメインロジックを以下の抽象的な処理に分けていた
- grouper: インプットをもとにグループの組み合わせを生成する
- evaluator: グループがRequestをどれくらい反映しているかをスコアとして付与する
- picker: 複数のグループの集合から何かしらの基準で一番良いとされるものを選択する
- これらの関数のインプットは引数として渡したり、(何度も呼ばれる関数に関しては)クロージャすることで渡していたのだが、インプットもアウトプットも基本は値オブジェクトの役割を持つmapを使用していた
- つまり私の頭の中では上記の3つの抽象的な処理は値オブジェクトを消費・生産するが自身はデータを持たない = ドメインサービスだと認識していた
- Clojureアプリではドメインロジックを以下の抽象的な処理に分けていた
- さて、以上と同じロジックをKotlinで実装しようとすると、何が起こったか
- OOP→FPの移行であれば当たり前に気づくものではあるが、(ドメインロジックがすべてドメインサービスになる)FP→(ドメインサービス・値オブジェクト・エンティティという選択肢がある)OOPに移行するとFPで通用したドメインモデルを再考する必要がある
振る舞いの持たせ方を注意深く考える必要がある
- Kotlinだと振る舞いを与える場合に、クラスのメソッド・Companion Objectのメソッド・拡張関数・トップレベル関数と選択肢が多いので、注意深く表現方法を考える必要がある
- 正解が考えられるほどまだ実践が足りないのだが、自分なりに良いと思っている分け方は以下の通り
- 値オブジェクト・エンティティの振る舞い→クラスのメソッド
- ドメインサービスの振る舞い→Companion Objectのメソッド > トップレベル関数 > データを持たないクラスのメソッド
- トップレベル関数は(クラス名がない分)比較的ドキュメント性が低いor良い関数名がつけにくいという意味でドメインを表現するという意味で難易度が高い(トップレベル関数を多用するならパッケージの命名から工夫が必要だと考える)。ドメインレイヤーの外で使われる関数でありコンテキストが限られているのであれば比較的使いやすい。
- データを持たないクラスのメソッドは、ドメインを記述するという文脈で正しくないという判断。また、ユースケース内でそのクラスを生成することになるので、単体テストでモックするのが難しくなる
- 上記二つほどデメリットがないという意味でCompanion Objectのメソッドを一番良しとしているが、今回使っているモックライブラリでいい感じにモックできないのでモヤモヤしている
- ドメインレイヤーの外で値オブジェクトなどに関連づけたい場合→拡張関数
FPにおける設計を学ぶ②: 実践から身に沁みた当たり前のこと
こんにちは
前回に引き続きまだまだFPを学んでいます
実際にClojureのアプリを設計して実践からFPの設計を学びました
行ったこと
- 急な目的のために急いで作った grouper というグループ分けのアプリに簡易Clean ArchitectureとFP的な考えを取り入れて改善を試みました
- 大まかに改善したこと
身に沁みたこと
- 関数を組み合わせて何もかも実現する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の様々な概念や考え方を幅広く扱っている
- メモ
The Power of Composition - Scott Wlaschin
- 概要
- Functional Design Patternsと多少内容が被るが、関数をどう組み合わせやすくするかにフォーカスしている
- メモ
- 引数1つ、返り値1つの関数を目指すと関数同士の組み合わせがしやすくなる
- 引数を1つだけにする: 事前に引数を埋め込んだ関数を生成する
- 条件分岐が発生する関数を組み合わせたい時: 条件分岐後のアクションを表現する関数を引数で渡すと良い
Functional architecture - The pits of success - Mark Seemann
- 概要
- メモ
Bottom Up vs Top Down Design in Clojure - Mark Bastian
- 概要
- メモ
- 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をイベントとして登録し、イベントの種類とパラメータを渡すと発火できる構想
- 1つのハンドラーで行うことを大きな関数ではなく1つのmapで表現している
Keynote: Transparency through data by James Reeves
- 概要
- DSLによって予測がしやすいコード=透明性の高いコードを書きましょうという話
- メモ
- 透明性のあるコード=予測がしやすいコード
- 制約を設ける(静的な型、イミュータブル、純粋関数など)と予測がしやすくなる
- 強力なものほど、制約が少ないので、予測がしにくくなる (Rule of least power)
- DSL(Domain Specific Language) = Least power
- map: 純粋関数の1つの引数と返り値の対応づけができる
- 分配束縛: 制約によって予測がしやすくなる
- ルーティング: Compojure vs ataraxy
- ClojureでDSLを実践するには