メインコンテンツへスキップ

概要

変更容易性(modifiability)とは、ソフトウェアが低いコストと低いリスクで変更を受け入れられる性質のことです。 ソフトウェア、とりわけユーザーインターフェイスに完成の状態はありません。デザインは磨き込まれ、機能要求は移り変わり、データの扱いも進化し続けるため、コードベースの実質的な評価軸は「いま正しく動くか」ではなく「次の変更をどれだけ安く安全に受け入れられるか」になります。 変更はデザインの変更、機能要求の変更、パフォーマンスなどデータ処理の都合といった複数の方向から同時にやってきます。 変更に弱い構造はこうした変更のひとつひとつを大がかりでリスクの高いプロジェクトに変えてしまい、変更のために作られた構造はその多くを小さな日常的な編集にとどめます。この構造を作り維持することは開発の中核的な仕事であり、後回しにできる仕上げ作業ではありません。 このページでは変更を分けるなおす壊すの3種に分類して変更容易性を定義し、分類が重要な理由、変更容易性が破綻する3パターン、そして職種を越えた協働による守り方を解説します。

変更の3分類

すべての変更がコードベースに同じ負荷をかけるわけではありません。やってきた変更を何が変わらず、何が変わるのかで分類すると、その変更が構造のどこに依存しているか、したがって何を事前に準備しておくべきかがわかります。 3種類の違いを、注文履歴の一覧を表示する次のコードを共通の例として見ていきます。表現を持つOrderItem、一覧をまとめるOrderList、入れ物であるOrderHistoryModalがそれぞれ自己完結したユニットで、必要なデータを外から受け取ります。
共通の例:注文履歴の一覧表示

分ける

影響を受けるユニットの入力も出力も変わらず、配置や表現、量だけが変わる変更が「分ける」です。 モーダルダイアログの内容を専用ページに移す変更が典型例です。その内容が受け取るデータも生み出すデータも同じで、変わるのは入れ物だけです。 モーダルの中身が、必要なデータを外から受け取るだけで完結するユニットとして実装されていれば、この変更はそのユニットの置き場所を変えるだけで済み、数日ではなく数分の作業になります。
分ける:モーダルをページにする
量の変化もここに含まれます。データが増えてページネーションが必要になっても、1件ごとのデータの形は変わらず、一度に扱う量が変わるだけです。 よく分離された構造では量に関する関心が隔離されているため、データ量の変化は量を管理するコード以外に触れません。 「分ける」を安くするのは、APIレスポンスの形や現在のレイアウトといった偶然の境界ではなく、概念的なデータモデル、すなわちドメインの本質的なエンティティに沿ってユニットを分割しておくことです。安定した概念を軸に作られたユニットは、周囲がどう並べ替えられても同じ入力を受け取り続けます。

なおす

入力や出力の表現が変わる変更が「なおす」です。カードに画像を表示する、フォームの入力項目を1つ増やす、一覧を新しい順に並べる、といったものです。 「なおす」では影響を受ける各ユニットが変更に応答する必要があります。 ユニットが安定した概念に沿って分離されていれば、応答はそれぞれ局所的にとどまります。変わった表現を持つユニットだけが適応し、その表現に触れないユニットは手つかずのまま残ります。
なおす:注文に商品画像を追加する
「なおす」を安全にするのは変更検知、すなわち変更の影響がどこに及ぶかを安く機械的に確認できる仕組みです。

静的型

型をSingle Source of Truth(SSOT、信頼できる唯一の定義元)から導出しておくと、定義の変更が影響するすべての箇所が、コードを実行する前に型エラーとして浮かび上がります。

自動テスト

観測可能な振る舞いを検証するテストは、変更が振る舞いを変えた箇所でだけ落ちるため、「これで何が壊れたか」が機械的に答えられる問いになります。詳細はテストコードを参照してください。
「なおす」の変更を加えたのに、テストがひとつも落ちず型エラーも出ないときは、その結果を疑うべきです。変更に観測可能な影響がなかったか、検知の網に穴があるかのどちらかです。本来落ちるはずのテストが落ちなかったことに気づき、テストの方を直すことも、「なおす」を安全にする仕事の一部です。

壊す

仕組みやその表現そのものを消す変更が「壊す」です。 コードを消すこと自体は簡単で、誰にでもできます。難しいのはそれを安全に、早く、簡単に実行することです。何かを消したとき、一見無関係などこかが壊れてはいけません。 ある削除が数分で済むか、隠れた依存関係を何日も追いかける調査になるかは、削除の時点ではなく、そのコードが書かれた時点の構造で決まっています。
壊す:注文履歴の機能を廃止する
削除に備えた設計とは、依存関係を明示的かつ一方向に保ち、「これに依存しているのは何か」という問いに機械的な答えがある状態を保つことです。依存元を列挙できないコードは自信を持って消せず、消せないコードはその後のすべての変更のコストを恒久的に押し上げます。

分類が重要な理由

この分類が有用なのは、変更の種類ごとに頼る構造的な準備が異なるからであり、「変更しやすいコードにする」というスローガンを具体的な設計判断に変えられるからです。 種類ごとに必要な準備が違います。「分ける」は概念的なデータモデルに沿ったユニット境界に頼ります。「なおす」は、変更の到達範囲を浮かび上がらせる変更検知(型とテスト)に頼ります。「壊す」は明示的で列挙可能な依存関係に頼ります。 コードベースはある種類に強く別の種類に弱いことがあるため、「これは変更しやすいか」は実際には3つの問いです。 分類を誤るとどちらの方向にも無駄が出ます。「分ける」で済む変更を「なおす」として扱えば、移動するだけでよかったロジックを書き直すことになります。「なおす」べき変更を「分ける」として扱えば、追随すべきテストや型を更新しないまま振る舞いの変更を出荷することになります。まず変更の種類に名前をつけることで、応答を釣り合ったものに保てます。 たとえば先ほどの「モーダルをページにする」変更は、扱い方によって次のような差になります。
「分ける」で済む変更を「なおす」として扱うと
分類は設計の道具です。新しいコードを書くとき、「将来のどの変更を『分ける』で、どれを『なおす』で、どれを『壊す』で受けるべきか」という問いは、ユニット境界をどこに置くか、どの型を再宣言せず導出するか、どの依存関係を明示しておくかという具体的な判断を生みます。 準備の差が生む違いは、型の宣言方法によく表れます。次の2つの型宣言は同じ振る舞いを実装しますが、依存先の定義が変わったときの応答はまったく異なります。
手で再宣言した型と導出した型
あとからgreetingに新しい必須フィールドが増えたとします。手で書き写した型ではMessageArgsが古い形のまま静かに残り、内部のgreeting呼び出しだけがエラーになって修正は局所にとどまります。その結果、messageの呼び出し元は自分が依存する概念が変わったことを知る機会を失います。 導出した型ではその要求がMessageArgsに伝播し、messageのすべての呼び出し元がコンパイルエラーになるため、変更の到達範囲が即座に見えます。 依存関係はどちらの書き方にも存在します。型を導出することは、その依存関係をコンパイラに見えるようにし、したがって検知可能にすることなのです。

変更容易性が破綻する3パターン

変更のために作られた構造にも限界はあります。よく構造化されたコードの変更がそれでも苦痛になるとき、原因はたいてい3つのパターンのどれかであり、3つともコードの中ではなく、変更がどう決められどう伝えられるかの中に発生源があります。

前提の破綻

構造は合意された概念モデルの上に作られます。どんなエンティティがあり、データはどうまとめられ、境界はどこにあるか、という合意です。そのモデル自体を無効にする変更、あるいは合意した分け方を後から破る実装が「前提の破綻」です。 たとえば「データは日付単位でまとめ、その中にデータの種類と値を入れる」と合意していたのに、届いたデータが種類ごとにまとめられ、その中に日付ごとの値を持っていたとします。合意したモデルから引かれていたすべてのユニット境界が誤った場所に引かれていたことになり、コストは1回の「なおす」ではなく、その連鎖になります。 概念モデルは「分ける」と「なおす」が立つ土台です。概念の認識を合わせること、そしてモデル自体の変更を通知ではなく拒否の余地がある相談として扱うことが、この土台を安定させます。

ゆらぎ

同じ種類の表現や同じデータ概念が場所ごとに違う仕様で決められる、あるいは仕様が変更と差し戻しを行き来する状態が「ゆらぎ」です。 ゆらいだ変種のひとつひとつが固有の構造と固有の変更検知の配線を必要とするため、変更を安くするための仕組みのコストを、概念的にはひとつのものに対して繰り返し支払うことになります。 変更容易性は反復を通じてより良いプロダクトに収束するための手段であり、実際には決められていない意思決定を吸収するための仕組みではありません。 防ぎ方は一貫性と記録です。確立した概念には確立したパターンを使い回し、意図的に決定を見直すときは何をなぜ変えたかを記録して、コードベースが振動ではなく収束に向かうようにします。

暗黙の変更

人間は差分の検知が非常に苦手です。 機能レベルの変更はリスク、実現可能性、互換性とあらゆる角度から確認される一方で、テキストを中央寄せにするといった小さな見た目や振る舞いの変更は、変更を望む側にとってリスクを感じさせないため、宣言されないまま通り抜けてしまいます。 この非対称には構造的な原因があります。機能要求には明白な失敗モードがあるため精査を引き寄せますが、非機能の細部にはそれがなく、確認の対象にならないまま実装に到達します。 しかし宣言されない変更はあらゆる安全網から見えません。テストは更新されず、レビュアーは何を見ればよいかを知らされず、変更の事実はどこにも記録されません。 防ぎ方はすべての変更を明示することです。変更を依頼するときに伝え、文章のログに残し、実装するプルリクエストにその変化を説明として書くことで、変更はレビュアーにも、あとから履歴を読む人にも見えるようになります。

職種を越えて変更容易性を守る

変更容易性は実装チームだけでは維持できません。破綻の3パターンはいずれも職種の境界、すなわちデザインと実装の間、APIの提供側と利用側の間、変更を依頼する人と実装する人の間で発生します。 したがって変更容易性を守ることは、コーディング規約ではなく協働のプロトコルです。
1

実装の前に概念を合意する

境界の両側でコードが書かれる前に、概念的なデータモデル(エンティティとそのまとめ方)について認識を合わせます。モデルの範囲内の日常的な変更は連絡だけで十分ですが、モデルを壊す変更は事前の相談に値し、利用側には実質的な拒否の選択肢が残されているべきです。
2

決定を一貫させ記録する

確立した概念にはひとつの仕様を与えて使い回します。決定を見直すときは、新しい決定とその理由を双方から見える場所に記録し、同じ議論の繰り返しとコードベースの振動を防ぎます。
3

すべての変更を明示する

どれほど小さく見える変更でも、依頼するときに宣言し、結果として生じる振る舞いの変化をプルリクエストに説明として書きます。作業を小さく独立して差し戻せる単位に分割しておくと、個々の変更が説明可能な大きさに保たれます。詳細はプルリクエストタスク分解を参照してください。
4

変更検知の網を維持する

型はSSOTから導出し、テストは観測可能な振る舞いを検証する状態を保ち、他の職種から依頼された変更を含むあらゆる変更の到達範囲が機械的に浮かび上がるようにします。詳細はテストコードを参照してください。
職種の境界での有効な基本姿勢は、日常的な変更は伝えてもらえれば歓迎し、合意した概念を破壊する変更だけは事前の交渉を必要とする、というものです。これにより普段の協働のコストを低く保ちながら、すべてが依存する土台を守ることができます。

関連ページ

プルリクエスト

個々の変更を小さく、説明可能で、レビュー可能に保つ、変更を明示する単位です。

タスク分解

独立して差し戻せる単位に作業を分割し、すべての変更を安く取り消せる状態に保ちます。

テストコード

「なおす」を安全にする変更検知の網を構築します。

リリース戦略

段階的に変更を届け、大きな変更でも可逆に保ちます。