NgRx が難しい 5 つの理由

Angular と一緒に使われることの多い NgRx。 非常に強力なライブラリである一方、学習コストが比較的高く、手を出そうとして途中で挫折してしまう方も多くいらっしゃることと思います。

今日はそんな NgRx の挫折あるあるポイントを紹介していきます。

はじめに

こちらの Developers.IO の記事の中で、僕が一昨年に書いた NgRx をりしてみた系メモが参考文献に挙げられていました。 うれし恥ずかし/////

dev.classmethod.jp

そもそも NgRx とは何なのか

さっそく公式サイト(の日本語訳)を引用します。

NgRx は、Angular でリアクティブアプリケーションを構築するためのフレームワークです。 NgRx は、状態管理、副作用side effectの分離、エンティティコレクション管理、ルーターバインディング、コード生成、およびさまざまな種類のアプリケーションを構築する際のディベロッパーズエクスペリエンスを強化する開発者ツールを提供します。

【原文】
NgRx is a framework for building reactive applications in Angular. NgRx provides state management, isolation of side effects, entity collection management, router bindings, code generation, and developer tools that enhance developers experience when building many different types of applications.

【出典】

なんだこの天下り的すぎる説明文は。

OK たーせる。 話が長くなるけど、NgRx が何物なのかをもう少し丁寧に見ていこう。

MVC と Flux と Redux

歴史的な話をすると、その昔、GUI アプリケーションのための MVC: Model View Controller というデザインパターンがありました。

ところが、かの有名な Facebook が、よくある「未読メッセージの件数表示」を従来の MVC のアプローチで実装しようとしたところ、どうしても画面に表示される件数がどこかでバグってしまう事象が発生。

結局、原因調査と解決は困難を極め、MVC の限界が露呈した形となりました。

もともとは、派生データを正しく処理するために着手しました。 たとえば、メッセージスレッドの未読カウントを表示し、別のビューがスレッドのリストを表示し、未読スレッドを強調表示したかったのです。

これは MVC で処理するのが困難でした。 単一のスレッドを既読としてマークすると、スレッドモデルが更新され、未読カウントモデルも更新する必要があります。

これらの依存関係とカスケード更新は、大規模なMVCアプリケーションで頻繁に発生し、データフローの絡み合いと予測不可能な結果につながります

【原文】
We originally set out to deal correctly with derived data: for example, we wanted to show an unread count for message threads while another view showed a list of threads, with the unread ones highlighted. This was difficult to handle with MVC — marking a single thread as read would update the thread model, and then also need to update the unread count model. These dependencies and cascading updates often occur in a large MVC application, leading to a tangled weave of data flow and unpredictable results.

【出典】

アプリケーションの規模が大きくなるにつれて、メッセージパッシングが複雑化して制御困難になっていったんだね。

この問題を解消するために、 FacebookFlux という新しいデザインパターンを編み出したんだ。

これが NgRx の元法になっている。


Flux はかなりインパクトが大きく、またたく間に世の中の開発者たちが追随しました。

Flux 自体は単なるデザインパターン(抽象的な設計思想)でしたが、競うように実装ライブラリが公開され、最終的に Redux という Flux 実装ライブラリが生き残りを遂げました。

Redux は SPA ライブラリの React と非常に相性が良く、『React + Redux』という構成が定番化しました。 その後 Angular 版 Redux として NgRx、Vue 版 Redux として Vuex がそれぞれ定着し、今に至っています。

Redux と NgRx

Redux 自体は単独動作するライブラリなので、別に React じゃなければダメという訳でもなかったです。 実際、2017年時点では Angular と 生の Redux を連携させる方法が書籍化されています(文献: AngularによるモダンWeb開発 実践編~実際の開発で必要な知識を凝縮~)。

ただ、Redux は「副作用が許されない」という宗教上の理由で、非同期処理の扱いがそれはそれは苦手でした。

結局、Redux で非同期処理を扱うには、Redux-Thunk や Redux-Saga などのライブラリを追加しなければならなかったんだ。

Redux 本来のクリーンな設計を半ば強引に侵襲する作りだったので、当時ちょっと賛否両論あったね。


一方、NgRx は初めから公式的に非同期処理をサポートしています。 また、アプリケーションの状態の変更通知は、これまた Angular と相性の良い RxJs の Observable を通して行われます。

Angular と相性がよく、Flux / Redux の良い点を受け継ぎ、さらに非同期処理の扱いにも対応したライブラリとして、こんにちの NgRx があります。

NgRx は難しい。

さて、NgRx 自体は大変高機能なライブラリですが、ネット上で公開されている「やってみた系記事」の大半は、状態管理@ngrx/store(とコード生成@ngrx/schematics)だけを扱っています。

理由はいくつかあるのですが、NgRx の話題をまともに扱おうとすると執筆コストがものすごく高い。 なぜかというと、NgRx 自体がそれなりに難しく、想定読者を相当絞り込まないと、簡潔でわかりやすい説明にならないからです。

NgRx はなぜ難しいのか

理由1: NgRx を学ぶ際の前提知識が多く、入口の敷居がいきなり高い

NgRx は、長い時間をかけた設計思想の積み重ねによって形を成しており、ライブラリを利用するプログラマにも相応のリテラシーが求められます。

まずは RxJs や Redux など、前提知識の基礎固めをしておかないと、最初で躓くことになります。 そもそも学習の出発点に立つまでの道のりが長いのです。

このことは、公式サイトでもトレードオフとして以下のとおり言及されています。

ただし、NgRx の使用にはいくつかのトレードオフが伴うことを認識することも重要です。 コードを記述し、ユーザーに多くのファイルの使用を促すための最短または最速の方法を意図したものではありません。 また、RxJs と Redux のある程度の理解を含む、急な学習曲線も必要になることがよくあります。

【原文】
However, realizing that using NgRx comes with some tradeoffs is also crucial. It is not meant to be the shortest or quickest way to write code and encourage its users the usage of many files. It is also often require a steep learning curve, including some good understanding of RxJs and Redux.

【出典】

ほとんどの技術ブログでは、NgRx の話をするためだけに RxJs や Redux まで話を遡るようなことはしないので、初心者の多くはここで足切りされてしまうのです。

RxJs 自体がパラダイムシフトの産物だからね。

かんたんに言うと関数型言語の文脈で Observable パターンを実装したものなんだけど……。

全然簡単に言えてないね。

ごめん。

そもそも、NgRx の前提知識の RxJs を学ぶには、さらにその前提知識が必要だということ。

キリがないね。

こういうのを、ヤクの毛刈りって言うんだ。

ある問題を解こうと思ったら別の問題が出てきて、それを解こうと思ったらさらに別の問題が出てきて……。


理由2: アーキテクチャを構成する一つひとつの登場人物が分かりにくい

下図に示すとおり、NgRx を用いたアプリケーションには、さまざまな構成要素が登場します。

f:id:tercel_s:20190914131545p:plain
NgRx によるアプリケーション状態管理のライフサイクル

Action, Effects, Reducer, Store, Selector, ...... これらの耳なじみの無い用語に対する認知負荷を克服する必要があります。 NgRx の世界では当たり前のように登場するため、これらが分かっていないとそもそも話についていけないのです。

でも、公式サイトに行けば用語の定義くらいはあるよね。

あるけど、一つひとつの定義を全て足し合わせても全体の理解にはならないよ。


定義したばかりの言葉がその直後から当たり前のように用いられる文章は、(特に理系が書いた文書を読み慣れていない方にとっては)かなりの読解力を要すると思います。

だいたいここで置いてけぼりを喰らって、そのまま挫折してしまう方も多いのではないでしょうか。

新語に出会った読者は、まず定義を丸飲みし、ひたすらチュートリアルやサンプルコードの写経を繰り返すうちに、じんわりと理解が染みついていくものです。

理由3: チュートリアルで、自分がいま何をしているのか分からない

これはホントありがちなのですが、たとえば「いま打ち込んだコマンドが一体何を意味するのか」が分かりづらい状態がずーっと続く系チュートリアル。 特に、後々になって効いてくる操作の場合、その時点では何の意味があるのかが分からないことが多いものです。

人間は、「何のためにやるのか分からないこと」を指示されると、そこで足踏みしてしまいがちで、結果的に体感難度も上がります。

チュートリアルの内容が古くなっていて、手順通りにやってもうまく行かないこともあるよね。

そうだね。

一度躓くと、他にも地雷が埋まっているんじゃないかと警戒してしまう。


理由4: NgRx を敢えて導入する必要性が理解できない

実は、Angular のデフォルト機能だけを使っても、アプリケーションの状態管理そのものはある程度実装可能です。

ですので、React や Vue のようにビュー特化型の SPA ライブラリと比べると、どうしても Angular のアプリケーションに状態管理ライブラリを追加で導入する動機が弱いことは事実です。

本題からは少し逸脱してしまいますが、「初めからできるのに、敢えてより複雑なやり方を強いられている」と思った時点で、人のモチベーションは低下します。

強いていえば、NgRx はコードが自動生成できることと、デバッグ用の強力なツールが附属していることが、大きな付加価値を生んでいると思います。

NgRx は状態管理の手法を一貫させるため、冗長ともいえる構成を採っている。

アプリの規模によってはぎゅうとうかっけいになるし、使いどころの見極めは難しいよね。

デバッグツール@ngrx/store-devtoolsは普通によいと思う。

学習コストはかかるけど、長い目で見ればデバッグ工数は減るんじゃないかな。

まぁ、新しいプログラミングスタイルやツールの習熟って思っている以上に大変だから、重荷に感じるプログラマはいそうだけどね。

アプリじゅうのデータを全部巨大なシングルトンにつっ込むスタイルに対して、拒絶反応を示す人は確かにいそう。

やっていることはデータベースへのアクセスとそんなに変わらないから、そこまで目くじらを立てることも無い気はするけれど。


理由5: 結局、データ配置の設計はプログラマのセンス次第

ここから先は、NgRx 導入後の話になります。

NgRx 依存のコード一式は新たな NgModule 配下に集めるべきか。 どの単位で Feature を切り分けるべきか。 Reducer が太りすぎないようにどう気を配ろうか。 データのツリー階層をどこまで深くしようか。

結局、ライブラリの使い方が分かったところで、プログラマのセンスが問われることに変わりはありません。 それも NgRx の都合に合わせてよりよい形を考える必要があり、僕自身まだ正解を見つけられずにいます。

どんなライブラリにも言えることだけど、使い方を理解することと、効果的に使いこなせるようになることは別モノだと思う。

活用レベルが怪しい段階でプロダクトに組み込むのは自殺行為かも知れない。

まとめ

今日はちょっと趣向を変えて、NgRx 関連の躓きポイントをまとめてみました。

「情報が充実していそうだから」「とりあえず鉄板構成だから」という理由で安易に NgRx 導入を決めるのではなく、メリット・デメリットを比較検討して、導入するならプログラマには充分な学習期間を与えて、開発チームのメンバ全員が困らない現場が増えていくといいなと思いました。

長くなりましたが、今日はここまで!

Copyright (c) 2012 @tercel_s, @iTercel, @pi_cro_s.