プロローグ — レガシーコードはあなたが引き継ぐ
新しいプロジェクトは長くは新しくない。半年で、昨日の新しいコードが今日のレガシーになる。1年で、誰も中身を正確に知らないモジュールが一つできる。3年で、「あれは触らないでください」という言葉が会議で出る。
レガシーコードの定義は人によって違う。「古いコード」と言う人もいれば、「自分が書いていないコード」と言う人もいる。Michael Feathers の定義が一番役に立つ。レガシーコードとは、テストのないコードだ。 テストがなければ、変えたときに何が壊れたか知る術がない。だから触るのが怖い。怖いから最小限だけ変える。最小限だけ変えるから、コードはどんどん奇妙になる。
ここにもう一つ、より正直な定義を足そう。レガシーコードとは、あなたが変えるのを恐れているコードだ。 恐れが核心だ。コードが古かろうが新しかろうが、テストがあろうがなかろうが、変えるときに手が震えるなら、それはあなたにとってレガシーだ。
この記事は、その恐れを扱う技術だ。恐れを「勇気」で乗り越えるのではない。恐れを手順に変える。安全に変える順序があれば、コードが怖くても手は震えない。characterization test で現在の振る舞いを固定し、seam を見つけてテストを差し込み、sprout と wrap でリスクを隔離し、strangler fig でシステム全体を段階的に置き換える。そして AI agent がこの作業のどこで加速装置になり、どこで地雷になるかも見る。
一行: レガシーコードを安全に変える秘訣は勇気ではなく手順だ。触る前に現在の振る舞いを固定せよ — その振る舞いが bug でも。
第1章 · レガシーコードのジレンマ — 鶏と卵
レガシーコードを安全に変えるには何が要るか。テストだ。変更の前後を比較できて初めて「何も壊れていない」と言える。
ではテストを足すには何が要るか。コードを変える必要がある。テストしやすいように関数を分割し、依存を注入できるようにし、グローバル状態を取り除く。
ここでジレンマが生まれる。
| やりたいこと | 先に必要な前提 | しかし |
|---|---|---|
| コードを安全に変える | テストが存在しなければならない | テストがない |
| テストを足す | コードをテスト可能に変えなければならない | その変更が安全か分からない |
| 変更が安全か確認する | テストが存在しなければならない | 振り出しに戻る |
これがレガシー作業の本質的な難しさだ。鶏と卵。テストなしでは安全に変えられず、変えなければテストを足せない。
抜け道は二つある。
一つ目、最小限の危険な変更を受け入れる。 テストを差し込むために必要な変更を、できるだけ小さく、機械的で、戻しやすいものにする。「メソッドの抽出」「変数を引数に」「コンストラクタで依存を受け取る」 — これらは IDE が自動でやってくれて、振る舞いをほぼ変えない。リスクはゼロではないが、十分に小さい。
二つ目、変更なしでテストできる地点を見つける。 これが seam だ。コードを書き直さずに、すでに存在する境界を通じてテストを差し込む。第3章と第4章のテーマだ。
核心は順序だ。まず現在の振る舞いを固定する安全網を張る(characterization test)。その次に、その網の下でコードをリファクタリングする。網が赤になったら止まる。無謀な「とりあえず直してみる」ではなく、一歩ごとに足元を確認することだ。
レガシー作業は登攀に似ている。片手が確保された後で初めてもう片方の手を離す。決して両手を同時には離さない。
第2章 · Characterization Test — 現在の振る舞いを固定せよ
characterization test はレガシー作業の出発点だ。名前が核心を含んでいる。このテストはコードが何をすべきかを検証しない。コードが現在何をしているかを固定する。
違いが重要だ。普通のテストは仕様から始まる — 「この関数は X を返すべきだ」。characterization test は、仕様がない、あるいは仕様が信用できない状況から始まる。仕様の代わりに、現在の振る舞いそのものを真実として受け入れる。
なぜ「現在の振る舞い」なのか、bug でも
レガシーコードの現在の振る舞いには bug が混じっている。それでも characterization test はその bug まで固定する。奇妙に聞こえるが理由がある。
あなたは今、このコードの振る舞いを理解しようとしているのであって、直そうとしているのではない。どの振る舞いが意図された機能で、どの振る舞いが bug なのか、今は区別できない。誰かがその「bug」に依存しているかもしれない(Hyrum の法則)。だからとりあえず全部固定する。その後、どの振る舞いが bug か確認できたら、そのテストを意図的に変える — それは明確な決定であって、知らずに壊したものではない。
普通のテスト: 仕様 -> テストを書く -> コードを通す
characterization: コードを実行 -> 出力を観察 -> その出力を期待値として固定
Characterization test を書く手順
- コードをテストハーネスに載せる。 関数を呼べる最小限の環境を作る。入力を与え出力を受け取れればよい。
- 明らかに間違った期待値で assertion を書く。
assertEquals("THIS_IS_WRONG", result)のように。 - テストを走らせて失敗メッセージを見る。 テストランナーが「期待: THIS_IS_WRONG、実際: 42」と教えてくれる。この 42 がコードの現在の振る舞いだ。
- 実際の出力を期待値として固定する。
assertEquals(42, result)。これでテストが通る。 - 繰り返す。 様々な入力 — 正常値、境界値、空、0、負、null — で振る舞いを固定する。
// before — 何をするかの仕様がないレガシー関数
function computeDiscount(order) {
let d = 0
if (order.total > 100) d = order.total * 0.1
if (order.coupon === 'VIP') d += 5
if (order.items.length > 10) d = Math.min(d, 20)
return Math.round(d * 100) / 100
}
// after — 現在の振る舞いを固定した characterization test
test('characterizes computeDiscount', () => {
// 仕様ではなく「現在こう動く」の記録
expect(computeDiscount({ total: 50, coupon: null, items: [] })).toBe(0)
expect(computeDiscount({ total: 150, coupon: null, items: [] })).toBe(15)
expect(computeDiscount({ total: 150, coupon: 'VIP', items: [] })).toBe(20)
// 以下は bug に見える — とりあえず固定し、後で意図的に変える
expect(computeDiscount({ total: 50, coupon: 'VIP', items: [] })).toBe(5)
})
これでこの安全網の下で computeDiscount をリファクタリングできる。変数名を直し、関数を分割し、条件を整理する。テストが緑のままなら振る舞いは変わっていない。赤になったら — 何かが変わったのであり、意図したものでなければ戻す。
Golden Master — 出力が巨大なとき
関数が単純な値ではなく巨大な出力(HTML ページ、JSON 文書、ログファイル、画像)を生むなら、出力全体をファイルに保存する。これが golden master、あるいは snapshot test だ。入力 100 個を入れて出力 100 個をファイルに固定する。リファクタリング後にもう一度 100 個を生成し、ファイルと diff する。一文字でも違えばテストが捕まえる。
golden master の強みは、振る舞いを一行も理解していないコードにも安全網を張れることだ。弱みは、diff が出たとき「この変更が意図されたものか」を人が判断しなければならないことだ。
第3章 · Seam を見つける — 書き直さずにテストを差し込む場所
seam は Feathers の最も重要な概念だ。seam とは、その場所でコードを編集せずに振る舞いを変えられる地点だ。
レガシーコードがテストしにくい本当の理由は、ロジックが複雑だからではない。依存のせいだ。関数の真ん中でデータベースに直接接続し、現在時刻を直接読み、ネットワークを直接呼び、グローバル singleton を直接触る。テストでこれを全部本物で立ち上げることはできない。
seam は、その依存をテスト時点で偽物に差し替えられる通路だ。seam があれば、関数本体を触らずに、その通路から偽物を押し込んでテストできる。
Seam の種類
| seam の種類 | 差し替える方法 | 例 |
|---|---|---|
| object seam | インタフェース/クラスを実装した偽オブジェクトを注入 | コンストラクタや setter で依存注入 |
| parameter seam | 関数の引数として依存を受け取らせる | now() の代わりに clock 引数 |
| function/module seam | import や関数参照をテストで置き換える | module モック、関数ポインタ |
| subclass seam | 危険なメソッドを override した subclass でテスト | テスト専用 subclass |
| build seam | build/link 時点で別の実装を繋ぐ | テストビルドで別ファイルを link |
最も多い手術 — 依存を引数に引き出す
レガシーコードで最も頻繁にやる seam 作業は「関数の中に埋め込まれた依存を引数に引き出すこと」だ。これは振る舞いを変えない機械的な変更で、ほとんどの IDE が自動でやってくれる。
// before — 時間依存が関数の中に埋め込まれている。テスト不可能。
function isSubscriptionExpired(subscription) {
const now = Date.now() // 隠れた依存
return subscription.expiresAt < now
}
// after — now を引数に引き出した。これが parameter seam。
function isSubscriptionExpired(subscription, now = Date.now()) {
return subscription.expiresAt < now
}
// これでテストが時間を制御できる — 関数本体はそのまま
test('expired when expiresAt is in the past', () => {
const sub = { expiresAt: 1000 }
expect(isSubscriptionExpired(sub, 2000)).toBe(true) // now=2000 を注入
expect(isSubscriptionExpired(sub, 500)).toBe(false)
})
既存の呼び出し側は一文字も変わらない — now にデフォルト値があるからだ。それでもテストは今、時間を自由に制御する。これが seam の力だ。呼び出し地点はそのまま、テスト地点だけが開く。
レガシーコードをテストできないのは、ほぼ常に依存の問題だ。seam を見つける作業は、つまり「どの依存を、どうやって偽物に変えるか」を見つける作業だ。
第4章 · ボーイスカウト規則 — 段階的な改善
レガシーコード全体を一度にきれいにすることはできない。その時間もないし、そうすべきビジネス上の理由も普通はない。だから段階的な改善の原則が要る。
ボーイスカウト規則: 来たときよりキャンプ場を少しきれいにして去れ。 コードに適用すると — あるファイルを触る理由ができたら、その作業をしながらその周辺を少しよくして出る。
「少し」が核心だ。bug を直しに入ったついでにモジュール全体をリファクタリングしてはいけない。その変更が大きくなると、レビューが難しく、bug 修正とリファクタリングが一つのコミットに混ざり、何かが壊れたとき原因を切り分けにくい。
| 範囲 | 適切な程度 | 過剰なもの |
|---|---|---|
| 名前 | 触った関数の曖昧な変数名を1-2個整理 | ファイル全体の変数名を一括変更 |
| 構造 | たった今触った関数から一塊を抽出 | クラス階層全体を再設計 |
| テスト | 今回直した bug の characterization test を追加 | モジュール全体のテストカバレッジ作業 |
| 死んだコード | 明らかに呼ばれていない関数を一つ削除 | 「使わなさそう」なコードを大量削除 |
核心の規則は二つ。一つ目、bug 修正コミットとリファクタリングコミットを分ける。 レビュアーが「これは振る舞いを変える変更、これは振る舞いを変えない変更」を別々に見られなければならない。二つ目、リファクタリングは安全網の下でのみやる。 整理したいそのコードに characterization test がなければ、まずテストから張る。
段階的な改善の累積効果は大きい。一度に 5 パーセントずつ、人がよく触る場所がよくなる。よく触る場所がよくなるのが重要だ — 誰も触らないコードはきれいである必要があまりない。変更が頻繁に起きる場所、そこが投資する価値のある場所だ。
第5章 · Sprout Method — 新しいコードをきれいな場所で始める
機能を追加しなければならないが、その場所が 200 行のテスト不可能な関数の真ん中だったらどうするか。
悪い答え: その 200 行の真ん中に新しいロジックをまた差し込む。関数は 220 行になり、もっとテスト不可能になる。
良い答え: sprout method。 新しいロジックを既存の関数の中に書かない。新しいメソッドとして別に作り — その新しいメソッドはテストと一緒にきれいに書く — 既存の関数ではその新しいメソッドを呼ぶだけにする。
// before — 注文処理関数。長く、テストしにくい。
function processOrder(order) {
// ... 150 行の検証、在庫処理、決済 ...
// ここに「プレミアム顧客ポイント付与」機能を追加する必要がある
// 悪い選択: この場所に 20 行をさらに差し込む
// ... 残りの 50 行 ...
}
// after — 新しいロジックは sprout method へ。きれいに、テストと一緒に。
function calculateLoyaltyPoints(order) { // sprout: 新しいメソッド、テスト可能
if (!order.customer.isPremium) return 0
const base = Math.floor(order.total / 10)
return order.hasPromoCode ? base * 2 : base
}
function processOrder(order) {
// ... 150 行はそのまま — 触らない ...
const points = calculateLoyaltyPoints(order) // 既存の関数では呼ぶだけ
order.customer.points += points
// ... 残りの 50 行もそのまま ...
}
// sprout method は最初からテストがある
test('premium customer with promo code earns double points', () => {
const order = { total: 100, hasPromoCode: true, customer: { isPremium: true } }
expect(calculateLoyaltyPoints(order)).toBe(20)
})
核心の利益は三つだ。一つ目、新しいコードは 100 パーセントテストされる — きれいな場所で新しく書いたからだ。二つ目、既存の 200 行はほとんど触らない — sprout method を呼ぶ一行だけ追加した。リスクがその一行に隔離される。三つ目、レガシー関数が少しずつ縮む — 新しい機能が外に積まれ、関数の中には積まれない。
sprout method は「レガシーコードを今すぐ整理することはできないが、少なくとももっと悪くはしない」という妥協だ。そしてその妥協が、時間が経つとレガシー関数を自然に小さくしていく。
第6章 · Sprout Class — Sprout Method でも足りないとき
sprout method は新しいロジックがメソッド一つに収まるときに使う。だが新しい機能がそれより大きいなら — 状態があり、複数の振る舞いが絡み、自前の協力オブジェクトが必要なら — メソッド一つでは足りない。
このときは sprout class だ。新しい責務を丸ごと新しいクラスにする。レガシークラスはその新しいクラスをインスタンス化して委譲するだけだ。
sprout class を使うもう一つの強い理由: レガシークラス自体がテストハーネスに載らないとき。 レガシークラスのコンストラクタがデータベースに接続したり、巨大な依存グラフを引き連れてきたりすると、その中に sprout method を作ってもテストしにくい。新しいクラスはその泥沼の外にあるので、自由にテストできる。
sprout method vs sprout class — いつどちらを
sprout method を使う:
- 新しいロジックがメソッド一つにきれいに収まる
- レガシークラスを(苦労しても)テストハーネスに載せられる
- 新しいロジックがレガシークラスの状態をあまり使わない
sprout class を使う:
- 新しい責務が状態 + 複数のメソッドで成る
- レガシークラスをテストハーネスにどうしても載せられない
- 新しい機能を独立してテストし再利用したい
- 新しい責務がレガシークラスと概念的に分離される
sprout class のリスクは、システムにクラスが一つ増えることだ。間違って使うと、小さな責務ごとにクラスが爆発する。だから基準は「この責務は本当に独立した概念か」だ。独立した概念なら、sprout class はむしろ設計を改善する — 巨大なレガシークラスから一つの責務を剥がして名前を付けたのだから。
sprout method と sprout class は同じ哲学の二つのサイズだ。新しいコードをレガシーの泥沼の中に建てるな。その隣のきれいな土地に建てて、橋だけ架けろ。
第7章 · Wrap Method — 既存の振る舞いに触れず行動を足す
sprout は「完全に新しいロジック」を追加するときに使う。だが別の状況がある。既存の振る舞いが起きるその瞬間に、何かをもっとやりたいときだ。例えば「決済のたびに監査ログを残したい」 — 決済ロジック自体は変えたくない。
このときは wrap method。 既存のメソッドの名前を変えて脇によけ、元の名前で新しいメソッドを作る。新しいメソッドは古いメソッドを呼び、その前か後に新しい行動を足す。
// before — 決済ロジック。呼び出し側があちこちにある。本体は触りたくない。
class PaymentService {
processPayment(order) {
const result = this.gateway.charge(order.total, order.card)
order.status = result.success ? 'paid' : 'failed'
return result
}
}
// after — wrap method。元のメソッドは名前を変えるだけで脇によけ、
// 元の名前で「包む」メソッドを作る。
class PaymentService {
// 元の本体 — 一文字も変えていない。名前を private にしただけ。
_processPaymentCore(order) {
const result = this.gateway.charge(order.total, order.card)
order.status = result.success ? 'paid' : 'failed'
return result
}
// 新しいメソッドが元の名前を占める — 呼び出し側は何も知らない
processPayment(order) {
this.auditLog.record('payment_attempt', order.id) // 足した行動(前)
const result = this._processPaymentCore(order) // 元の振る舞いをそのまま呼ぶ
this.auditLog.record('payment_result', order.id, result.success) // 足した行動(後)
return result
}
}
wrap method の核心は、元のメソッド本体を一文字も触らないことだ。_processPaymentCore は昔のコードそのままだ — だから古い振る舞いが変わるリスクがない。新しい行動は全部包む層にあり、その包む層は別にテストできる。
近い親戚に wrap class — decorator パターン — がある。メソッド一つではなくインタフェース全体を包みたいとき、同じインタフェースを実装しつつ中に元のオブジェクトを抱えるクラスを作る。sprout と wrap を合わせれば、レガシーコードをほぼ触らずに機能を足し行動を変えられる道具一式が完成する。
| 技法 | いつ | レガシーコードを |
|---|---|---|
| sprout method | 新しいロジック追加、メソッドサイズ | 呼び出し一行だけ追加 |
| sprout class | 新しい責務追加、クラスサイズ | 委譲一行だけ追加 |
| wrap method | 既存の振る舞いの瞬間に行動を足す | 名前だけ変更、本体保存 |
| wrap class | インタフェース全体に行動を足す | 全く触らない |
第8章 · Strangler Fig パターン — 新しいシステムを古いシステムの周りに育てる
ここまでは関数とクラスのレベルの技法だった。だがレガシーがシステム全体なら — モノリス一つ、古びたサービス一つを丸ごと置き換えなければならないなら — どうするか。
ビッグバン書き直しの誘惑がここから来る。「全部新しく書いて、ある日一度に差し替えよう。」これはほぼ常に失敗する(第10章で詳しく)。代替が strangler fig パターンだ。
名前は Martin Fowler が熱帯雨林の strangler fig(絞め殺しの無花果)から取った。この木は宿主の木の上で芽を出し、根を下に伸ばしながら宿主をゆっくり包む。数十年後、宿主の木は枯れて消え、無花果は宿主の形そのままで独り立ちする。突然の差し替えではなく、段階的な置き換えだ。
ソフトウェアに適用するとこうなる。
strangler fig — 手順
1. 横取りする層(facade/proxy)を古いシステムの前に立てる。
全トラフィックがこの層を通る。最初は 100 パーセント古いシステムへ流す。
2. 機能を一つ選び、その機能だけを新しいシステムで実装する。
横取りする層がその機能のトラフィックだけを新しいシステムへルーティングする。
3. 検証する。新しい経路は古い経路と同じ結果を出すか。
必要ならしばらく両方に送って結果を比較する(シャドーイング)。
問題が起きたらルーティングを古いシステムへ即座に戻す — ロールバックが一行。
4. 次の機能で繰り返す。古いシステムの責務が一片ずつ新しいシステムへ移っていく。
5. 古いシステムへのトラフィックが 0 になったら — 削除する。
横取りする層もルーティングするものがなくなったら取り除く。
strangler fig の利点をビッグバン書き直しと比べると明確だ。
| 項目 | ビッグバン書き直し | strangler fig |
|---|---|---|
| リスク露出 | リリース日一日に全部 | 機能ごとに少しずつ分散 |
| フィードバック | 全部終わった後で初めて | 最初の機能から即座に |
| ロールバック | 事実上不可能 | ルーティング一行で |
| ビジネス価値 | 終わるまで 0 | 最初の一片から発生 |
| 古い/新しいコードの共存 | しない(だから危険) | する(だから安全) |
| スケジュールが遅れたら | 全部が危険 | 古いシステムが回り続ける |
横取りする層を立てることが、最初で最も重要な手順だ。その層がなければ段階的なルーティングが不可能だ。HTTP サービスならリバースプロキシや API ゲートウェイ、ライブラリなら facade クラス、データベースなら抽象化層がその役割を果たす。一度全トラフィックを一点に通せば、その一点で一片ずつ向きを変えられる。
strangler fig の核心は「古いものと新しいものがしばらく共存する」ことを受け入れる点にある。共存は汚く見えるが、その汚さこそ安全網だ — いつでも古いものへ戻れるからだ。
第9章 · 馴染みのないコードを速く読む — 入り口、コールグラフ、実行、ログ
レガシーコードを変えるにはまず読まなければならない。だが 10 万行のシステムを最初から最後まで読むことはできない。全部読もうとするな。必要な経路だけ追え。
入り口から始める
コードはどこかで始まる — main、HTTP ルートハンドラ、イベントリスナー、cron ジョブ、CLI コマンドパーサ。変えたい振る舞いがユーザーにどう見えるかをまず決め、その振る舞いの入り口を見つける。そこから一層ずつ追っていく。
コールグラフを追う、脇道は無視する
入り口から始めて呼び出しを追うが、今変えたい振る舞いに関係する呼び出しだけを追う。ロギング、メトリクス、設定読み込みのような脇道はとりあえず無視する。IDE の「コール階層の表示」「定義へ移動」「使用箇所の検索」が核心の道具だ。頭の中ではなく紙か画面にコールグラフを描きながら追う。
とりあえず実行してみる
読むだけだと推測が積もる。実行して確認する。 デバッガを入り口に掛けて一段ずつ踏むと、どの分岐が実際に通るか、変数に何が入ってくるかが推測ではなく事実として見える。コードを 5 分睨むより、デバッガで一度踏む方が速いことが多い。
理解のためのログを置く
デバッガを使えない環境なら、コールグラフの核心地点に一時的なログを置く。「ここに到達」「この変数の値はこれ」 — 一度流れを把握するためのログだ。流れを理解したら消す。これはデバッグではなく地図を描くことだ。
Characterization test が学習の道具だ
第2章の characterization test は安全網であると同時に学習の道具でもある。入力を変えながら出力を固定していくと、コードがどの入力にどう反応するかを手で学ぶことになる。「この入力にはこれが出るな。なぜ。」という問いが、コードを読む道しるべになる。
| 状況 | 速く読む道具 |
|---|---|
| 振る舞いがどこで始まるか分からない | 入り口リストから — ルート、main、リスナー |
| 関数がどこへ繋がるか分からない | IDE コール階層、定義へ移動 |
| どの分岐が実際に通るか分からない | デバッガで入り口から一段ずつ |
| 本番でだけ再現する流れ | 核心地点に一時的なログ |
| 入力と出力の関係が分からない | characterization test で入力を変えながら観察 |
第10章 · 書き直し対リファクタリング — 書き直しの罠
レガシーコードに直面したすべてのエンジニアが一度は抱く考え: 「これを直すより新しく書く方が速い。」たまにそれは正しい。だがほとんどの場合それは罠だ。この罠には名前がある — 書き直しの罠(the rewrite trap)。
なぜ書き直しはほぼ常に長くかかるのか
レガシーコードは醜い。だからその中に含まれた価値を過小評価しやすい。だがその醜いコードの隅々には、長年かけて発見された bug 修正とエッジケース処理が埋まっている。「変な if 文」一つ一つが、たいてい誰かが深夜に障害を経験して追加したものだ。新しく書けばその知識が全部消える。そして同じエッジケースを同じ順序でまた発見することになる — 今度は本番環境で。
その上、書き直す間、古いシステムは止まらない。古いシステムに新しい機能が入り bug が直る。新しいシステムは動く標的を追う。追いついたと思えば標的がまた動いている。
書き直し対リファクタリング — 判断基準
| 兆候 | リファクタリングが正しい | 書き直しを検討する価値がある |
|---|---|---|
| コードは動くか | 動きはする、変えるのが怖いだけ | 核心機能が実際に壊れている |
| ドメイン知識 | コードにだけある、文書・人にない | ドメインが単純でよく理解されている |
| 技術スタック | 古いが対応はされている | セキュリティパッチが止まった、人を採れない |
| 段階的な経路 | seam を見つけられる | strangler 層すら立てられない構造 |
| 規模 | 大きい | 小さくて1スプリントで書き直せる |
| 変更頻度 | よく変わる(だから改善価値が高い) | ほとんど変わらない(放っておいてよい) |
核心の洞察は二つ。一つ目、「書き直し」と呼ぶものの安全な形が、まさに strangler fig だ。 新しく書きたいなら、ビッグバンで書くな。古いシステムの周りに段階的に育てろ。そうすればそれは書き直しではなく「段階的な置き換え」であり、罠ではない。
二つ目、書き直したい衝動は、たいていコードを理解していないことから来る。 第9章の方法でコードを十分に読み、第2章の characterization test で振る舞いを固定すれば、「新しく書くべきだ」と言っていたコードが、実は数か所のリファクタリングで十分だったと分かることが多い。書き直しを決める前に、まず理解せよ。
第11章 · AI 時代のレガシーコード — Agent は加速装置であり地雷である
AI コーディング agent は、レガシー作業の両面を同時に変えた。うまく使えば最も退屈な部分を数分に縮め、間違って使えば理解していないコードを自信たっぷりに壊す。
Agent が得意なこと
| 作業 | なぜ agent が強いか |
|---|---|
| characterization test の大量生成 | 入力を多様に作り出力を固定するのは退屈だが機械的 — agent が速い |
| コールグラフの追跡 | 巨大なコードベースで「これがどこから呼ばれるか」を速くなめる |
| seam 候補を見つける | 「この関数の隠れた依存」を見つけ引数に引き出すパターンを知っている |
| 馴染みのないコードの要約 | 10 万行モジュールの入り口と流れを速く説明する |
| 機械的なリファクタリング | メソッド抽出、名前変更のような振る舞い保存の変更を安全にやる |
特に characterization test 生成は agent のキラーユースケースだ。人がやると退屈で数個だけ書いて終わる作業を、agent は数十の入力ケースで速く埋める。安全網が厚くなる。
Agent が危険なこと
問題は、agent が理解していないものを理解したかのように話すことにある。レガシーコードの「変な if 文」は、たいてい重要なエッジケース処理だ。agent はそれを「不要に見えるコード」と判断し自信たっぷりに削除する。その if 文がなぜそこにあるかはコードのどこにも書かれていない — 5 年前の障害振り返りにだけある。
AI agent とレガシーコード — 安全規則
まず安全網から:
- 「コードを変える前に、まず characterization test で現在の振る舞いを固定して」
- agent が作った characterization test を人がレビュー — 本当に現在の振る舞いを捕まえているか
- golden master があれば agent の変更の前後で必ず diff
変更のサイズと種類を制御:
- 「振る舞いを変えるな。振る舞い保存のリファクタリングだけ。」と明示
- 一度に一つ — 「リファクタリングと機能追加を同じ変更に混ぜるな」
- 「変に見えるコード」を削除する前に「なぜそこにあるか」をまず説明させる
理解を強制:
- agent が「このコードは不要だ」と言ったら — git blame、関連 issue を確認
- 大きな構造変更は strangler fig で — agent にビッグバン書き直しをさせるな
- agent の「これはこう動きます」という説明は、検証前は仮説
核心は、第1章の手順が人にも agent にも同じく適用されることだ。まず安全網(characterization test)、その次に小さく振る舞い保存の変更、一歩ごとに検証。agent はこの手順の退屈な段階 — 特にテスト生成とコールグラフ追跡 — を劇的に速くする。だが「この変更が安全か」の最終判断は依然として人がやる。agent の自信は正しさの証拠ではない。
agent はレガシーコードに安全網を張る作業を 10 倍速くしてくれる。だが安全網なしにレガシーコードを変える作業も 10 倍速くする。どちらをやらせるかはあなたが決める。
エピローグ — チェックリストとアンチパターン
レガシーコードを扱う技術の核心は一文だ。変える前に現在の振る舞いを固定せよ。 characterization test で安全網を張り、seam でテストを差し込み、sprout と wrap でリスクを隔離し、strangler fig でシステムを段階的に置き換える。恐れを勇気で乗り越えるのではなく、手順に変えることだ。
レガシーコード変更チェックリスト
- 何を変えるのか一文で言ったか。 — 範囲が明確か、それとも「このモジュールを整理」のように曖昧か。
- 現在の振る舞いを固定したか。 — 触ろうとするコードに characterization test(または golden master)があるか。
- その振る舞いが bug でも固定したか。 — 今は理解が目的だ。bug 修正は後で意図的に。
- seam を見つけたか。 — どの依存を、どの種類の seam で偽物に変えるか。
- テストのための変更が小さく機械的か。 — メソッド抽出、引数追加 — IDE がやれるレベルか。
- 新しいコードをきれいな場所で始めたか。 — sprout method/class で、レガシーの泥沼の外で。
- 既存の振る舞いに行動を足すとき包んだか。 — 元の本体を一文字も触っていないか。
- リファクタリングコミットと振る舞い変更コミットを分けたか。 — レビュアーが二つを別々に見られるか。
- システム置き換えなら strangler か。 — 横取りする層を立てたか、ロールバックが一行か。
- 書き直しを決める前に十分に理解したか。 — 醜さを無価値と取り違えていないか。
- 一歩ごとに安全網が緑か。 — 片手が確保された後でもう片方の手を離したか。
- agent にやらせたなら — 安全網から、振る舞い保存だけ、「なぜそこにあるか」をまず説明させる。
アンチパターン
| アンチパターン | なぜ悪いか | 代わりに |
|---|---|---|
| テストなしで「とりあえず直す」 | 何が壊れたか分からない | 安全網から、characterization test で |
| bug を見つけて即座に「直して」固定 | 意図された振る舞いか知らずに変える | とりあえず固定、bug 修正は別の意図的決定 |
| 200 行の関数の真ん中に新しいロジックを差し込む | 関数がもっとテスト不可能になる | sprout method — 新しいコードはきれいな場所に |
| bug 修正のついでにモジュール全体をリファクタリング | レビュー不可、原因追跡不可 | ボーイスカウト規則 — 「少しだけ」 |
| ビッグバン書き直し | ドメイン知識喪失、ロールバック不可、価値 0 | strangler fig — 段階的な置き換え |
| 「醜いから新しく書こう」 | 醜さと無価値を混同 | まず理解 — たいていリファクタリングで十分 |
| 古いメソッド本体を直接修正して行動を追加 | 古い振る舞いが壊れるリスク | wrap method — 本体保存 |
| 10 万行を最初から最後まで読む | 時間の無駄、脇道で迷う | 入り口から、必要な経路だけ |
| 両手を同時に離す | 壊れたら何のせいか分からない | 片手確保の後でもう片方 — 一歩ごとに検証 |
| agent の「不要に見える」をそのまま信じる | エッジケース処理を削除 | git blame・issue を確認、理由からまず説明 |
次回予告
次回は **「テストダブルを正しく使う — モック、スタブ、フェイク、そして過剰モックの罠」**だ。この記事で seam が依存を「偽物に差し替える」と何度も言ったが、その偽物にも種類があり、間違って使うとテストが実装に貼り付いてむしろリファクタリングを阻む。スタブとモックとフェイクの違い、何をモックし何をモックすべきでないか、モックが過剰だと気づく兆候、そして「ロンドン派対古典派」論争を実用的に整理する。安全網を張れるようになったら、次はその安全網が本当に安全か見る方法だ。
현재 단락 (1/266)
新しいプロジェクトは長くは新しくない。半年で、昨日の新しいコードが今日のレガシーになる。1年で、誰も中身を正確に知らないモジュールが一つできる。3年で、「あれは触らないでください」という言葉が会議で出...