フリーランス
エンジニア
現役フルスタックWebエンジニアが解説!変更に強い設計をしましょうというお話
oskn259と申します。
最近は開発プロジェクトの進行や、中長期のソフトウェア開発のアンチパターンに興味を持っているのですが、先日良い記事を見かけたのでそれを解説していこうと思います。
ソースの記事はこちらです。
How To Optimize for Change
複雑さの回避について思うところが僕と近かったので、翻訳も兼ねて纏めていきます。
また、今回の内容はこちらで書いた記事の修正版でもあります。
https://blog.oskn259.com/article/optimize_for_requirement_change
TL;DR
ソフトウェアは将来の変更も含めて仕様みたいなもの。
これに対応するためのポイントは以下の通り。
- 通常の変更の想定に留める
- 単純な値のみを扱う
- 変更距離を最小化する
- エラーを早期発見するDevOps
導入
あなたがMagic Money Corpを運営していて、以下のコードが利益を生んでいるとします。
js
let input = { step1: 'collect underpants' }
doStuff(input)
profit(input) // $$$!!!
`doStuff`に問題が見つかり、一時的にロジックから削除しなくてはいけなくなりました。
しかし、これをコメントアウトした瞬間、`profit`内部がエラーまみれになって資金源が絶たれてしまいました!
元々は`doStuff`内部で`input`に様々な更新が加えられており、`profit`ではその更新が前提となる処理をしていました。
そこで`doStuff`がコメントアウトされてしまったため、不整合が生じてしまったということです。
では、`input`をImmutableにすればどうでしょうか?
js
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などの、リアルタイムに値を再現できるデバッグツール
頻繁すぎる変更には注意
ビジネスが成長したり世の中のニーズが変更すれば、求められる要件も変わっていきます。
ただし、コードの変更のしすぎはそれ自体が良くない結果を招きます。
変更のしすぎとは安定性よりも速度を重視した状態のことで、あらゆるものがすぐに変わっていく様子はユーザーに動揺を与えます。
おわりに
いかがでしたでしょうか?
筆者が元の記事の意図を拾いきれず散らかった部分もあるので、気になる方はぜひ元記事も読んでみてください。
複雑さとは要素が絡み合った状態であり、順番に依存しないことが複雑さ回避への道、という考えは色んなところで使えそうですね。
最近では宣言的、状態の記述、といった概念も現れていますが、まさにこうした考えに基づいているのかもしれません。