30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

SPA フルリニューアル計画における技術選定や設計思想(2019年2月版)

SPA のフルリニューアルを技術選定や設計からやることになった。

前回の記事も、そのために検討や調査を行っている際に生まれた副産物をまとめたものだ。
目指すべきは変更しやすいシステムであり、そしてそれは、堅牢性を実現することで達成されるはずだという結論に至った。

numb86-tech.hatenablog.com

今回の記事では、堅牢なシステムの実現に向けてどんな技術を選んだのかを記録しておく。
まだ検証フェーズというか、試し書きや検証を行っている段階なので、今後変わる可能性はある。

前提

現行のアプリは Rails アプリで、その上に Vue を載せて SPA を作っている。
フロントエンドのビルドは Webpacker 。別のプロダクトでは Webpacker を剥がしてしまったが、このプロダクトでは実現できていない。
ビュー関連の処理について、どこまでを Rails でやってどこからをフロントエンドが行うのか、その責務の切り分けが曖昧で、整理されていない。記述が重複しているためデッドコードになっている、なんてこともしょっちゅう。

このアプリを、Rails とフロントエンドで完全に切り分け、Rails アプリはAPIサーバーとして使うようにする、というのが主目的の一つ。
自分はフロントエンド側の構築を行う。

詳しくは書けないが、かなり問題を抱えているコードで、一瞥しただけで強い違和感や忌避感を抱くような代物になっている。
別にフロントエンドだけが汚いというわけではなく、Rails 側の実装についても、Rails 初心者の自分でも突っ込みを入れたくなるような実装が多々あった。

コミットログを見る限り、長期の運用で技術的負債が積み上がったわけではなく、ローンチ時点で既に様子がおかしいようだった。
当時私はまだ在籍していなかったが、どうやら、リリースを最優先してかなり急拵えで作ったものらしい。本当にただ、動いているだけ。眼の前の要求を満たすためだけのコードが無秩序に積み上がっている。
土台が歪んでいると、新しく追加するコードもそれに合わせねばならず、どんどんカオスが拡がっていく。テストも満足に書かれていないから、リファクタリングもままならない。

そして、土台を作った開発者たちは既に誰もいない。開発会社に外注しており、その会社との契約はローンチ後に切れている。
プロジェクトを取りまとめるべき、弊社側の開発責任者は、ローンチのかなり前の段階で消えてしまったらしい。
現在のメンバーは、最古参でも、ローンチ直前に関わり始めたくらいのレベル。

これが前提。
今回はこの過ちを繰り返さないようにしたい、というのが主要なテーマ。
それを実現するために採用しようとしている技術、設計思想、テクニックを、列挙していく。

React

既存アプリに対する問題意識

書き方に一貫性がない。同じ意味のことをやるのに、違う書き方、違う機能を使っている。
Vue は良くも悪くもどうにでも書けてしまうというか、柔軟すぎるという印象を持っている。結果、統一感がなく可読性が低い実装になる。

さらに、何でも許容してしまうことで、保守性や可読性が低い書き方をしても、取り敢えずは問題なく動いてしまう。
一例として、エントリポイントでVue.component()を連発しているので、コンポーネント同士の依存関係も分かりにくくなっている。Webpacker でビルドしているからモジュールシステムを使えるのに、なぜわざわざグローバル変数を増やしまくる?

コンポーネントが状態とロジックを持ちすぎているという問題もある。
datamethodsをどんどん使って気軽に複雑にしていく……。

チーム内でコーディング規約を定めてそれを遵守していれば、防げたのかもしれない。
実際これらは、 Vue の問題というより、Vue の使い方の問題である。
Vue であったとしても、正しく使えば堅牢なシステムを構築できるのかもしれない。
しかし、人間の善意や注意力に頼った規約だのルールだのは、いずれ破られる。複数人で開発するなら、確実に破られる。
型システムが典型だが、人間の意思ではなく、仕組みによって書き方を制限して逸脱を抑止するのが理想。

期待している効能

React を使うことで、副作用のない、冪等性の保たれた関数として、コンポーネントを作りたい。
同じpropsを渡せば必ず同じビューを返す、シンプルな関数。
DDD がアンチパターンとしている SmartUI を避け、UIを構築するという本来の役割に集中させたい。

そうすることで、シンプルな実装になってコードの見通しがよくなり、疎結合になって再利用性やメンテナンス性も高まる。

React は原則としてコンポーネントに状態を持たせないことを推奨しているので、「コンポーネントに状態やロジックを持たせすぎない、という方針からいつの間にか逸脱していた」ということが起こりにくいはず。少なくとも、どんな書き方でも出来てしまう Vue よりは方針を守りやすい。

状態を持つのはボタンのオンオフなどそのコンポーネントで完結するものだけで、ドメインロジックは全て Redux で扱う。
そして、Redux とやり取りするのはコンテナコンポーネントのみで、それ以外のコンポーネントは親からデータを受け取るだけにする。
これを徹底することでコードに一貫性が生まれるという効果もあるのではないかと、期待している。

Redux

Vuex ではなく Redux を使いたかったというのも、React を採用した理由の一つ。

既存アプリに対する問題意識

現状では状態管理に Vuex を使っているが、到るところから Store が参照されているし、Store の更新方法も統一されていないため、コードを追いにくい。また、影響範囲を読みづらいため、作業がしづらいしバグを埋め込むリスクも高い。

そもそも、Store に入れている情報もあれば、コンポーネントが持っている情報もあり、そこに法則性はない。複数のコンポーネントで使っている値でもコンポーネント毎に保持していたりするので、恐らく特に方針とかはない。
最初はあったのかもしれない。方針なりルールなりが。だが少なくとも自分が参画した時点では、何も無くなっていた。

Vue と同じでこれらも、そのライブラリが悪いのではなく使い方の問題だとは思う。
そして、Vuex にもメリットはある。非同期処理は Vuex のほうが扱いやすいし、getterも便利だ。

だが Vue も Vuex も、厳密性がなく、どんな書き方でも許容されてしまうのが問題だと思っている。
外注先の開発会社によるコミットやPRの記録が残っているだが、これがひどかった。
先輩社員と思われる人物が、コードレビューで Vuex の使い方について指摘していた。その内容は自分も同意見だし、修正も難しいものではない。だがレビューイは「公式ドキュメントを読んでもよく分かりませんでした。あとで相談させてください」と返して、そのまま放置してマージされていた。そのままでも動くことは動くからだろう。結局、数ヶ月後に見つけた自分が直した。

色々と思うところはあるのだが、取り敢えず「よく分からないコードをプロダクトに入れるんじゃねえよ」という気持ちだ。
こういう人はどのライブラリを使っても同じなのかもしれないが、Vue は「よく分かっていない人でも動かせてしまう」という傾向が相対的に強い気がする。
「変な使い方をしている奴が悪い」と言ってしまえばそれまでだが、そもそも変な使い方を可能な限り制限するべきではないのか。

期待している効能

Redux を採用し、そのお作法に従う。入門記事でよく図解されているけど、Redux ではデータの流れが決まっている。
開発者はそれを理解し従う必要がある。
だから Redux を採用することで、状態の取り扱いを統一化できると期待している。各コードが好き勝手に状態を取り扱っている現状から脱却する。

そして、Redux とやり取りを行うのはコンテナコンポーネントだけにして、その下にプレゼンテーションコンポーネントをぶら下げていく。
プレゼンテーションコンポーネントは親となるコンポーネントからpropsを受け取るだけ。
こうすることで、状態の取り扱いの見通しがよくなる。どのデータがどこからどう使われているのか追いにくい、という状況が少しはマシになるはず。

デメリットとしては、状態を子コンポーネントに渡していくバケツリレーが発生してしまうこと。そして、必ず決まった方法でないと状態を更新できないため処理が冗長になること。
だが冗長になる代わりに一貫性が生まれるし、方法を一つに決めてしまうことでコーディング規約が守られやすくなる。
複数の書き方を許容したり、あるルールに従って状況に応じて書き方を変えるようなやり方は、混乱を招きやすい。特に複数人での開発でそれをやると、必ず破綻するだろう。

そのコンポーネントで完結する状態については、React Hooks で管理することを検討している。

また、このアプリはいくつかの機能に分割できるのだが、メインではない小規模なものについては、Hooks や Context で対応する予定。
小規模なアプリに Redux を導入するのは、無闇に冗長にしてしまうだけで、割に合わないと思っている。

Store に DDD の考え方を導入する

既存アプリに対する問題意識

とにかく Store が雑然としている。ただ単に「一箇所に集まっている」以上の意味はなく、無秩序にデータが寄せ集められている。しかもその「一箇所に集まっている」というのも、上述のようにコンポーネントが状態を持ってしまっていることで崩れている。

APIのレスポンスをそのまま Store に入れてしまっているのだが、APIの設計もかなり問題を抱えており、本来の意図とは違う使い方を余儀なくされるなどの状況になっている。

その結果、データの二重管理が起きていたり、本来の意味とは違う意味や役割を担わされてしまっていたり、そもそも意味が明確に定義されておらず「取り敢えずこれを使えば要求された機能を実現できるから」という理由で値を参照していたりする。
すると、どのデータをどこでどう使っているのか分かりづらく変更がしにくいし、使い勝手も悪い状態になっている。
新規開発や修正の際にどの値をどのように使うべきかも、明確ではない。

期待している効能

雑然と無秩序にデータが寄せ集められている Store から、意味のある構造を持った Store に変えたい。
そのために Store に DDD の考え方を導入する。

ヒントになったのはこの記事。

medium.com

Redux ではなく Vuex だが、問題意識や現状がかなり近いし、記事の内容にも大きく共感できた。
複雑さを Store に閉じ込めそこで戦うようにして、複雑さがコンポーネントに漏れ出さないようにするのが重要だと思う。
Store を、DDD でいうところの腐敗防止層として機能させる。

そのために、APIで得た値をそのまま入れるのではなく、ドメインモデルを作ってそこに必要な値を入れていく。
必要なデータが、きちんと意味のあるまとまりとして Store に入っている状態を目指す。

今回のフルリニューアルで肝になるのが、この部分だと思っている。
自分たちが扱っているドメインは複雑なものなので、複雑になること自体は避けられない。また、仕様変更や仕様漏れも頻発する。
それをいかに Store で制御できるか勝負。

CSS in JS を導入する

既存アプリに対する問題意識

とにかくカオスとしか言いようがない。

まず、依存関係を追うのが困難。
node_modules/を git でコミットしており、その中にあるスタイルシートを直接参照している。
Webpacker とアセットパイプラインの使い分けが出来ていない。両方で使われているスタイルシートが複数ある。
SPAも、スタティックなページも、Rails でビューを管理しているページも、同じスタイルシートを参照していたりする。本当に必要な定義はどれなのか、よく分からない。

スタイルの定義自体もよくない。
マークアップの階層構造に強く依存する形で書かれており、スタイルどうしも関連を持ってしまっており、変更に弱い。いろんなものが密結合になっている。
body直下のdivに入っているspan要素のうちhogehogeというクラスを持っている要素の小要素の……」みたいな書き方をしているので、div要素を1個削除しただけで、全てが壊れかねない。
!importantを連発しており、設計が壊れている。

こうなってしまうと、どのスタイルがどこに適用されているのか理解するのが困難で、手を加えにくい。触るのが怖い。
影響範囲を読めないので、変更が必要になった時はやむを得ず、新しいクラスを作って上書きする。クラスの粗製濫造である。

これもまた、次第に腐っていったわけではなく、デザイナー(これも外注)が納品してきた段階で、既にこんな感じだった。
改善しようとしたこともあったが、途方に暮れ、挫折した。

期待している効能

正直なところ自分もスタイルシートは苦手であり、知識もなく、解決法を持たない。

とにかく、どのスタイルシートをどこから読み込んでいて、どのコンポーネントがそのスタイルシートの適用を受けているのかを、分かるようにしたい。
カプセル化して、影響範囲を小さくしてしまいたい。そうしないととてもじゃないがメンテナンスできない。

スタイルシートそのものは外注しているデザイナー(上記のデザイナーとは別)が作成するので、出来上がったものをどのようにコンポーネントに適用していくのかを考える。

具体的なライブラリについてはまだ調査していないから、CSS in JS で本当に解決できるのかもよく分かっていない。
取り敢えず、Styled Componentsemotionといった有名所について調べる予定。

JSDoc でコメントを書く

その関数が、どんな構造の引数や返り値を想定しているのかがすぐに分かるのは、メンテナンス性の観点からとても重要だと思っている。
TypeScript を導入するのがベストなのだろうが、私自身やチーム全体の学習コストを考えて、今回は見送る予定。中途半端に手を出すより、それ以外のところの学習や熟達にリソースを割く。
当面は JSDoc でいく。

テストを書く

テストがないことで、バグを埋め込んでも気付けない。それゆえ、動いているコードに手を加えることに消極的になるし、リファクタリングにも二の足を踏んでしまい、悪い設計やコーディングが放置されて更に混沌が深まるという悪循環に陥る。
勇気を出して、あるいは必要に駆られて変更を行うときも、恐る恐るコードを触ることになるし、いちいち手動テストをしなければならず効率が悪い。

テストでバグに気付けない場合、ユーザーからの問い合わせで不具合が発覚することが多い。
そうなると、スピード優先の対応ということでその場しのぎのコードが埋め込まれてしまい、ますますコードの質が下がり、システムの見通しは悪くなっていく。

テストがあることで、バグの混入に気付きやすい、テスタブルにすることでコードの品質が高くなる、テストがあればリファクタリングや修正の土台になる、テストコードがドキュメントの役割も果たす、といった効能が生まれる。

コンポーネントの動作確認には Storybook の利用も検討している。

結果はいかに

愚痴も書いたが、技術選定はその技術を選ぶに至った背景や文脈が重要なので、ちゃんと書いた。

まず課題ありきだからだ。
解決したい課題があって、それを解決するためのアイディアとして、いろんな技術や知識がある。
最新のライブラリや人気の開発手法を取り入れれば万事解決、というものではない。

だから、自分はどういうことを経験してきて、何を課題だと感じているのかが、大切になる。
そこから、選ぶべき技術やライブラリが導き出されるのだから。

ここに書いたものは、あくまでも自分が置かれている状況や課題に対して有効だと思ったから、採用したに過ぎない。
React より Vue のほうが適している状況だってあるだろう。

本当にここに書いた内容で上手くいくのか、その答え合わせは半年後くらいだろうか。
その前にこのプロジェクトや会社そのものが終わっている可能性もあるわけだが。