■ 1. クラフトの定義と発生原因
- クラフトの起源:
- 技術的負債という言葉はしばしば手抜きなコードや読みにくいコードを指して使われる
- 本来の意味とは違うという指摘がよく入る
- ウォード・カニンガムの定義:
- 最初にリリースするコードは負債を抱えるようなもので少しの負債は開発を加速させるが迅速にリファクタリングで返済される必要がある
- 開発速度と将来のリファクタリングを引き換えにする行為を負債を負うことに例えた
- マーティン・ファウラーの定義:
- 負債を負う選択をした結果予測が外れると理想とは異なるコードができてしまいその後のメンテナンスに悪影響を及ぼす
- 内部品質の問題をクラフトと呼んだ
- クラフトの発生要因:
- 技術的負債以外のさまざまな要因でも発生する
- 明確な設計やコーディングルールが定まっていない状態で作られた不統一なコード
- コーディング規約やアーキテクチャの方針が曖昧なまま進められたプロジェクトでは各開発者の経験や好みで異なるアプローチが採用され全体の一貫性が失われがち
- 設計に問題があると認識していても解決方法が分からなかったり時間的制約から動作するから良いと判断して後回しにしたまま本番環境にデプロイされたコード
- ビジネス要件の急激な変化や予期せぬスケジュール短縮で十分な設計検討ができなかった場合
- クラフトの影響:
- 技術的負債に起因するかどうかに関係なく一度クラフトが生まれると解消されない限りメンテナンスコストを増加させ続ける
- コードの理解や修正にかかる時間の増加
- バグの発生率の上昇
- 新機能追加の難易度上昇
- この負のスパイラルは時間とともに加速し最終的にはシステム全体の健全性を脅かす
■ 2. クラフトの解消方法
- リファクタリングの必要性:
- ウォード・カニンガムもマーティン・ファウラーもクラフトを解消する最も有効な手段はリファクタリングであると指摘している
- クラフトは放置していても自然に消えることはない
- コードベースにおける変更コストの非線形的増大を通じて時間とともにシステム全体の開発速度を低下させる
- ファウラーが述べるように設計上の腐敗として現れ修正コストが逓増する前に手を打たなければ後のリファクタリングは指数的に困難になる
- XPの原則:
- リファクタリングは変更のための投資活動とみなされ機能開発と同列のタスクとしてスプリント内に組み込むことが推奨されている
- 重要なのはクラフトが顕在化した箇所を単に修正するのではなく体系的にリファクタリングを進めること
- クラフト解消のコスト:
- クラフトの解消には必ずコストがかかる
- 全てのクラフトを悪と捉えて躍起になって修正する必要はない
- リファクタリングに要する時間やテストの再実行やレビューといった工数は短期的には機能開発の速度を低下させる
- クラフトを残したままメンテナンスを続けると変更コストが継続的に増大し最終的には累積コストが解消コストを上回る
- この二つのコストを比較し解消の優先順位と実施タイミングを定めることが合理的なクラフト管理の出発点となる
■ 3. クラフトが溜まる理由
- スピード重視の問題:
- アジャイル開発やリーン開発では速く出すことが重視される
- 意思決定のサイクルを短くし顧客からのフィードバックを迅速に得ることで学習速度を高めるのが目的
- この学習速度と実装速度は混同されがち
- コードを速く書くことは学習を早めるための手段でありゴールではないはず
- 仮説としての実装:
- スピードを優先するチームは意思決定を仮説として捉えることがある
- 完全な理解を待たずに仮説として設計や実装を進める
- これは合理的な選択だがその仮説が外れたまま長期間放置されるとコードベースに局所的な歪みすなわちクラフトが生じてしまう
- 問題の本質はスピードそのものではなく仮説を検証した後に修正する時間を確保しないことにある
- 整えるフェーズの欠如:
- 多くのチームでは技術的負債を記録せず修正の優先順位を明示的に管理しないためクラフトが静かに堆積していく
- コードの整合性を取り戻す機会はスプリント外に追いやられ結果として速く出すことが修正できない構造を作り出す
- これが短期のスピードが長期の速度を奪う典型的なパターン
- クラフトが溜まるのは速度の問題ではなくサイクルの非対称性すなわち作るフェーズはあるが整えるフェーズが欠けていることによって生じる
- 負債返済計画の必要性:
- ウォード・カニンガムが述べたように負債は返済を前提とする限り健全なもの
- 問題は返済計画のない負債を繰り返し発生させることでありそれがクラフトの主要な供給源となる
- スピード重視の開発を持続可能にするにはどの負債をいつ返すかを明文化しリファクタリングを開発プロセスの一部としてスプリントに組み込む必要がある
- スピードと品質は対立しない
- 両者の調和は仮説としての実装と検証としてのリファクタリングを一つの循環とみなせるかどうかにかかっている
- 真にアジャイルなチームとは速く作るだけでなく速く修正できる構造を維持し続けるチーム
■ 4. 意思決定の記録の重要性
- ADRの必要性:
- 戻せる構造を制度的に支えるのが意思決定の記録
- どのような仮説のもとにどんな妥協を選択したのかどのリスクを受け入れどの技術的負債を意図的に残したのかを明文化しておく必要がある
- この記録がないまま時間が経過するとコード上の痕跡から意図を再構成することは困難になる
- 結果として後任の開発者はなぜそうなっているのかを理解できず同じ問題を再発させる
- クラフトの多くはこのように技術的選択の文脈が失われた状態から生じる
- クラフトの抑制にはリファクタリングだけでなく意思決定の透明性を保つ仕組みが不可欠
- ADRの構成要素:
- ADRは単なるドキュメントではなく現時点での最良の仮説を記録するメタ構造
- 決定の背景
- 選ばなかった代替案
- 想定している寿命
- 検証すべき条件
- これにより将来の開発者はこの判断はどのような前提でなされたかやどの時点で再評価すべきかを再現的に理解できる
- ADRの役割:
- ADRとは技術的負債を再構築可能な形で管理する装置
- 負債そのものを悪とみなすのではなくいつやどのように返済するかをチームが合意できるようにするための会計帳簿のようなもの
- これが存在すれば速く作ることは短絡的なスピードではなく可逆的な実験としてのスピードに変わる
- コードだけでなく意思決定の履歴までもがリファクタリング可能な構造になる
- クラフトを溜めないために必要なのはスピードを抑えることではなくむしろ速く動きながらも意思決定の痕跡を残す習慣を持つこと
- ADRはそのための最小限の形式であり継続的リファクタリングと並んで技術的負債を健全な仮説として管理するための基盤
■ 5. 技術的負債に伴うクラフトの4分類
- 分類の概要:
- 設計時点の予測と後になって判明した事実との乖離
- これこそが技術的負債に伴って発生しやすい問題だが大きく4つに分類できる
- 4つの分類:
- 混在: 異なる概念が同じ場所に置かれる
- 分散: 同じ概念が分割されて別々の場所に置かれる
- 冗長: 同じ概念が重複してあちらこちらに置かれる
- 欠落: 必要なのに実装されていない
■ 6. 混在の問題と対処法
- 混在の発生:
- 設計時には業務理解が足りず本来分離すべき概念を同じものとして扱ってしまうことがある
- ECサイトの有料会員プランの例:
- 会員プランには3つがありそれぞれ利用可能な機能が異なる
- フリープランは基本的な購入機能のみ
- スタンダードプランは送料無料と5%割引
- プレミアムプランは送料無料と10%割引と優先配送と限定セール参加
- 契約プランによって機能の利用可否を判定するとプランやその特典は会員獲得やリテンション向上を目的として頻繁に追加や変更される
- 問題点:
- プラン判定ロジックがControllerに直接書かれている場合プランが追加されたりプランで使える特典が変わったりするとこれがクラフトになる
- 判定のif文をあちこち修正しなければならない
- 契約プランで判定しているため契約なしで有料機能が使えるキャンペーンを実施する際もこのif文の修正が大変になってしまう
- 対処法:
- 契約プランと使える特典の制御を一体化して考えてしまっていることが原因
- 契約プランと適用される特典を切り離すことでプランを追加したり内容を変更したときの影響範囲を小さくできる
- 業務理解が浅い段階ではこうした判断は難くとりあえずリリースしてから考えようと技術的負債を選択しがち
- セールス都合での仕様変更は他の機能に比べて頻繁に発生する
- 変動性の異なるものは初めから切り離しておくことでクラフトを最小限に抑えられる
■ 7. 分散の問題と対処法
- 分散の発生:
- 分散のクラフトは技術的負債を選択しなくても発生する可能性がある
- 過剰なレイヤー定義などによって責務の不明確な処理が作られることがあるから
- よくあるケース:
- Web層とService層を分けているものの各レイヤーで扱うデータが変わらないまたは同じような型に詰め替えているだけというケース
- これでは再利用性はなく結局Web層とService層のコンポーネントは密結合してしまう
- ホテル予約の例:
- リクエストをもとに予約サービスを呼び出す
- 予約サービスはService層のコンポーネントなのでReservationRequestはそのまま使わず似たような構造のReservationDtoに詰め替える
- この2つの型はほぼ同じ構造を持っているがバリデーションアノテーションの有無だけが異なる
- Web層のReservationRequestにはバリデーションアノテーションがあるがService層のReservationDtoには存在しない
- 問題点:
- Web層でバリデーションを行っているため一見問題ないように思えるが予約サービスを他のエンドポイントや別のシステムから再利用する際に問題が発生する
- ReservationDtoが満たすべき条件が分からないから
- Web層とService層でレイヤーを分けるならServiceの呼び出し条件をReservationDtoの不変条件として表現しなければならない
- そうしなければServiceは特定のエンドポイント専用になってしまう
- 対処法:
- 予約に関するデータの処理の分散を避けるためには業務としてValidであることを保証する型を作るのが有効
- Reservationクラスはコンストラクタで全ての不変条件をチェックしインスタンスが生成された時点で業務として妥当な状態であることを保証する
- Web層ではReservationRequestからReservationを生成しService層ではReservationを受け取る
- コードに重複はあるがそれぞれが異なる関心事を表現している
- Web層のバリデーションはユーザー体験のためドメイン層の不変条件はビジネスルールの保護のため
- 実装の工夫によりコードの重複も避けることも可能
■ 8. 冗長の問題と対処法
- 冗長の性質:
- 冗長はDRY原則に違反するものであり技術的負債とは無関係にクラフトとして現れることがある
- ドメインへの理解が浅い段階でDRY原則にこだわり過ぎると誤った抽象化をしてしまう危険がある
- DRY原則の本質はコードの重複を避けよではなく知識の重複を避けよ
- 十分な知識がなければこの判断はできない
- Rule of Three:
- DRY原則を適用する前にはRule of Threeを意識する
- Three strikes and you refactorすなわち3回出るまで抽象化しない
- ホットスポットの存在:
- 初めから考えておいた方が良いものもある
- 多くのシステムには他の機能に比べて明らかに追加や変更が頻繁に発生するホットスポットが存在する
- モバイルオーダーのキャンペーン例:
- 初回注文20%OFFやオフピーク100円引きやアプリ決済5%OFFなどのキャンペーンを実施したい要求がある
- 今後どういうキャンペーンがあるかも分からないのでキャンペーンごとにバラバラに実装することは技術的負債を選択する観点からも理にかなっている
- 問題点:
- キャンペーンが増えるにつれて同じような割引計算ロジックがあちこちに散らばり保守性が低下していく
- 各割引は独立しているわけではなく適用順序や併用条件が存在するためキャンペーン間の関係性も管理しなければならない
- このような場合は早い段階でキャンペーンを抽象化する仕組みと割引の適用ルールを一元管理する設計を導入しておかないと後からの追加は難しくなってしまう
- 対処法:
- これは混在で述べたセールスの都合と通じるものがある
- キャンペーン機能は頻繁な変更が予想される領域であり早期に抽象概念を作っておく価値が高いホットスポット
- こういったホットスポットは同種のOSSプロダクトやSaaSプロダクトの設計を知っておくと勘所がつかめるようになる
- 今回のディスカウントの仕組みならShopifyのDiscount APIのような柔軟な割引設定が参考になる
- Shopifyでは割引の種類や適用条件や併用ルールなどを統一的なインターフェースで管理できる
- こうした既存の成功事例から学ぶことで将来クラフトになりそうな領域を見極め適切な抽象化のタイミングを判断できるようになる
■ 9. 欠落の問題と対処法
- 欠落の発生:
- 技術的負債を選択する際今は必要ないので作らないと判断するのは実は非常に難しい
- ファイルアップロード機能の例:
- Excelファイルをアップロードし内容をパースしてデータベース登録する
- 元のファイルも残しておきダウンロードできるようにする
- どこまで作り込む必要があるか
- 実装判断の例:
- 同期的にアップロードするか非同期にするか
- アップロード中のユーザーへの進捗表示をするか
- エラーレコードをユーザーにどう知らせるか
- ファイルサイズの上限をチェックするか
- アップロードファイルを一時ファイルに書き出す設計にするか
- アンチウイルスのチェックをするか
- これらのうち現時点でおそらく不要と判断して実装を省略したものが後になって実は必要だったと判明するケースが欠落に該当する
- 技術的負債として今は作らないと決めたつもりでも実際には作るべきだった機能が抜け落ちている
- 対処法:
- 非機能要求が明確に定義されていないと欠落として現れやすくなる
- 性能要件やセキュリティ要件や可用性要件などが曖昧なまま開発を進めると後になってこの規模のデータ量では処理が遅過ぎるやこのレベルのセキュリティ対策が必要だったといった問題が発覚する
- これはシステム運用継続のノックアウトファクターにもなりえる
- 開発するシステムのミッションクリティカル性にもよるが非機能要求に関わる実装は決して後回しにして良いものではない
- 非機能要求に関わる問題は後から対応しようとするとシステムアーキテクチャの根本的な見直しが必要になるケースが多いから
- 具体例:
- 同期処理で作られたファイルアップロード機能を後から非同期化しようとするとUI層からデータベース層まで広範囲な変更が必要になる
- セキュリティ対策が不十分なまま本番稼働してしまうと個人情報漏洩などの重大なインシデントにつながるリスクがある
- 技術的負債として後で対応するという選択は機能要求に対しては有効
- しかし非機能要求に対しては往々にして取り返しのつかない設計上の欠陥を生み出してしまう
■ 10. まとめ
- クラフトをゼロにすることの不可能性:
- 技術的負債に伴うクラフトの分類とその抑止可能性について見てきた
- 当然ながらクラフトをゼロにすることはできない
- 未来予測の活用:
- 技術的負債を選択する際の意思決定において未来予測を完全に捨てる必要はない
- むしろ過去の経験や類似システムの知見やチームの技術的強みやドメインの特性を考慮に入れることでより賢明な判断ができるはず
- キャンペーン機能のように頻繁な変更が予想される領域では早期の抽象化を一方で一時的な機能では最小限の実装を選択するといったメリハリのある意思決定が可能になる
- 持続可能なアプローチ:
- 完璧なコードベースを目指すのではなく技術的負債とクラフトの性質を理解し継続的な改善を通じてビジネス価値を提供し続けながらシステムの品質を維持していく
- それが現実的で持続可能なアプローチ