この記事は、Yumemi.vue #4でのLTした内容を基に、改めて文書として再整理したものになります。
LTスライドはこちら。
表記注
「Vuexを使う」と言う時、実際に私達がアプリケーションから利用するものは、厳密にはVuexそのものではなくVuexによって生成されるデータストアオブジェクトです。
「Vuexというライブラリそのもの」と、「Vuexによって生成されるデータストアオブジェクト」を区別するため、この記事では
- Vuex = ライブラリ
- Vuexストア = Vuexによって生成されるデータストアオブジェクト
という表記分けを用います。
対象
この記事では
- Vue.jsを使っている人
- Vue.jsを知っている人
- Vue.jsを使おうとしている人
のいずれかに当てはまる人、その中でも
- Vuexを使っている人
- Vuexを知っている人
- Vuexを使おうとしている人
のいずれかに当てはまる人を対象として想定しています。即ち、Vue.jsとVuexの両方について、少なくとも関心を持っている人を想定しています。
また、この記事に以下の内容は含まれません。
- javascriptのコード解説
- Vue.jsのコード解説
- Vuexのコード解説
概要
Vuexは、グローバルデータを一元管理するための管理パターンを提供してくれる強力なツールです。
一方、その強力さ故に、Vuexの本来の目的を逸脱した使い方も散見されるように思います。
この記事では、Vuexを使って何をするべきか、Vuexを使って何をしないべきかを考えます。
Vuexストアの責務を考える
Vuexが私達に提供してくれるものは、機能的には
- グローバルかつ単一のデータストア
- 上記データストアへの限定的なアクセス方法
という2つの性質を持ったストアオブジェクト = Vuexストアの生成です。
さて、実際にVuexストアをアプリケーションで利用するに辺り、Vuexストアが持つべき責務の範囲はどの程度なのでしょうか。換言すれば、Vuexストアが責務を負うべきもの、責務を負うべきでないもの1は、一体何なのでしょうか。まずはそれを考えてみます。
何のためにVuexを使うのか
そもそもとして、Vuexはどのような問題を解決するためのライブラリなのでしょうか。
ライブラリもまたソフトウェアである以上、第一にまず解決したい問題が有り、その問題を解決するために何らかの手段が考案され、その実装としてライブラリが有るはずです。解決したい問題が存在しないソフトウェアというものは、架空のものか、さもなければタイピング練習の副産物として作られたものでしょう。
Vuexもまた実際に利用されているライブラリである以上、何らかの問題解決を目的としたものであるはずです。
ではその目的とはなんぞやという問について、この記事では
- コンポーネントを超えて存在する情報を管理するため
- 上記のストアオブジェクトへの更新を安全にするため
が解であると考えます。
コンポーネントを超えて存在する情報を管理する
Vueは元々、コンポーネントという単位でのデータ管理方法を提供してくれます。しかしながら、システムの要件によっては、特定のコンポーネントに限定されず、かつ、ページを跨いでもなお同一性が求められるような情報も、時として現れるでしょう。
そのような問題を解決する一つの手段として、コンポーネントの単位を超え、かつどの場面でアクセスしても同一の実体であるようなデータストアを用意すること、即ち「グローバルかつ単一のデータストア」を用意し、それを各所から利用することが考えられます。
Vuexは、「コンポーネントを超えて共有される情報」を管理するための手段として、まさにこの「グローバルかつ単一のデータストア」を提供してくれます。
ストアオブジェクトへの更新を安全にする
単純に「グローバルかつ単一のデータストア」が欲しいだけであれば、
export default {
hoge: {
items: [/*...*/]
},
fuga: {
/**/
}
}
のようなオブジェクトを一つ定義しておけば事足ります。
しかしながら、単なるグローバルオブジェクトではあらゆる場面から自由にアクセス可能であるのみならず、自由な更新すらも可能となってしまいます。仮に更新用のメソッドや関数を定義したとしても、プログラマーがそれらを一切無視してデータを直接更新することは妨げません。そのようなストアを安心して利用することは、およそ難しいでしょう。
Vuexは奔放な情報更新を許さず、 action → mutation → state → getter
という一方向でのデータ更新を強制するストアオブジェクトを提供します。ストアオブジェクト内のデータを更新する場合には action
あるいは mutation
という予め定義された手続きを経由せねばならず、 state
を直接更新することは禁じられています2。
こうしたVuexの提供する単方向データフローという(Fluxアーキテクチャを基とした)構造により、私達は無秩序なストア更新を心配する必要が無くなります。
Vuexストアの責務
上記から、Vuexを使うそもそもの目的は
- コンポーネントを超えて共有される情報を管理すること
- その実現方法として、グローバルなストアオブジェクトを使う
- そうしたストアオブジェクトへの更新を安全にすること
- そのために単方向のデータフローを強制する
と整理できるでしょう。
更に一文にまとめるならば、コンポーネントを超えて共有される情報へ、安全にアクセスする方法を提供することがVuexのそもそもの目的である、という形となると思います。
これがVuexの目的であるならば、そのような目的の下に生成されたVuexストアの責務は、コンポーネントを超えて共有される情報へ、安全にアクセスする方法を提供することであると言って良いでしょう。
Vuexストアのアンチパターンを考える
前節で、Vuexストアの責務をコンポーネントを超えて共有される情報へ、安全にアクセスする方法を提供することと定義しました。これによって、「Vuexストアでするべきこと」は定義されたと考えて良いでしょう3。
この定義を出発点に、以降では「Vuexストアでしないべきこと」を考えてみます。
何が「Vuexストアのアンチパターン」か
ここでは、Vuexストアの責務を逸脱した方法全てをアンチパターンと見做すこととします。
些か過激に聞こえますが、Vuexストアに対しては必要なものだと思っています。以下、その理由を説明します。
SRP/SDP
ここで土台としている考え方は
の2つです。SRP・SDPそのものについての詳細な解説は出典となるテキスト、あるいは他記事を参照していただくこととして、それぞれの内容をざっくり書くと
- SRP
- 一つのモジュール7に複数の責務を持たせると、様々な理由で変更されるようになり、予期しない変更が発生しやすくなる
- したがって、一つのモジュールに複数の責務を持たせてはならない
- SDP
- 不安定なものに依存すると、変更の影響が激しくなるためである
- したがって、より多くから依存されるものはより安定させなくてはならない
- ここでの「安定」とは、より依存が少ないということである
というものです。
Vuexストアの責務を逸脱した方法 = アンチパターン
「Vuexストアの責務を逸脱した方法」でVuexストアを使うということは、即ちVuexストアに新たな責務を追加するということです。それにより、Vuexストアは複数の変更理由を抱えることになり、Vuexストアは"本来の責務"と"新しい責務"の両方に常に対応することを強いられます。どちらかの責務に変更が発生すればVuexストアも変化することが求められ、しかもその変更がもう一方に波及してはいけません。
このような取り扱い方は、Vuexストアを不安定にし、場合によってはVuexストアの改善すらも拒みます8。
何らかの理由によって、あえてSRP違反を承知で作るという場面ももしかするとどこかで有り得るのかも知れません9。しかし、そうした事を考える前に、Vuexストアはコンポーネントを超えて共有される情報を扱うグローバルストアであるということを確認する必要があります。
通常であれば、フロントのデータや振る舞いはコンポーネントに閉じるため、そのスコープと影響範囲もまた、コンポーネントツの内部に閉じます。一方でVuexストアは、上記のような性質を持つ関係上、フロントアプリケーション上の様々なコンテクストから参照され得る、すなわち、より多くから依存されるということです。であるならば、アプリケーションを安定させようと思うなら、それ全体から参照されるVuexもまた可能な限り安定していなくてはなりません(SDP)。
しかしながら、様々な理由でVuexストアに変更が発生する = Vuexストアが様々な前提に依存する = Vuexストアが不安定な状態では、そこに依存するアプリケーション全体までもがその変更の影響を受ける = 不安定になってしまいます。城を建てるのであれば、足元には砂ではなくしっかりした土台が必要です。
Vuexストアがアプリケーション全体に影響し得るものである以上、Vuexストアは可能な限り安定性を高めるべきです。
したがって、ここではVuexストアを不安定にさせうる、最終的にはアプリケーション全体の不安定さを誘発しうるものについては、強く排除する考えを取ります。
その結果がVuexストアの責務を逸脱した方法全てをアンチパターンと見做すということです10。
まとめ: Vuexストアでするべきこと、しないべきこと
- Vuexストアでするべきこと = Vuexストアの責務 = アプリケーション全体で共有される情報へ、安全にアクセスする方法を提供すること
- Vuexストアでしないべきこと = Vuexストアの責務を逸脱すること全て
です。後者に該当するノウハウは、全てアンチパターンと見做します。
「するべきこと」の根拠は、そもそもとしてVuexが解決する問題は何かということ、
「しないべきこと」の根拠は、SRP/SDPの考え方と、「アプリケーション全体で共有されるデータを管理する」という性質上、Vuexストアは可能な限り安定にするべき、という考えからです。
おわりに
この記事では、Vuexの使い方について、「このように書く」「こんなことができる」という使い方の詳細ではなく、「どのように使うべきか」という使い方の基礎11に当たる部分を考えてみました。
Vuexは強力な武器をフロントエンドエンジニアに与えてくれます。しかしながら、その強力さ故に、誤った使い方はその利益より多くの被害を周囲に与えかねません。それこそ「使わない」という選択の方が適切な場面の方が多いかもしれません12。
「大いなる力には大いなる責任が伴う13」という言葉通り、Vuexという有る種の諸刃の剣を使うには、それを振るうべき時と場所を見極め、時には刃を収められるだけの節制が必要だと思います。
この記事が、Vuexの使い方について改めて考える契機となれば幸いです。
備考(Vuex公式の紹介について)
Vuex公式のVuexとは何か?より
一つ目は、プロパティ (props) として深く入れ子になったコンポーネントに渡すのは面倒で、兄弟コンポーネントでは単純に機能しません。二つ目は、親子のインスタンスを直接参照したり、イベントを介して複数の状態のコピーを変更、同期することを試みるソリューションに頼っていることがよくあります。これらのパターンは、いずれも脆く、すぐにメンテナンスが困難なコードに繋がります。
では、コンポーネントから共有している状態を抽出し、それをグローバルシングルトンで管理するのはどうでしょうか? これにより、コンポーネントツリーは大きな "ビュー" となり、どのコンポーネントもツリー内のどこにあっても状態にアクセスしたり、アクションをトリガーできます!
ん?
どのコンポーネントもツリー内のどこにあっても状態にアクセスしたり、アクションをトリガーできます!
「アプリケーション全体で共有される情報へ、安全にアクセスする方法を提供すること」以外の使用方法を、Vuex公式が推奨 or 想定している疑惑
「どのコンポーネントでも、状態にアクセスしたりアクションをトリガーできる」について、ここでは文章の流れから「props/emitのバケツリレーの代わりに、Vuex使うと楽ちんだよ!」という風に読めそうです14。
それって、「安全なグローバルストア」という責務の他に、Vuexストアをグローバルなイベントバスとしても使おう = 責務の追加、ってことでは・・・。
props/emitのバケツリレーが面倒くさいというのはとても良くわかるのですが、それをやってしまうと「このボタンクリックしたらXXを更新したい」「このテキストが変わったら、AAのBBだけ変更したい、他はそのまま」「こことここがチェックされたら、YYをZZに変更したい」という、UI側の都合がどんどんVuexストアに流入してきそうな気配を感じます。
ここまで散々考えてきた「Vuexの目的」「Vuexの使い方」は、根本的に誤っていたのでしょうか?
Vuex公式の言葉なので、ちょっと受け止め方に迷いましたが、この記事ではVuex公式のこの文が不適切だという(些か傲岸不遜な)スタンスを今回は取りたいと思います。
というのも、
- 「Vuex公式が言っている」 → 「正しい設計である」
- 「Vuex公式が言っている」 → 「正しい使い方である」
とは、必ずしも言えないからです。
要件定義やマーケティングの文脈では、「ユーザーは自分が欲しいものを自分で分かっていない」とよく言われます。自分が何をしたいのか、本当に求めているものは何かを正確に把握することは、一般に非常に難しいことです。それと同様に、Vuex公式のメンバーだって、自分達がVuexに何を求めているかを誤解している可能性は十分に有り得ます。もしかしたら、深い意味は無く、単にキャッチーで分かりやすい利用例を挙げてみただけかもしれません。
そうした可能性を考えると、「公式がそう言っているから」と思考停止するよりは、「私はこのように考えるので、この文は間違っていると思う」と主張する方が、私は好ましいと思います。実際、私はこの公式の文はVuexの目的を曖昧にしてしまうと思います(実践的にも、この利用方法はアンチパターンにあたると思います8)。
以上より、Vuex公式の「props/emitのショートカットとしてVuexを使おう」と取れる文について、この記事では**「それは不適切だ」**というスタンスを取ります。
-
視点を変えれば、「私達がVuexストアに責務として負わせるべきもの、負わせてはならないものは何か」、という表現も可能だと思います。 ↩
-
「禁じられています」と言いつつ、実は直接state変更もデフォルトでは可能なようです。Vuexの厳格モードを有効にすれば、直接stateを更新すると例外が発生するようになります。 ↩
-
責務の内容が、即ち「Vuexストアですべきこと」である、ということです。 ↩
-
https://en.wikipedia.org/wiki/Single-responsibility_principle#cite_ref-3 (at 2020/07/02) ↩
-
https://drive.google.com/file/d/0ByOwmqah_nuGNHEtcU5OekdDMkk/view (at 2020/07/02) ↩
-
https://web.archive.org/web/20150906155800/http://www.objectmentor.com/resources/articles/Principles_and_Patterns.pdf p.23 (at 2019/12/13) ↩
-
原文ではClassと限定されていますが、クラスに限定せずとも一般に言える内容だと思うので、ここではモジュールという言葉にしています。 ↩
-
ただし、少なくともVuexにおいて、その判断が好ましい一般的な場面は特に思いつきません。あくまで「可能性として0ではないかもしれない」ぐらいのつもりです。 ↩
-
今後のVuexの進化やVuexを組み込んだ設計・実装の発展次第では、新たなVuexストアの役割が見出され、また別の責務を応用になるかもしれません。その時は、またその責務に応じてアンチパターンの範囲も変更されるでしょう(と言いつつ、新しい役割を見出すぐらいなら、その役割をよりスマートにこなすライブラリが新たに登場する気もします) ↩
-
ここでの「基礎」は、文字通り全体の土台となる部分、の意味です。 ↩
-
個人的には、可能な限り使わない方が好ましいと思っています。 ↩
-
Rubyメタプログラミングの文脈で聞いた覚えのあるフレーズなのですが、出典知りません。アメコミ? ↩
-
自分の誤読か、あるいは誤訳であって欲しい(が、英語版をgoogle翻訳にかけても同じような文章なので、訳は多分正確・・・)。 ↩