2015年3月16日
2015年に向けたJavaScriptアプリケーションアーキテクチャ Part 2
本記事は、原著者の許諾のもとに翻訳・掲載しております。
PART1はこちら : 【翻訳】2015年に向けたJavaScriptアプリケーションアーキテクチャ PART 1
オフラインの課題
オフラインでアプリケーションを使えなければ、真のモバイルWebエクスペリエンスとは言えません。
これまで、アプリケーションをオフラインで使用することは根本的に困難でしたが、状況は改善されつつあります。2014年を振り返ると、WebプラットフォームのAPIは、より良いプリミティブを提供できるよう進化し続けてきました。最近の事例で最も興味深かったのは Service Worker です。Service Workerは、オフラインでもサイトを稼動させることができるAPIです。ネットワークリクエストに割り込んで、そのリクエストをどう処理すべきかをブラウザに伝えます。
コントロールのレベルが適正かどうかという点以外は、アプリケーションキャッシュのあるべき姿を実現しています。これによって、コンテンツをキャッシュし、与えられた変更を処理し、ネットワークをその拡張のように扱うことができます。Service Workerについてもっと詳しく知りたい方は、Matt Gauntの優れた解説記事 Service Workerの紹介 またはJake Archibaldの興味深い記事 オフラインのパターン をご覧ください。
2015年にはService Workerに、さらにルーティングや状態管理ライブラリの進化が加えられることを期待しています。ナビゲートしたあらゆるルートに対して、オフラインと同期に関する最高のサポートがあれば素晴らしいことです。さらに、開発者がそれを自由に使えるとしたら実にうれしいですね。
キャッシュされたビューとアセットを使うことにより、繰り返して閲覧する場合に、パフォーマンスをかなり向上させることができます。Service Workerはまた、ある意味で基盤APIとも言えるものであり、リクエストコントロールは、 プッシュ通知 や バックグラウンド同期 のような、利用できる多くの新機能の1つに過ぎません。
現在のChromeでのプッシュ通知の使い方についてもっと知りたい方は、Matt Gauntによる プッシュ通知とService Worker をお読みください。
コンポーネントAPIとファサード
以前 ちょっと触れた”Facadeパターン”について、今でも実行可能だという考えに異を唱える人もいるでしょう。特にコンポーネントの実装のディテールがパブリックAPIにリークされるのを避けたい人は、この意見に反対かもしれません。
コンポーネントにクリーンで堅牢なインターフェースを定義することができれば、コンシューマは実装のディテールについて心配することなく、前述したコンポーネントを活用し続けることができます。またダメージを最小限に抑えつつ、いつでも変更することができます。
さらに付け加えると、これはフレームワークとライブラリの製作者が、公開するパブリックコンポーネントをフォローする際の優れたモデルとなり得ます。これはWebコンポーネントとは全く関係ありませんが、私はこれまで、Polymer paper-* elementsが、パブリックコンポーネントAPIに与える影響を最小限に抑えながらも、装置的な背景とともに少しずつ進化していく様子を興味深く見守ってきました。こうした段階的な進化は、ユーザにとって本質的に大切なことです。できる限りユーザを驚かせないという原則を守りましょう。言い換えれば、ユーザがコンポーネントAPIの動作に戸惑うようではいけません。この原則を守ることで、ユーザと開発チームの両者が満足できるのです。
不変データ構造と永続データ構造
大規模なJSに関する以前の記事では、データ構造の不変性、もしくは永続性についてはきちんと触れませんでした。 immutable-js や Mori のようなライブラリを経験してもその価値がよく分からなかった方には、この手っ取り早い入門的な解説が役に立つかもしれません。
不変 データ構造とは一度作成された後は変更できない構造のことです。言い換えれば、可変的なコピーを作成することが効率的に変更する方法となります。 永続 データ構造とは、変更される際に、変更前のバージョンを保存するデータ構造です。このようなデータ構造はイミュータブルです(作成後はその状態は変更できません)。変更の際にはインプレースの構造をアップデートするのではなく、更新後の新しい構造を生成します。本来の構造を指し示すものは何であれ、決して変更されないことが保証されています。
不変データ構造の本当の利点は、参照の等価性にあります。ですから、メモリ内のアドレスを比較すれば、同じオブジェクトを持っているだけではなく、同じデータを持っていることがよく分かります。
Pascal Hartig, Twitter
Todoアプリのフォームを使って、不変データ構造を理論的に説明してみようと思います。Todoアイテム用の標準的なJS配列を持ったアプリを考えてみましょう。メモリ内にはこの配列に対する参照と特定の値があります。その配列が変わるような新しいTodoアイテムをユーザが追加します。これによって、配列は変更されてしまいました。JavaScriptでは、この配列に対するメモリ内の参照は変わりませんが、その参照が何を指し示すかという値は変わります。
配列の値が変化したどうかを知るには、配列の要素を比較する必要があります。これは時間のかかる作業です。では、普通の配列の代わりに、不変配列をイメージしてみましょう。不変配列はMoriやFacebookのimmutable-jsで作ることができます。配列の要素を修正することで、新しい配列やその参照を得ることができます。
もしメモリ上で、配列の参照が同じものだと確認ができれば、配列は変わっていないことが保証されます。つまり、値は同じということです。この方法を使えば様々なことが可能になります。素早くて質の高い確認作業ができるのです。配列の全ての値を確認するよりも参照のみを確認するほうが、作業の効率は上がりますよね。
前述のとおり、不変性はデータ構造(例えばTodoアイテム)が変更されていないことを保証してくれます。次の例を見てみましょう(適当なコードです)。
var todos = [‘Item 1', ‘Item 2', ‘Item 3'];
updateTodos(todos, newItem);
destructiveOpOnTodos(todos);
console.assert(todos.length === 3);
このアサーションが成功した時点で、その配列が作られて以来どの操作でも変更されていないことが保証されます。おそらく、データ構造の変更に関して注意深い人であれば、これはそんなに大きな問題ではないでしょう。しかし、この方法で”たぶん大丈夫だろう”を”絶対大丈夫だ”という確信に変えることができるのです。
私は以前からMutation Observerのような既存の技術を使用したUndoスタックを実行することを避けてきました。これを使ったシステムを使用すれば、メモリ使用量が右肩上がりになるのです。永続データ構造では、Undoスタックが構造を共有している場合、メモリの使用は格段に減る可能性を秘めています。
不変性のメリットは以下のとおりです。
- 他の配列に属するオブジェクト上のadd、append、removeなどの典型的な破壊的更新によって意図していない副作用を引き起こすことはありません。
- それぞれの変更が値を生成しているという記述によって、更新を扱うことができます。関数の引数としてオブジェクトを渡すことができ、それらの関数がオブジェクトを変更する心配がありません。
- これらのメリットはWebアプリを書く上で役に立ちます。しかし、これらのメリットを活用しなくても作業できますし、たくさんの人が実際に今までやってきています。
不変性はReactのようなものにはどのように関わってくるのでしょう。まずアプリケーションの状態を見てみましょう。状態が不変的なデータ構造によって表されている場合、アプケーション(または個々の要素)を再レンダリングする際に、参照が等しいことを確認できるはずです。もしメモリ内の参照が等しかったら、アプリケーションやコンポーネントのデータは変更されていないことがほぼ保証されます。これにより、Reactで再レンダリングをする必要がないと言えます。
では、Object.freezeはどうでしょう? MDN description でObject.freezeに関してのページを見ると、不変性の問題を解決するのになぜ追加のライブラリが必要になるのか、疑問に思う人がいるかもしれません。Object.freezeはオブジェクトを凍結し、新しいプロパティの追加、既存のプロパティの削除、既存のプロパティやプロパティの列挙可否、設定変更可否、書き込み可否の変更を抑制します。本質的には、オブジェクトを実質的に不変にします。素晴らしい! では、なぜこれでは不十分なのでしょう?
基本的にはObject.freezeを使えば不変性は保たれます。しかし、不変オブジェクトを修正する必要が出た場合には、全てのオブジェクトをコピーしてから、そのコピーを変更し、またそれを凍結させますよね。こうした手順を踏むと時間がとてもかかるので、多くのユースケースの場合、実用的ではありません。そこで、Immutable.jsやMoriが便利なのです。Immutable.jsやMoriは不変性を保ってくれるだけでありません。意図しない破壊的更新を避けながら永続データ構造を扱いたい時にとても役に立つのです。
手間をかける価値はあるか?
(ある特定のユースケースの)不変データ構造では、コードが引き起こす副作用を気にすることなく作業ができるようになります。コンポーネントやアプリケーションに対する作業中に基礎情報が別のエンティティで変更されてしまったかもと思っても、データ構造が不変であれば何も心配する必要はありません。 おそらく、不変性の主なデメリットはメモリ面のパフォーマンスが落ちることです。でもこれは、扱うオブジェクトのデータ量によります。
まだ先は長い
根本的にCompositeな構造が良いということは認めますが、多くの面で賛同できない部分がまだあります。例えば、関心の分離、データフロー、構造、不変データ構造の必要性、データバインディング(双方向バインディングが単方向バインディングよりも優れているとは限らないし、コンポーネントの中で可変の状態をどのように設けるかによる)、適切なレベルの抽象化によるマジックなどの点です。さらには、レンダリングパイプラインの解決方法(ネイティブvs.仮想)や、ユーザインタフェースでどのようにして60fpsに合わせるかという課題もあります。そして、テンプレートの問題もあります(そうです、私たちはいまだに全員がTemplateタグを使っているわけではありません)。
さらなる前進と向上
結局、これらの問題を解決する上で浮かんでくる疑問が以下の3つでしょう。
- 独断的なフレームワークに、このような決定や選択を任せてしまっていいのか。
- 既存のモジュールを使って、これらの問題を解決していく方法でいいのか。
- これらの問題を解決するアーキテクチャやパーツを自分自身で(1から)作り上げるのか。
私は頭が悪いので、これらの疑問にはまだ答えられません。まだ勉強中なのです。それを踏まえて、パターンやフレームワークの中でアプリケーションのアーキテクチャは将来、プラットフォーム上でより必要なっていくのかどうか、あなたの意見を聞かせてください。この記事の私の見解に間違いなどがあれば(おそらくあると思います)、遠慮せずに訂正をお願いします。もしくは、下記以外のもので休日に読みたくなるような記事があれば、私に紹介してください。
- オフラインの手引き
- なぜgeneratorで非同期処理をするのはバカげたことなのか
- JavaScript のCSPとTransducers
- ホーアのCommunicating Sequential Processes
- Transducersと ベンチマーク
- ES6 ModuleによるエクスポートするHTMLとインポートされる HTML
- Fluxの実践
- ES6 Moduleの実際のワークフロー
- Traceur、Gulp、 Browserify、 ES6
- JavaScriptの関数プログラミングはゴミだ
注釈:Webコンポーネント関してのコンテンツが不十分と思われるものもあるでしょう。Chromeに関わる私の仕事は、 Webコンポーネントのプリミティブ と Polymer で記事になっています。私はこれで十分にカバーしていると思っています(もしかしたらしてないかも! )。しかし、私はいつでも新しい意見を受け入れる体勢は整っていますので、これらのトピックに関してのリンクをぜひシェアしてください。
私は特にWebコンポーネントとVirtual-DOMのアプローチの違いによるギャップを縮める方法や、進化するコンポーネントのメッセージングパターンはどこで見ることができるのか、Webアプリ製作者が不足しているという現状に関して皆さんがどう思っているか、などに特に興味を持っています。
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa