社内Wikiの「困ったときの外部相談先リスト」を見つけたのは、年1回のシステム棚卸しの前日だった。
ページの作成日は3年前。作成者は前々任者の山田さん。退職時に引き継ぎ資料をろくに残さなかった人だが、このメモだけは妙に丁寧だった。
レガシー・コード・インベスティゲーション(LCI) コードの設計問題を専門に調べてくれる。連絡先は以下。 ※変わった人なので覚悟すること
私は長谷川マリ、38歳。社内基幹システムの受発注モジュールを7年間保守している。チームリーダーという肩書だが、実質的にこのモジュールを触れるのは私だけだ。前任者の佐々木さんは2年前に辞め、その前の山田さんは5年前に辞めた。
コードベースの半分以上は、佐々木さんか山田さんが書いたものだ。そして、その半分以上が「触れるな危険」状態になっている。コメントアウトされた旧ロジック。到達しない分岐。使われていない属性。何のためにあるのかわからないメソッド。
毎年の棚卸し会議で「不要コードの整理」が議題に上がる。毎年「怖くて消せない」で先送りされる。私が先送りしている。
消して壊れたら、直せるのは私しかいない。
LCIにメールを送ったのは先週のことだ。翌日の返信は一行だった。「現場を拝見しなければ判断できません。お伺いします」。
旧社屋の発掘現場
棚卸し作業のために使っている部屋は、旧社屋のサーバールーム隣の作業スペースだった。
フロアの半分は使われなくなったサーバーラックが占めている。残りの半分には段ボール箱が積まれている。紙に印刷されたコードレビュー記録、10年前のリリースノート、誰かが持ち込んだまま放置されたディスプレイ。物理的な地層だ。
私のデスクの上には、モニター1台とノートPC。それからPerl::Criticの出力を印刷した紙の束。未使用のプライベートサブルーチンを警告する ProhibitUnusedPrivateSubroutines が78件のヒットを返していた。78件。1つ1つ確認する気力は、正直なところ、ない。
約束の時間の5分前に、廊下の向こうから足音が聞こえた。
ドアを開けて入ってきた男は、サーバールームの景色を見て立ち止まった。年齢不詳。スーツの上にコートを羽織っている。出張先なのに現場検証に来た刑事のような格好だ。
ロックさんは部屋に入らず、まずサーバーラックの間を歩いた。鼻を近づけるようにして、ラックの隙間を覗き込む。
「……長谷川さん」
「はい」
「この部屋はいつからこの状態ですか」
「会社が新社屋に移ったのが4年前です。それ以来、ここは倉庫代わりで……」
ロックさんは段ボール箱を1つ開けた。中には紙に印刷されたコードが入っている。ページの右上に日付が手書きされている。2016年。
「いい地層だ」
独り言のように言った。箱の中身をぱらぱらとめくって、そっと戻す。
「では、デジタルの地層も見せていただこう。……ワトソン君」
「長谷川です」
思わず苦笑した。山田さんの「変わった人」という注意書きの意味がわかった。
証拠品の分類
私はノートPCの画面をロックさんに向けた。受発注モジュールのメインクラス OrderProcessor を開いている。
「これが7年間保守してきたクラスです。私が書いた部分もありますが、半分以上は佐々木さんか山田さんのコードです。Perl::Criticを通すと78件の警告が出ます。どれが本当に不要で、どれが必要なのか、判断がつかなくて」
ロックさんは画面を覗き込んだ。スクロールしなかった。最初の画面に映ったコードだけで、すでに何かを見つけたようだった。
| |
「legacy_tax_rate」
ロックさんはその属性名を声に出した。
「これは何のための属性ですか」
「消費税が8%だった頃の税率です。佐々木さんが10%対応をしたときに、念のため残したと聞いています」
「念のため」
「はい。……使われてはいません」
ロックさんは頷いた。まだ判断は下さない。スクロールを示唆して、私は画面を進めた。
| |
「ここにも層がある」ロックさんはモニターに指を近づけた。触れはしない。「if (0) は山田さんのものですか」
「おそらくは。障害調査のときにデバッグ出力を仕込んで、戻すときに if (0) で無効化したんだと思います。消すのが怖かったのか、面倒だったのか」
「コメントアウトされた旧税率の計算は」
「佐々木さんです。消費税改定のときに新しいロジックを書いて、古い方はコメントアウトしました。コミットメッセージには『消費税8%→10%対応』とだけ」
ロックさんは立ち上がり、部屋を一周した。段ボール箱の列を見渡してから、私の方を向いた。
「長谷川さん。この部屋の段ボール箱と、あなたの OrderProcessor は同じ病気にかかっている」
「……何の病気ですか」
「溶岩流だ。Lava Flow。熱いうちは流動的だったコードが、冷えて固まり、誰も動かせなくなった。触ると壊れるかもしれない。だから触らない。触らないから、その上にまた溶岩が流れる。層が重なり、やがて退路が埋まる」
正確な比喩だった。
「さらに続きを見せてもらえますか」
私はスクロールした。
| |
「calculate_total の around 修飾子が残っています。元のメソッドは compute_total にリネームされたのですが、around だけが……」
「亡霊だ」
ロックさんの声が低くなった。
「存在しないはずのメソッドに取り憑いている。いや——正確に言えば、calculate_total 自体がまだ残っているから around は動作する。だがどこからも呼ばれていない。死体と、その死体を見張る亡霊。二重のデッドコードだ」
「消していいですか」
「待ちたまえ。まだ全容が見えていない」
えん罪を暴く
ロックさんは Perl::Critic の出力を印刷した紙の束を手に取った。
「78件のうち、本当のデッドコードはいくつだと思いますか」
「わかりません。だから困っているんです」
「静的解析には限界がある。特にMooを使ったコードでは」
ロックさんは紙の束をめくった。指が止まったのは、_build_discount_table の警告だった。
| |
「これは」
「Perl::Critic が未使用メソッドだと警告しています。でも——」
「見せたまえ」
私は該当箇所を画面に出した。
| |
ロックさんは腕を組んだ。
「長谷川さん。_build_discount_table はデッドコードですか」
「いえ、これは discount_table の lazy ビルダーです。discount_table に最初にアクセスしたときに自動的に呼ばれます」
「その通りだ。このメソッドは、ソースコード上のどこからも直接呼ばれていない。だが Moo のメタプログラミングが内部で呼び出す。Perl::Critic はその関係を追えない」
「つまり、これはえん罪ですか」
「えん罪だ。容疑者名簿にあるからといって全員が犯人ではない。このメソッドを消せば、discount_table にアクセスした瞬間にシステムが壊れる」
「Perl::Critic でえん罪を防ぐ方法はありますか」
「Perl::Critic の設定で _build_ プレフィックスを除外するか、skip_when_using で Moo を指定すればいい。道具は便利だが、道具の判断を鵜呑みにしてはいけない」
ロックさんは紙の束をデスクに置いた。
「では、えん罪を除いた上で、本物の容疑者を絞り込もう。もっと信頼できる証拠が必要だ」
鮮やかな発掘
「Devel::Cover を使いたまえ」
ロックさんが指示したのは、Perl のテストカバレッジ分析ツールだった。
「テストスイートを実行して、各サブルーチンがテスト中に何回呼ばれたかを測定する。呼ばれた回数が0のサブルーチンは、デッドコードの有力な候補だ」
「0%だからといって、テストが足りないだけかもしれませんよね」
「正しい警戒だ」
ロックさんが頷いた。私の慎重さを否定しなかった。この人は、もっと遠慮なく切り捨てるタイプだと思っていた。
「だからこそ2つの問いを立てる。1つ目:このメソッドを呼んでいるコードは、ソースツリーのどこかにあるか。2つ目:このメソッドを消したとき、何が壊れるか。どちらの問いにも答えが出なければ、容疑者名簿からは外せない」
Devel::Cover の結果が出た。0% カバレッジのサブルーチンが4つ。
calculate_total— 0%_validate_legacy_format— 0%_notify_warehouse— 0%_build_discount_table— 0%(ただしdiscount_table経由で間接利用)
4つ目は先ほどの「えん罪」だ。ロックさんが _build_discount_table に赤ペンでバツを入れた。「これは除外。残り3つを1つずつ処理する」
「1つずつ?全部まとめて1コミットで消したほうが楽ですが」
「証拠品は一つずつ鑑定し、一つずつ処理する。一度に焼却すれば、無実の品まで灰になる。もし何かが壊れたとき、どの削除が原因か特定できなくなるだろう」
第一の除去: コメントアウトされた旧税率計算
ロックさんは git log を叩いた。
「コミットメッセージ——『消費税8%→10%対応』。佐々木さん、2024年3月。目的は明確だ。旧税率のロジックはもう不要であり、この情報はバージョン管理に残っている」
「コメントアウトのまま残しておく理由は?」
「ない。コメントアウトはバージョン管理を信頼していない証拠だ。git が履歴を保持している限り、コメントアウトは不要なノイズでしかない。消したまえ」
私は旧税率のコメントを4行削除した。テストを実行。全パス。
第二の除去: if (0) デバッグブロック
「if (0) で囲まれたデバッグ出力。grep でこのブロックを参照しているコードがソースツリーにあるか確認しよう」
結果はゼロ件だった。
「消して、テストを実行」
全パス。
「……あっさりしていますね」
「デッドコードとはそういうものだ。消す前は恐ろしく見えるが、消してみると何も起きない。起きないことを確認したから、安心して消せたのだ」
第三の除去: calculate_total と亡霊の around
「calculate_total を呼んでいるコードは?」
grep の結果。呼び出し元は around 修飾子の内部だけだ。外部からの呼び出しはない。
「修飾子の中からしか呼ばれていない。そして修飾子自体がどこからも呼ばれていない。共依存の死体だ。両方消したまえ」
私はメソッドと around を削除した。テスト実行。全パス。
第四の除去: 未使用属性と未使用メソッド
legacy_tax_rate、_validate_legacy_format、_notify_warehouse。いずれも grep で参照元なし。テスト結果はすべてパス。
最後の _notify_warehouse を消すとき、少しだけ手が止まった。
「ロックさん。このメソッドにはコメントがあります。『TODO: 倉庫通知。佐々木さんが実装予定だった』。……佐々木さんはもういません」
「実装予定だったものが、実装されないまま2年経った。それは予定ではなく、遺言だ。そして遺言は墓碑銘として git のコミットメッセージに刻めばいい」
私は削除した。
発掘を終えて
リファクタリング後の OrderProcessor は、こうなった。
| |
テストは全パス。計算結果はリファクタリング前と完全に一致する。
classDiagram
class OrderProcessor_Before {
+order_id
+items
+customer
+legacy_tax_rate ⚠️ 未使用
+discount_table (lazy)
+_build_discount_table() ✓ lazy builder
+calculate_total() ⚠️ 未使用
+around calculate_total ⚠️ 亡霊
+compute_total()
+_apply_discount()
+_validate_legacy_format() ⚠️ 未使用
+_notify_warehouse() ⚠️ 未使用
}
class OrderProcessor_After {
+order_id
+items
+customer
+discount_table (lazy)
+_build_discount_table() ✓
+compute_total()
+_apply_discount()
}
OrderProcessor_Before --> OrderProcessor_After : Dead Code除去
note for OrderProcessor_Before "7つの定義のうち4つがデッドコード\nコメントアウト2箇所 + if(0)ブロック"
note for OrderProcessor_After "3つの定義に整理\n_build_discount_tableはえん罪"
画面に並ぶ緑色のテスト結果を見ながら、不思議な気分だった。
半分近い行が消えている。消えていないのは、必要なものだけだ。こんなにすっきりするなら、なぜ3年も先送りしたのだろう。
——いや、理由はわかっている。証拠がなかったからだ。消していい根拠がなかった。
ロックさんが隣で立ち上がった。
「ワトソン君」
呼ばれて顔を上げた。「ワトソン君」に訂正を入れるのを忘れていた。いつから忘れていたのか、もう覚えていない。
「1つ聞きたい。この作業を3年間先送りした理由は何だ」
「……壊れるのが怖かったんです。前任者が何を考えてこのコードを残したのかわからない。わからないものを消すのは無責任だと」
「それは無責任ではない。正しい警戒だ」
意外な言葉だった。この人は、もっと傲慢に「消せばよかったのだ」と言い切ると思っていた。
「ただし」ロックさんは続けた。「警戒を行動に変えるには道具がいる。テスト、カバレッジ、バージョン管理——3つの道具は、あなたがいま自分の手で使った。怖いから動けなかったのではない。道具を持たなかったから動けなかったのだ」
ロックさんは帰り支度を始めた。コートの襟を正し、鞄を肩にかける。
その目が、旧サーバールームに積まれた段ボール箱の山に向いた。
「報酬の話をしよう」
「いくらですか」
「金は要らない。あの箱の中に、O’Reilly の『Programming Perl』の初版があった。あの1冊をいただこう」
「あれは廃棄予定の——」
言いかけて、やめた。
廃棄予定のものに価値を見出す人だ。コードであれ書籍であれ。ただし、価値があるものと、ないものの区別はつけている。消すべきものは消し、残すべきものは持ち帰る。
「……どうぞ」
ロックさんは段ボール箱から古い技術書を1冊取り出した。表紙のラクダが色あせている。
「いいものだ。溶岩に埋もれた化石は、地上に出してこそ価値がある」
本を抱えたまま、ロックさんは旧社屋の廊下に消えていった。
私はPCに向き直った。Perl::Criticの出力を印刷した紙の束を、ゴミ箱に入れた。
残っている警告は、_build_ の設定を直せば消える。
探偵の調査報告書
| 容疑(アンチパターン) | 真実(パターン) | 証拠(効果) |
|---|---|---|
| コメントアウトされた旧実装の放置 | バージョン管理への信頼 | コメントノイズの除去、可読性向上 |
if (0) による到達不能コード | 不要コードの削除 | 条件分岐の単純化 |
リネーム後のメソッド残骸と around 亡霊 | 未使用メソッドの除去 | メソッド数の削減、構造の明確化 |
未使用属性(legacy_tax_rate) | 不要属性の除去 | クラスインターフェースの整理 |
| 未実装 TODO メソッドの温存 | 不要メソッドの除去 | YAGNI原則の適用 |
_build_* の誤検出 | Perl::Critic 設定の調整 | えん罪の防止、lazy ビルダーの保護 |
推理のステップ
- 静的解析(Perl::Critic)を実行する:
ProhibitUnusedPrivateSubroutinesでデッドコード候補を洗い出す。ただし Moo の_build_*メソッドは誤検出される可能性がある - カバレッジ分析(Devel::Cover)で裏を取る: テストスイートを実行し、0% カバレッジのサブルーチンを特定する。静的解析とカバレッジの両方で「未使用」と判定されたものが有力候補
- 呼び出し元を
grepで確認する: ソースツリー全体を検索し、そのメソッドを呼んでいるコードが本当にないか確認する git logで意図を調査する: コメントアウトや unused コードがなぜ残されたか、コミットメッセージから経緯を読み取る(Chesterton’s Fence)- 1件ずつ削除し、テストを実行する: 削除のたびにテストスイートを走らせ、問題がないことを確認する。まとめて消さない
- 削除理由をコミットメッセージに記録する: なぜ削除したかの判断根拠を git の履歴に残す
ロックより
溶岩が流れるのは、地面が熱いからだ。開発の熱気がコードに注がれ、冷めたあとに固まったものが残る。それ自体は自然なことだ。問題は、固まった溶岩を誰も片づけないまま、その上に次の溶岩を流すことにある。
消すことは、壊すことではない。消す前に証拠を集め、1つずつ鑑定し、1つずつ処理すれば、壊れるリスクはコントロールできる。テストが防壁になり、バージョン管理が安全網になる。「いつか使うかもしれない」は、バージョン管理がすでに引き受けている。
溶岩に埋もれた化石の中には、確かに価値あるものがある。だが、それは地上に出して初めてわかることだ。
