先日Vue.jsアーキテクチャリング勉強会にお邪魔致しまして、色々と触発されたので書いてみようと思います。
Vue.jsでWEBアプリを作る際に、クリーンアーキテクチャやMVVMの色んな記事を参考に色々試行錯誤しておりましたので、その途中経過というか現状についてまとめます。
クリーンアーキテクチャについて
実装クリーンアーキテクチャ
[DDD]ドメイン駆動設計で実装を始めるのに一番とっつきやすいアーキテクチャは何か
Clean Architectureで分からなかったところを整理する(#1~#4)
ここら辺の記事を参考にしてます。
上記リンク先にある、よく見る画像のレイヤー名やレイヤー数は一例であって、層を増やしたり、いらないと思った層を消したりするのは問題無いようです。
- 変化の多いものから変化の少ないものへの一方的な依存をしましょう。
というのが本質です。
Vue.jsはどこのレイヤー?
Vue.jsのガイドラインを見ると、Vue.jsはMVVMのViewModelについて注目しているとありますので、UIであるDOMとそれ以外のレイヤーをつなぐインターフェースアダプター層と捉えます。すなわち、ドメインやユースケースはVueを知りません。
また、MVVMの要項として、
- ViewとViewModelがあってそれ以外がModel。
- ModelはViewModelを知らない。
というのがあります。これらの制約を踏まえると、Vueというのは限りなくUI(インフラストラクチャ)に近いインターフェースアダプターです。従って、PresenterやControllerを作ってる場合はそれらはVueを知りません。
エンティティ(ドメイン)層
ドメインロジックはアプリケーションによって色々異なるので一概には言えません。しかし、恐らく以下の2つは大体のアプリケーションには含まれるんじゃないかと思います。
- UIのステート
- バックエンドにあるサービスの仕様
どんなアプリケーションにしろ表示するコンテンツがある以上そのコンテンツの入れ物は必須です。これはフレームワークに何を使うかは関係ありません。従って、UIのステートはドメインロジックで表現するべきかなと。
また、フロントエンドだけで完結するサービスはあまりなく、大抵はバックエンドにAPIがあるので、そのAPIの仕様もドメインロジックで表現するべきです。ここで注意しなければいけないのは、表現するのはあくまで仕様であり、プロトコルはなんだとか、渡すデータはクエリパラメータに入れるのかリクエストボディに入れるのか等の制約はインフラの話なので、ドメインロジックではAPIはどんなパラメータを受け付けるのか、何ができるのかなどをインターフェースとして定義するだけです。
ユースケース層
ユースケースはその名の通りユーザーがアプリを使う際に実現したいことや操作一個一個クラス化していきます。ある程度関係しそうな操作を一個のクラスにまとめてもよさそうですが、個人的には一個一個クラスにした方がいいと思います。理由としてはこちらのアプリケーションサービスの凝集度を高めたいの記事をご参照ください。
バックエンドのAPIを使う場合はドメイン層で定義したインターフェースをコンストラクタでDIして受け取ります。DIコンテナにはinversifyJSというのがあります。
インターフェースアダプター層
ここからVue.jsが出てきます。コンポーネント設計はドメインに縛られる必要はありません。Atomic DesignでもContainer componentでもなんでもいいです。
ユースケースの呼び出しは先程も出たアプリケーションサービスの凝集度を高めたいにあるMessage Busパターンを使います。Message Busは色々呼び方があるようですが、私はCommand BusとEvent Busというのを使うことが多いです。コマンドをBusに送って対応してるサービス(ユースケース)を実行するのがCommand Busと言われ、BusにSubscribeされたリスナーに発行されたイベントをハンドリングするのをEvent Busと言われてるようです。尚、BusはDIでコンポーネントに注入します。Vue.jsとinversifyJSの連携用パッケージにはvue-inversify-decoratorが便利です。これを踏まえて、Vueのコンポーネントが行うのは、以下の2つです。
- ユーザーの入力をトリガーにコマンドをBusに送る。
- Busを経由して発行されたイベントに対するリアクション。
例として、Atomic Designでコンポーネント設計して、ユースケースを実行するコンポーネントをOrganismとして作っていきます。ニュース記事一覧を表示するコンポーネントと、ニュース記事を新規登録するコンポーネントがあったとして、ニュース記事が新規登録されたときにニュース記事一覧にリフレッシュをかけたいとします。ニュース記事を新規登録するコンポーネントからemitして親コンポーネント経由でニュース記事一覧を表示するコンポーネントに伝播してもいいですが、あまりViewModel同士でコンテンツのイベントをやり取りしたくありません。理由としてはこちらの記事で説明されてる内容と同一です。また、せっかくBusもあるので、これを活用します。流れとしては以下のようになります。
- ニュース記事を新規登録するコンポーネントが新規登録コマンドをBusへ送信
- Busはニュース記事の新規登録ユースケースを実行
- 新規登録完了したらニュース記事の新規登録イベントをBusへ発行
- Busを経由して新規登録イベントをニュース記事一覧を表示するコンポーネントが受け取る
- ニュース記事一覧を表示するコンポーネントがニュース記事一覧をリフレッシュ
このようにすれば、この2つのコンポーネントは互いがどの階層でもemit地獄はせずに、あくまでそれぞれがメッセージをBusに送ったり、受け取りたいイベントをBus経由でSubscribeするだけです。
インフラストラクチャ層
DOMやバックエンドのAPIを叩くクラス、Cookieを使ったりするクラスはここに書きます。ドメインロジックで定義したインターフェースを実装する形で定義して、DIコンテナに登録しておきます。
DIコンテナ
アプリケーション内で使うクラスのインスタンス化、依存関係の解決、ライフサイクルを管理します。Busはメッセージを受け取るとこのコンテナから該当するユースケースクラスをインスタンス化して実行します。また、Event Busのように、アプリケーション全体で共有しなければいけないインスタンス等はSingletonスコープで登録しておくこともできます。
最後に
色々垂れ流しましたが、まだまだ試行錯誤中です。特にBusに関しては元々JavaScriptにはインターフェースの概念がなく、TypeScriptからトランスパイルしてもインターフェースの定義消えてしまうので、TypeベースのBusがうまく実装できてません。@node-ts/bus-coreといういい感じのライブラリも見つけたのですが、コンポーネントへのイベントのハンドリングのやり方に悩んでたりしてます。
なんかいい方法あったら教えて下さい。
ちなみにTypeScriptを使ってて型付けしづらいというのと、グローバル変数的なステートは余り使いたくないなと思ってVuexは使ってません。