前回の記事では、Scala.jsをどのように利用したかについて概観を見てきました。
前回、UI層は素直にVue.jsの単一ファイルコンポーネントで書いて、モデル層はScala.jsで書く、というスタイルを取る、と述べましたが、今回はモデル層がどのようなインターフェイスをUI層に公開して、UI層がどのようにそれらを利用する設計としたのかについて見ていきます。
モデル層の公開するみっつの種類のクラス/オブジェクト
モデル層は、UI層に対して、「コマンド」「クエリ」「イベント」のみっつの種類のクラス/オブジェクトを公開します。まずは「コマンド」から見ていきましょう。
コマンドの責務
コマンドは、アプリケーションの状態を更新するような操作(たとえばTodoを追加する、Todoをdone状態にする、など)の窓口を提供します。
なにかを更新するような操作を行う場合、UI層は、
- コマンドをインスタンス化し
- コマンドを組み立てて
- コマンドを実行
します。また、コマンドクラスは「このコマンドはvalidか?」を判別する責務も持ちます。これがUI側からどのように見えるのかがよくわかるのがTodoInput.vueです。JS部分を引用してみます。
import {AddTodoCommand} from '../../../target/scala-2.12/scalajstodo-fastopt' //...(1) export default { beforeCreate(){ this.addTodoCommand = new AddTodoCommand; //...(2) }, data(){ return { todoInput: "", dueDateInput: "", isTodoInputValid: true, isDueDateInputValid: true }; }, watch: { todoInput(v){ // ...(3) this.addTodoCommand.todoInput = v; //...(4) this.isTodoInputValid = this.addTodoCommand.isTodoInputValid; //...(5) }, dueDateInput(v){ this.addTodoCommand.dueDateInput = v; this.isDueDateInputValid = this.addTodoCommand.isDueDateInputValid; } }, methods:{ addTodo(){ //...(6) this.addTodoCommand.execute(); } } }
まずは(1)
の部分で、Scala.jsから公開されているAddTodoCommand
というクラスをimportして、(2)
でコマンドをインスタンス化しています。
フォームのinputに変化が起こると、データバインドされたtodoInput
に変化が起こり、それをwatchしている関数(3)
が呼び出されるので、その中でコマンドの値をセットています(4)
。さらに、コマンドクラスは「この値がvalidな値かどうか」を知っているので、「この値はvalidですか?」というのをコマンドクラスにたずねて、その結果をUI側に書き戻しています(5)
。
フォームがsubmitされると、addTodo
というメソッドが呼び出されます(6)
。この中では、組み立て終わったコマンドを実際にexecute
しているだけです。この際、コマンドは投げっぱなしになっていることに注意してください。
コマンドクラスは「アプリケーションの状態を更新する」ための窓口であり、「更新されたあとの状態を取得する」ための窓口ではありません。「更新結果の取得」にはQueryを利用します。
クエリの責務
UI上にアプリケーションの現在の状態を描画するためには、現在の状態をなんとかして取得しなければなりません。その際に利用するのが、Queryです。これもUI側からの利用例を見ましょう。TodoList.vueです
import { TodoQuery, /* 中略 */ } from '../../../target/scala-2.12/scalajstodo-fastopt' export default { beforeCreate(){ this.todoQuery= new TodoQuery; //...(1) }, /* 中略 */ data(){ return { todos: this.todoQuery.all() //...(2) }; } }
(1)
でインスタンス化したquery
を利用して、(2)
でアプリケーションの状態(今回ならば「現在のtodo一覧」)を取得して、databind用の変数に書き戻しています。
ところで、このqueryは「いつ」実行すべきでしょうか。(2)
の部分では、まず、UIの構築時に「初期データ」としてアプリケーションの状態を読み出しています。
しかし、よく考えるとそれだけではなくて、「アプリケーションの状態に変化があったとき(つまり今回ならtodoのリストに変化があったとき」にも、状態を読み出してUIを更新しなければなりません。そこで必要になるのが「イベント」です。
イベントの責務
イベントは、モデル側でなにか状態に変更が起こったのをUI側に伝えるためのオブジェクトです。これも例を見るのが一番でしょう。クエリと同じく、TodoList.vueです。
import { /* 中略 */ TodoRepositoryChanged //...(1) } from '../../../target/scala-2.12/scalajstodo-fastopt' export default { /* 中略 */ created(){ // (2) this.subscription = TodoRepositoryChanged.subscribe(() => { this.todos = this.todoQuery.all(); console.log(this.todos); }); }, beforeDestroy(){ this.subscription.unsubscribe(); }, /* 中略 */ }
TodoRepositoryChanged
というイベントをScala.js側からimportしてきています(1)
。さらに、(2)
でそのイベントが発火したらtodoを読み込み直すようにしています。UIがdestroyされたときにきちんとunsbscribeするのも忘れないようにしましょう(3)
。
UI層とモデル層のコミュニケーションまとめ
以上をまとめると、以下のようなフローでUI層とモデル層はコミュニケーションすることになります。
まず、初期データをQueryを通じて読み込みし、UI側でレンダリングします。
状態の更新を伴うような操作は、Commandを組み立てて実行します。
Commandの結果としてアプリケーションの状態が変化し、Eventが通知されます
Eventに応じて、最新の状態を読み出し、表示します。
このように、モデル層では「更新の窓口」と「読み出しの窓口」と「状態通知の窓口」のみっつを設けて、UI層ではそれらを適切にimportし利用することで、「UIの定義」と「アプリケーションのロジック」を分離することができます。いわゆる「プレゼンテーションとドメインの分離」ってやつですね。
次回予告
次回は、オフラインファーストなアプリケーションにおいて、モデル層をどのように設計していったかについて書きます。