Usecaseを使おう(コードのentry pointからロジックを分離する方法の例)
前提
例によって Clean Architecture のあの図より。
Clean Architecture 自体はいろいろ誤解しやすかったりするが、他の類似のパターンも言ってることはそんなに大きく違いはないのでそのまま参照する。
まず、基本的なことを確認しておくと、
いわゆるフレームワークでコードの起点になる部分はロジックを書く場所ではない
という点がある。
例えば
- Rails MVC は ( routing を除けば ) Controller が起点になるが、Controller は Usecase の外側に位置している
- React 的な View Framework は Presenter 辺り
以上のように、メジャーなフレームワークのコードの起点となるパーツは、基本的にロジックを書く場所として機能するはずの Usecase や Entity ではない。そしてこのズレはよく
- Fat Controller 問題
- Component の設計問題
として浮上する。
例えば Vue では plugin でネットワークやストレージの I/O を直接 component の中に持ってくるアプローチが有名だが、これは上の図に照らし合わせると二つの意味で不適切な考え方をしている。
- Presenter の中で直接、他の Gateway に依存している
- 他の Gateway の結果を Presenter の中で受け取るのでその結果に対するロジックも Presenter の中に書いてしまいやすい
しかしそもそもそんなところにアプリケーションロジック、ビジネスロジックを書くべきではない。(View を規定するロジックだけが含まれているべきである。)
これはちょっと視点を変えて Web UI と同じ機能を CLI でも使いたいと思えば簡単に分かるのだが、CLI コマンドを作れると自分で思っていない人には想像しにくいのかもしれない。
ではいわゆるModelがロジックを担う場所なのか
Rails MVC では伝統的に「ロジックは Model に書け」と言われていた。そのため「DBMS を扱わない Model を作ってよいか?」がたびたび疑問に挙がっていた。
これは Rails 本体が示す方法に M と V と C しかないので当然の答えになるし、Model を守るのであれば DBMS にアクセスしないものも Model にしてしまえばよい、という結論になる。1
しかし DBMS 以外にも I/O はたくさんあり、やはり I/O を直接扱うレイヤーにロジックを書くとテストしにくいことは多い。例えば Web サーバサイドではクラウド前提になり、フルマネージドなサービスを利用するケースでは実際の動作をすぐに手元で再現できるとは限らない。
また Web クライアントサイドではいっとき State 管理だけが脚光を浴びて、以下のような Store が Model であるかのような言説が多い
Redux Fundamentals, Part 6: Async Logic and Data Fetching | Redux
が、Store はあくまで State を中心にできており、State の変更をガードするためのものであり、State は各種 I/O とのやりとりの結果の写像になるものであり、view へ反映しやすいことを重視して設計されている。
データを中心に据えるのはデータのあり方そのものがルールである2 分には正攻法ではあるが、データの読み書きにはその理由があり、実際にはこの場合は A のルールに従うが、この場合は B のルールに従うというケースが十分あり得る。こうした場合にはデータではなく Usecase をロジックの中心に据えると収まりがよくなる。
もう一つ、Store に middleware を積み上げていくスタイルは、ある程度以上の経験のある人からすると依存オブジェクトまみれで非常に危険なにおいがするはずである。
結論としては、「ロジック」という言葉の意味する範囲(レイヤー)も 「Model」という言葉の意味する範囲(レイヤー)も実は多様で曖昧なので、一概に「ロジックは〇〇に書く」と決めつけない方がよい。
近年の自分の解答例 - Vueで複雑な処理を書きたい場合 -
だいたい
Almin · Flux/CQRS patterns for JavaScript application.
を参考に Usecase オブジェクトを抽出して、いわゆる Model を Component と Usecase で共有し、Component はほとんどすべて props を読むだけ、副作用は Usecase に丸投げ、という形で実装するようにしている。3
イメージとしては「子component は props を受け取り event を投げるだけ」の延長で、「Vue component は Model の変化を受け取るだけで副作用は Usecase に任せる」感じである。
副作用 | メモ、その他 | |
---|---|---|
Gateway | 自由 | ネットワークやストレージの一つ上のレイヤー |
Usecase | Gatewayを利用して副作用を司る | ViewにもGatewayにも収まらないロジックを担う |
親Component | Usecaseに任せる | Gatewayから見える値を反映 |
子Component | Eventを発生するだけ | propsをtemplateに適用するだけ |
コード例で近いのは
View FrameworkインスタンスをControllerに預けて管理してもらう現実的で雑なClean Architecture (2019-03-02) | あーありがち
だが、component 自身を預けるのではなく、あくまで依存オブジェクトを与える形になっている。
これは Vue 3 から API が変わり、Vue の中と外のインタラクションを直接扱う方法がなくなってしまったためでもあるが、結果的によかったと思っている。
この Usecase から I/O の Gateway を叩いてロジックを組み立てる方法の何がよいかというと、
- (スキルと人手があれば)ロジックの実装と UI の実装のどちらもお互いにブロックしない
- なんなら UI のフレームワークは変えてもよい
点である。先にフレームワークやその plugin, middleware などを調べ切らなくてもよいし、一つのツールでゴリ押しする必要もない。Virtual DOM でも生 DOM でもその時々で自由に切り替えてよい。
Usecaseオブジェクトの実装方法
これは自由。Almin.js では POJO で書けるよとしているが、個人的にはなんらかのレールがあってよいかなと考えていて、よく Interactor を利用している。
- collectiveidea/interactor: Interactor provides a common interface for performing complex user interactions.
- V1.3: Interactors | Hanami Guides
- vadimdemedes/interactor: Organize logic into interactors
- interlock/promise-interactor: Promise pattern applied to the interactor pattern applied to the JS
Interactor はいわゆる Command パターンを実現するもので、一つだけ public メソッドを持つ。
注意点として、Hanami::Interactor 以外は DI に向いていない点がある。ベースが Rails っぽい文化に寄ってるところがあるのでグローバルにアクセス可能なオブジェクトに依存している部分がある。これは前提に置いた Clean Architecture に反している(Usecase から外側の Gateway などに依存している)のだが、例えば Controller から反対側の DB などに依存しているよりははるかにマシで、Usecase のすぐ外側の近い領域にだけ依存し、I/O の種類が変われば Usecase も変わるようにしたうえで Usecase を organize すれば、かなり影響は限定的だし、テストの際に差し替えるのもさして難しくないと考えている。
この際、「Usecase なの? Interactor なの?」と思うかもしれないが、再利用できるものは Interactor としての置き場所に収めておき、entry point から直接呼ばれるものを Usecase とするのがよさそう。たまたま使っている道具は Interactor で、役割としては Usecase と Interactor があるという形。
Usecase の中には処理的にはそれなりに重複ができるが、Usecase が他の Usecase に依存するのはおかしいので、抽出できる部分を Interactor に。
まぁ、無理に Interactor を使う必要はもちろんないので、この辺りはほんとに自由に考えればよいと思う。(だから難しいんだけど。)
DBMS についてはもはや20世紀とは違っていい具合に抽象化してくれるライブラリがたくさんあるしマシンスペックも高いので、愚直に I/O を叩いてもよいのでは?と思うシーンも多い。ただし、あくまで上の図を考慮すると Gateway にロジックを書いていることには注意しておく方がよい。 ↩
例えば金額であれば金額を満たすルール、身長、体重などはそれぞれのルールがある。そういうルールはデータを中心に定義すべき。 ↩
実際には手癖になっていてスッと書けるというレベルではなくて、書くたびに「あぁ、そういえばこういうことをやりたいんだった」と帰ってくる感じ。特に Rails MVC のような強いフレームワークのない領域を扱ったり、どうしても JavaScript で書く必要があって非同期の処理に頭を悩ませる際に顕著。これは Web クライアントサイドに限った話ではないが、分かりやすさのために Vue を例にしている。 ↩