オフラインファースト的なGUIアプリケーションをScala.jsで書く話 / Vue.jsによるUI層とScala.jsによるモデル層のコミュニケーション

前回の記事では、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側でレンダリングします。

f:id:nkgt_chkonk:20170522150803p:plain

状態の更新を伴うような操作は、Commandを組み立てて実行します。

f:id:nkgt_chkonk:20170522150924p:plain

Commandの結果としてアプリケーションの状態が変化し、Eventが通知されます

f:id:nkgt_chkonk:20170522151017p:plain

Eventに応じて、最新の状態を読み出し、表示します。

f:id:nkgt_chkonk:20170522151058p:plain

このように、モデル層では「更新の窓口」と「読み出しの窓口」と「状態通知の窓口」のみっつを設けて、UI層ではそれらを適切にimportし利用することで、「UIの定義」と「アプリケーションのロジック」を分離することができます。いわゆる「プレゼンテーションとドメインの分離」ってやつですね。

次回予告

次回は、オフラインファーストなアプリケーションにおいて、モデル層をどのように設計していったかについて書きます。

オフラインファースト的なGUIアプリケーションをScala.jsで書く話 / Scala.jsについて

はじめに

オフラインファーストへの要求

近年、オフラインファーストというか、「オフラインのときにも普通につかえて、オンラインになったら同期する」みたいなことに対する要求が高まっているように感じます。

その場合は、ローカルにもきちんと永続データを持っておき、オンラインのときにバックエンドと通信をしながらバックエンドのデータと同期していく、というスタイルを考えるのが自然だと思います。

また、普通のJSアプリケーションであっても、「サーバーに投げる際に失敗したデータはローカルでメモリ上にもっておいてリトライしたい」などの要求もあるでしょう。

さらに、ここでモバイルアプリも視野に入っているとなると、どうしても「オッRealm Mobile Platformか!?」という感じが出てきますが、Realm Mobile Platformにロックインされるのと引き換えに開発の速を選ぶのか、それとも、というのは判断の別れるところでしょう。

そこで、一旦オフラインファーストなGUIアプリケーションをどのように設計するべきかの素振りを、ブラウザプラットフォーム上でしてみることにしました。これはわたしが単に慣れているプラットフォームだから、という理由です。

複雑なロジックは表現力の高い言語で書きたいという要求

オフラインファーストなアプリケーションを書いていく場合、「いつ同期するのか」「同期のストラテジはどうするのか」「同期中のデータはどう管理するのか」「同期に失敗した場合は?」「サーバーから降ってくる同期用データをどうハンドリングするか」など、かなり複雑なことをモデル層以下でハンドリングしなければならないということが予想されます。

また、そもそもモデル層はなるべくリッチで設計上の制約がない言語で書いて、ドメインの設計やアプリケーションの設計に集中したいという要求があります。

今回は、「どうせ素振りなのだから」ということでScala.jsに手を染めてみました。ちなみにリポジトリShinpeim/Scala.jsTodoExampleにあります。

本記事では、この素振りによって得られた知見をまとめます。全3,4回くらいになる予定です。

初回はScala.jsについてです。

Scala.jsについて

JSとの協調、懸念点

Scala.jsを使ってみるにあたって

  • JSのライブラリ資産ってScala.js側からは使えるの?
  • Scalaのライブラリ資産ってそのまま使えるの?

というところがまず懸念点として上がります。また、やはりJSが得意とする部分(たとえばVue.jsのシングルファイルコンポーネントでUIを組み立てていくこととか、Routingとか、そういうブラウザと密結合している部分)についてはJSで書けたほうが余分なハマりが少なそうという感じもあります。

そこで、今回は

  • index.js(エントリポイント)とUI定義であるVue.jsのシングルファイルコンポーネント(*.vueってやつ)だけJSで書く
  • そのほかはすべてScala.jsで書き、sbtがwatchしてJSにコンパイル
  • webpackがそのファイルもwatchして、がっちゃんする

というスタイルを取ることにしました。

このスタイルの懸念点としては、

  • Scala.jsが吐くファイルサイズがでかくなるけどそれをwebpackでがっちゃんって実用に耐えるの?
  • そもそもScala.jsで吐いたJSってES6 importとかCommonJS requireで扱えるの?

というあたりがあるので、そのあたりの検証も行いました。

以下、その懸念点に対する回答です。

JSのライブラリ資産ってScala側からは使えるの?

結論から言うと、使おうと思えば使えますが、「そのまま」は使えません。

  • すでにJSライブラリをScala.jsで動かすためのファサードが提供されている場合はそれ使う
  • 自分でファサード書く
  • 型検査なしにScala.js側からがんばってJSのオブジェクトなどを操作して使う

というみっつの選択肢があります。

詳しくは公式のドキュメント - JavaScript libraries for Scala.jsを参照してください。

ただ、今回はJSのほうが得意な部分は素直にJSで書く、というスタイルを選択しているため、jQueryだとかVue.jsだとかをScala.js側から触る必要はないので、JSのライブラリ資産をScala側から利用するシーンはありませんでした。

Scalaのライブラリってそのまま使える?

結論から言うと、「Scala.jsコンパチなライブラリならそのまま使える」ということのようです。

公式のドキュメント Compatible Scala librariesを御覧ください。ScalazとかCatsとかShaplessとかあって強い……

joda.timeを利用しようとしたらうまくfastOpt(最後にjs吐く部分の操作だと思って)できなくて、「どういうライブラリなら"Scala.jsコンパチ"って言えるんだ?」ってことはよくわかってなくて、まだ調べきれてません。

Scala.jsが吐くファイルサイズがでかくなるけどそれをwebpackでがっちゃんって実用に耐えるの?

  • でかいとwebpackがbundle.js吐くのに時間かかって開発のサイクルが遅くなったりしない?
    • 開発時はminifyなどをスキップする、Scala.jsが吐くjsファイルはバベったりローダー噛ませたりしないでそのままimportすると、現実的な速度でサイクル回せるなあという感じがする
  • で、最終的なサイズはどうなの、実用できそう?
    • 今回のリポジトリの規模で、minifyなしで4.08MB、minifyありで2.16MB。でかいライブラリに依存したりするとかなり厳しいかなという気がするが、まだちょっと未知数という感じがする。

そもそもScala.jsで吐いたJSってES6 importとかCommonJS requireで扱えるの?

扱える。see Export Scala.js APIs to JavaScript - Exports with modules

Scala.jsの書き味はどうだった?

最高の体験だった……。静的型付けによるIDEの賢いリファクタ……、堅牢で柔軟な言語仕様……、なんか矛盾があればsbtさんが怒ってくれる……、traitを利用したDI……。ほしいものがここにあった……ここは地上の楽園か……?

また、Scalaが表現力の高い言語なので、結果として設計やロジックに集中することができて、JSでがんばって書きながら設計してたときよりもより良い設計が導けた気がする。

次回予告というか今後語られる予定の話

  • Vue.jsのシングルファイルコンポーネントの世界とScala.jsの世界をつなぐ窓口をどのように設計したか(それはつまりプレゼンテーション層とドメイン層をつなぐ窓口の設計である)について
  • オフラインファーストを視野に入れた際のドメイン層の設計について

-> 書きました

オフラインファースト的なGUIアプリケーションをScala.jsで書く話 / Vue.jsによるUI層とScala.jsによるモデル層のコミュニケーション - 猫型の蓄音機は 1 分間に 45 回にゃあと鳴く

vue-routerのafterEachグローバルフックがhookされるタイミングとonReadyがhookされるタイミング

vue-routerにはonReadyというフックが用意されている

ドキュメントを読めばわかるように、これは when the router has completed the initial navigation, which means it has resolved all async enter hooks and async components that are associated with the initial route. 、つまり最初に表示するルートに紐付けられたbeforeEnter ナビゲーションガードやbeforeEachglobalナビゲーションガードが解決され、「どのrouteが実行されるべきか」が解決されたあとに呼ばれる。

これが便利になるのはSSRのときで、preloadしたいデータがあるような場合はbeforeEnter内で非同期読み込みし、その読み込みが終わったらnextするようにしておき、onReadyのタイミングでSSRすればよい、というような使いかたができるわけだ。

ところで、ナビゲーションガードが解決されたあとに呼ばれるhookはもうひとつある。それがafterEachグローバルhookだ。

このafterEachというhookとonReadyhookはどちらが先に呼び出されるのだろう。ドキュメントを読んでもよくわからなかったので、ソースを追った。

まず、onReadyで登録したhookを実際に呼び出しているのはここである。

  transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const route = this.router.match(location, this.current)
    this.confirmTransition(route, () => {
      this.updateRoute(route)
      onComplete && onComplete(route)
      this.ensureURL()

      // fire ready cbs once
      if (!this.ready) {
        this.ready = true
        this.readyCbs.forEach(cb => { cb(route) })  // ココね!!!ここ!!!
      }
    }, err => {
      if (onAbort) {
        onAbort(err)
      }
      if (err && !this.ready) {
        this.ready = true
        this.readyErrorCbs.forEach(cb => { cb(err) })
      }
    })
  }

で、その前にコールされてるthis.updateRoute(route)の中身見ると

  updateRoute (route: Route) {
    const prev = this.current
    this.current = route
    this.cb && this.cb(route)
    this.router.afterHooks.forEach(hook => {
      hook && hook(route, prev)
    })
  }

ここでafterHooksを呼んでいる。というわけで、onReadyよりも先にafterEatchのhookが呼ばれるようになっていることが確認できた。

注意すべき点として、(あたりまえだが)afterEachは同期的に呼び出されているので、その中で非同期な操作を行った場合、その非同期操作の完了を待たずにonReadyhookが呼び出されるので、onReadyにhookしてSSRしても、afterEach内で行った非同期操作の結果はSSRされない。

VuexとPDS、immutable modelの所感

Twitterに書いた内容再掲。

最近Vuexについて考えてるのは、Vuex使ってPDS実現するとなると、状態はVuexのレイヤーで持つことになるから、モデル層はステートレスで作るのが相性良い(というか自然とそうなる)だろうな

その時にJSでイミュータブルに寄せて行く辛さがどれくらいキツいものなのかの知見が自分にはない

ある程度以上に複雑なアプリケーションでは、サーバーサイドからpushされる情報のためにコネクション握りっぱなしにする部分とかも出てくるであろうと思っていて、モデル層以下をイミュータブルに寄せて行った時にそういうのをどうハンドルすれば良いかも自分に知見がない

Vuex少し触ったところとても筋が良いと感じるんだけど、そのあたりみんなどうしているのか知りたいなあ

まあある程度の複雑さまでなら「Vuexの層に全部書く」でよいからそれでええやん、というのはありそう

イミュータブルモデルの良いところはテスタビリティがぐんぐんあがるよ!ってところだと思っていて、そのメリットは捨てがたいので知見貯めていきたい

画面ごとの複雑さが異なるアプリケーションにどう立ち向かうか

ツイッターで書いた内容です

SPAやってると画面ごとに複雑さが全然ちがって、単なる一覧表示してる画面の複雑さといろいろな操作ができる画面の複雑さは雲泥の差になる

こういうときに、「この画面はきちんとドメイン層作ったほうが見通しがよくなるけど、この画面もそうしちゃうとオーバーキルなんだよな〜」ということがある

そういうときにアプリケーション内で一貫したアーキテクチャでやっていくのか、この画面はXの層をスキップしますとするのか、こーたえーはーなーい(ABSTRACT TRUTH / NUMBERGIRL

ディレクトリ構成、名前空間構成に貴様の 意思 を込めろ」という煽りを思いついたので自由に使ってください

ひとつのアプリケーションの中に複数の画面があり、画面ごとに複雑さが異なるような場合、やはり画面ごとにアーキテクチャを変えるというのが正解な気がしてきた。その場合、たとえばパッケージ構造は次のようにする

- presentation
  - A
  - B
- model
  - transaction
    - A transaction
  - layered
    - usecase
      - B usecase
    - domain
    - infrastructure

その上で、domainとinfrastructureの下は画面構造に依存しない分割構造でパッケージを分ける

どうか。

しかし、これを実践するためには、意図をきちんと説明したら理解してくれるメンバーで開発を行う必要があり、弊社なら問題ないな

MVVM, Layered Architecture, Clean Architectureを綜合して位置づけを整理する

一つ前の記事にもある通り、掲題の通りの試みを行いました。

github.com

一応、「一通り書けたぞ!」という感じになったので、ここにてこのREADMEを Ver.1.0.0 メジャーリリースとします。

自分ではかなりわかりやすく整理できたつもりでいますが、まだまだ「えっここってどういうこと?わかりにくいな〜」という部分があったり、「あ、ここは間違いですね」といった部分があるかと思います。そういうときには是非 issue やブックマークなどでフィードバックをください。それをもとにさらに良いものに育てて行きたいと思います。

また、もしこのリポジトリがみなさんの一助になるようなことがあったとしたら、

  • READMEの最後に書いてあるwishlistからカンパする
  • 弊社で一緒に働いて一緒に議論を深めていく

のどちらかを是非!是非!!!!検討してみてください。twitter @neko_gata_s が誰からでもDMを受け付けるようになっているので、弊社に興味がある方はぜひメッセージをください。techblogもやってます。

GUIアプリケーションアーキテクチャ総合!みたいなやつ書いてる

最近はずっとJSでGUIを書くっていうお仕事をやっていて、その中で様々な知見が溜まってきてます。

そのときにひしひしと感じたんだけど、世の中にはさまざまなアプリケーション・アーキテクチャの話が溢れかえっていて、結構混乱を産んでいるように思います。

MVVMだけ見ても、「MVVMで実装しています」って主張しているものがMVVMパターンと違うパターンで実装されていたり、「軽量MVVM」なんて言葉が生まれていたりという状況があって、これはだいぶ混乱が極まっているぞ、と感じる状況です。

また「最近はClean Architectureが熱いらしい!MVVMを捨ててやってみよう!」とか言う発言を目にしたりして、「Clean ArchitectureとMVVMは矛盾しないからMVVMを捨てる必要はないんだよ!」って思ったりもするわけです。

そういう混乱の中にあるひと(それは知見を貯めるまえの自分のことでもあります)のために、今自分がきちんと理解している範囲で、様々なアプリケーション・アーキテクチャと、それぞれの関係みたいなものがきちんと説明できたらいいな、と思って、サンプルアプリケーション付きのリポジトリを作りました。

github.com

サンプルアプリ自体はGithubPagesで動いています(再生ボタン押すと音が出るので注意)。

README.mdに解説が書いてあります。

このサンプルアプリのアーキテクチャを解説していく中で、Presentation Domain Separation と MVVM と Layered Architecture と Clean Architecture について、整理された概念を読者が獲得していくことを目指しています。

と、いいつつ、実はこのREADME.md、「執筆中」って部分があるんですけど、まあ、そうです、まだ執筆中なんですよね。

とはいえ、すでに結構な分量が書かれたので、一旦ここで「こういうの書いてるよ!よかったら読んでね!」ってアナウンスしておこうかな、と思った感じです。

読んでいただいて、よかったら「よかったよ!」って言ってもらえると、続きを書くモチベーションにもなるので、心優しいひとは読んでみて、よかったら「良かったよ!」って言ってください……。

また、技術的な誤りなどが含まれている場合はIssueなどでご指摘を歓迎いたします。よろしくおねがいします。