変更に強い設計をしましょうというお話

今回はこちらの記事の解説(意訳)になります。

1
2
How To Optimize for Change
https://www.swyx.io/optimize-for-change/

複雑さの回避について思うところが近かったので、翻訳も含めてまとめていきます。

TL;DR

ソフトウェアは将来の変更も含めて仕様みたいなもの。
これに対応するためのポイントは以下の通り。

  • 通常の変更の想定に留める
  • 単純な値のみを扱う
  • 変更距離を最小化する
  • エラーを早期発見するDevOps

導入

あなたがMagic Money Corpを運営していて、以下のコードが利益を生んでいるとします。

1
2
3
let input = { step1: 'collect underpants' }
doStuff(input)
profit(input) // $$$!!!

doStuffに問題が見つかり、一時的にロジックから削除しなくてはいけなくなりました。
しかし、これをコメントアウトした瞬間、profit内部がエラーまみれになって資金源が絶たれてしまいました!

元々はdoStuff内部でinputに様々な更新が加えられており、
profitではその更新が前提となる処理をしていました。
そこでdoStuffがコメントアウトされてしまったため、不整合が生じてしまったということです。

では、inputをImmutableにすればどうでしょうか?

1
2
3
let input = ImmutableMap({ step1: 'collect underpants' })
doStuff(input)
profit(input) // $$$!!!

これならdoStuffをコメントアウトしても、profitの動作には影響ありません!

これは一例ですが、このような、変更しやすい設計とはどういうものなのでしょうか?

なぜ「変更しやすい設計」なのか

改めて、変更しやすい設計は以下のような理由で求められます。

  • 削除しづらいコードは、削除しやすい他のコードを駆逐し続けます
  • 削除しづらいコードは、修正を重ねる過程で技術的負債になっていきます
  • したがって早い段階で、将来の変更に強い設計にする必要があります

この考えは、以下の概念から派生したものです。

ソフトウェア開発において仕様が固まっていることがベストですが、
現実の開発においてそうはいきません。
むしろ、要求が変化すること自体が仕様とも言えます。
開発者は、このことに気を配りながら抽象化していかねばなりません。

通常の変更(common change)に留めて想定する

もし、全てが変更されうる、としたら一体どうやってシステムを構成すれば良いでしょう?
将来の変更に対する過剰な想定は、コード量を倍にも増やしてしまいます。

クライアント要求の微妙なブレに対応できる程度に収め、
システムデザインやアーキテクチャを変更するような事態までは考えない、
というのは一つの方法として良さそうです。

ごく稀な変更に関しては、それは作り直すための理由となり得る、という点もあります。
また、根本からの変更を要求されるような場面は、そもそも予想しやすいという事もあります。

単純な値として扱う

こうした考えを「変更しやすい設計」とする場合、以下のような実装が求められます。

  • 削除しやすい
  • 切り貼りしやすい
  • 抽象化を介して機能の作成、削除ができる

https://www.infoq.com/presentations/Simple-Made-Easy/
Simplicityについて説いたこの資料によれば、
オブジェクトやインスタンスを受け渡している箇所において、
代わりにそれをimmutableでシンプルな値として渡すように変換できます。
このことであらゆる種類の潜在的なバグを回避できるということです。

このことを突き詰めると、イミュータブル化、関数型プログラミング、
といった実践的な概念を得ることができます。
単純さを追求すると計算的なコストを支払うことになりますが、
キャッシュ等の方法で軽減することはできます。

「変更距離」を短縮する

以下の図は、複雑さをよく表しています。

複雑さとは、要素が編み込まれてしまいほどくのが難しいことであって、単一の要素から複雑さが生まれることはありません。
言い変えれば、順番に依存している状態を複雑であると言えます。
順番にはたとえば以下のようなものがあります。

  • 命令の実行順
    • コードの行数を入れ替えることによって全体の動作が変化する
  • プロセスの実行管理による順番
    • OSによるコンテキストスイッチで、どのプロセスが先に実行されるかは変わる
  • ファイルの記述順
    • あるファイルのコードを変更した場合、他の数々のファイルが変更を要求される
  • 引数の順番
    • 与える引数の順番によって関数の動作が変化する

また、複雑さは「変更距離(edit distance)」を用いて定量化できます。

  • もし引数の順番を変更したら、その関数を使用しているすべての箇所を書き直す必要がある
  • ステートレスなコンポーネントに新たに状態を追加する
  • 非同期的なデータの取得が追加された際、いくつものコンポーネントやredux定義をまたいで変更しなくてはならない

複雑さの式が存在するわけではないにしろ、開発者は感覚としてこれを知っていて、現にプロジェクトの進行に影響を与えています。
また、見えないコストとして、コードの変更自体が苦痛なのでイノベーションが生まれなくなるという点もあります。

端的には、変更しやすいコードとは、要素を「編み込む」ことができない設計のことを言います。

バグの早期発見

複雑さを可能な限り排除したとして、本質的な機能に関わる複雑さは当然残ります。
こうした排除できない複雑さへの対処として、短いスパンでフィードバックを得ることが有効です。

このことはShift Leftという単語で表されており、
具体的には以下のような要素を指します。

  • リファクタ時にエラー箇所を指摘できるユニットテスト
  • 型情報によるチェック
  • 15分以下で完了するデプロイ
  • 本番環境を再現できるローカル環境
  • replay.ioなどの、リアルタイムに値を再現できるデバッグツール

頻繁すぎる変更には注意

良い変更であっても、変更のしすぎはそれ自体が良くない結果を招きます。
変更のしすぎとは安定性よりも速度を重視した状態のことで、
あらゆるものがすぐに変わっていく様子はユーザーに動揺を与えます。