フロントエンドの負債と向き合う - mizchi's blog

フロントエンドの負債と向き合う

某所で書いたものを公開用に書き直したもの

前提

フロントエンドでTDDは難しい、というかほぼ不可能である。なぜなら事前に副作用をデータとして表現できるか不明だからだ。たとえばあなたのプロダクトの画面の何処かにボタンを追加するために、その内部表現を事前に思い浮かべることが可能だろうか?

react-redux などのFluxフレームワークは如何に副作用をアクションとして表現することで、テスト・デバッグのための情報を残すか、という視点で発展してきた側面がある。あの冗長なアクション定義は、全てデバッグのために書いていると言っても、過言ではない。それすら「Textは文字がある」といったトートロジーなデータになりがち。

フロントエンドの現実的な単体テストは、他の開発者のために、自分が書いたコードの要求を満たしているか検知する手段として、防衛的にテストアフターしておく。これぐらいしか現実的な手法がない。それ以上は、手動テスト、E2E、あるいはUXデザインの範疇だ。

GUIの開発は、最終的に目に見えてるものがすべてであって、DevTools上のパフォーマンス上のメトリクスではなく、「ユーザーがどう感じたか」に集約される。そして人間の感性は理不尽だ。その理不尽さに付き合うために、どの部分を外部から入力可能な値にしておくか、が設計の妙になる。

要は、UXは単体テストの範疇外だが、そのための変数は外部入力で受け付けるべきだ。

負債と向き合う

よくある状況

このデッドロックを乗り越えるためには、実際必要なのは「気合い」である。ただし「気合い」にも種類がある。より効率が良い「気合い」を目指すべきだ。

とはいえ、最初にいうべきは次のような言葉になる。

「このような状況は会社、開発チームとして溜め込んだ負債であるからして、この改善には痛みを伴う」

残念なことに、まずこれを認めることから始めなければならない。自分も、チームも。

手段

負債と向き合うための現実的な施策として、次のような感じになるだろう。

  • 手動テストを密にやる
  • 書き換えたコードを一部のユーザーにだけ徐々に適用していく
  • エッジケースのエンバグを一時的に許容してもらう
  • (厳密にはリファクタリングではないが)コードの簡略のために仕様を変更することを認めてもらう
  • そもそもコードを破棄して作り直す

個人の裁量でやれるのは、おそらく最初の手動テストの厚さだけで、それすら外部テスターがいる場合にはそれを共有する必要があり、要は大抵は「政治」が付いて来る。誰を説得するか、それもリファクタリングに必要な手続きだと思うしかない。

とにかく手動テストを繰り返しながらコードを分解する。

破棄して作り直すかどうかは、プロダクトの成長を一時的に止めることになるので、それが許容されるかどうかはプロダクトの事情による。見積もり次第。

一つの指標として、暗黙知を多量に要求するものは困難なので、他人が書いた jQuery は作り直した方がいい。

リファクタリングの仮のゴール設定

最初の単体テストの話をした理由だが、自分は大抵、「単体テストが書ける」という状況を、リファクタリングの仮のゴールに設定する。単体テストが書けるということは、不要な依存が切れて、モジュールの境界面が自明になった状態にたどり着いたということである。

備考: これは最近までモジュールシステムが存在しなかった、 JS ならではの事情による。長く開発されてきたプロダクトは、グローバル変数渡しや、自前のDI機構が残っていることだろう。

正直な所、「単体テストできるような状況」を目標にすればよく、実際に単体テストを書く必要はなかったりするのだが、わかりやすい成果物としてユニットテストを追加することが多い。この状態に至ってから、目的を達するコードを書き始めることができる。パフォーマンスだったり、ボタン追加だったり。

ただ、そういうテストがメンテされなくなって久しくなったときに、捨てられるかどうかの判断をできる必要はある。担当者が去ったテストが、カーゴカルト的に維持されてることは多い…

できることからやる、ために小さく分解する

大変なものを大変だと言いながらやる前に、痛みが少ないものからまず向き合う。外堀を固めよう。

フロントエンド開発で、最初に考えるべきは、

  • lint ルールを追加する
  • コードフォーマッタを入れる
  • 型を書く

これらは比較的痛みがない。まず eslint を導入して、CI に入れる。そしてルールを思いつくだけ書く。(各プラグインのrecommendedを全部突っ込むなど) 現代では、ホワイトスペースや改行ルールは人間が考えるものではない。prettier の導入で終わり。

経験上、この3つがわかりやすく効く。

  • globals: 使用可能なグローバル変数の列挙
  • no-unused-vars: 未使用変数の禁止
  • no-unreachable: 到達不可能なコードの禁止

ES Modules がある現代では、特殊な事情(ライブラリが勝手に生やしたり、期待しているもの)がある場合を除いて global 変数は一切使わずにコードが書けるはずだ。その場合も部分的な lint 無効化で対応できるはず。

(この節は、人によって意見が異なるだろう)

現代のJavaScriptにとって、一定以上のプロダクトで静的型は避けては通れないテーマだと思っている。フロントエンドは前述したように自動テストが困難なので、静的解析で出来る限りカバーするだけ事故が事前に検知できる。

とくに静的検査で強烈な開発体験の向上として現れるのが、React の JSX の props に対する静的検査だ。プロパティを変えると、大抵呼び出し元のプロパティ名を修正しないといけない。または自分の想定以上に使われていて、その修正漏れなどが発生する。手元でコードを書いていてもうっかりするから、ましてや他人のコードなど考慮外である。

JSXの発明の偉大な点の一つは、テンプレートに対する入力値を宣言的に記述することで、事前条件を明示したことにある。React以外の他のフレームワークも、現代的なテンプレート、コンポーネントは、ステートレスな関数的な振る舞い、またはステートフルであることが明示されたオブジェクトの形をしているはずだ。また、Component という単位にしたことで、依存が明示的になる。

ただ、痛みのない型の導入、は難しい。というかコツがいる。

flowtype で @flow アノテーションを段階的に追加したり、 typescript の allow-js モードで一旦全部tsコンパイラ管理下に入れてしまって、段階的に拡張子 .ts に置き換える、などの手法がある。

ただし、これはプリコンパイラが前提にある。プリコンパイラがない場合は、まずその導入からはじまる。これはデプロイワークフローに絡んでくるので、なかなかに辛い。ステージング環境を用意して、何度もトライアンドエラーするしかないだろう。

辛いものは辛い。そこは認めるべき。

リファクタの諦めが発生する原因

自分が経験した状況としては…

  • 元のコードの担当者が忙しい or 退職していて、レビューが受けられれない
  • そもそも仕様が↑の脳内にしかなく、逸失している
  • コードが予想もつかない場所で使いまわされていて、テスト範囲が膨大になる(駄目なDRY)
  • 特に仕様が決まっていなかった部分を、一部の人間(開発側またはユーザー両方)にバグと認識されてコミュニケーションコストがかさむ
  • プルリクエストが巨大化 or 細分化され、ブランチの維持で手一杯になるうちに心が摩耗していく

全部辛い。辛いものは辛い。 辛い時は辛いと言う。アラートを上げることは恥ではない。

リファクタリングの目的

プロダクトの改善の為、という大義名分は立てやすいが、そもそも何のためにリファクタするのか、見失わないように意識しておく。

  • 機能追加の速度を上げる
  • バグ抑止
  • 人員追加のためにキャッチアップコストを抑える
  • パフォーマンスチューニングの下準備
  • 自己満足
  • 採用アピール
  • 離職対策

後半の理由は後ろ向きだが、表向きの理由とは別に、現実的にこの目的で行われるリファクタリングは多い。自己満足を含むことが別に悪いことではないが、ただ過度に自己目的化してないか見つめ直す必要はある。過剰な抽象度を備えてしまうと逆に品質は悪化する。

中長期的なリファクタリングは、そのイテレーションの間で発生した現実的な泥臭いユースケースを踏まえ、プロダクトの向かう方向性を描きながら自分たちの事情(ドメイン)をコードで表現するとよい。

自分はフリーランスとしてパフォーマンスチューニングの案件を受けることが多いが、何にせよ最初にやることは大抵リファクタリングである。パフォーマンスの伸びしろは、ほとんどの場合、きれいなアーキテクチャによって担保されるからだ。そして、機能追加の速さも一つの「速度」ではある。

まとめ

リファクタは大変だがやりがいがある。やりすぎは危険。需要に応じて段階的にアーキテクチャを変化させていくとよい。

おまけ