Featured image of post コード探偵ロックの事件簿【Dead Code / Lava Flow】固まった溶岩の下の真実〜消せないコードが埋めた退路〜

コード探偵ロックの事件簿【Dead Code / Lava Flow】固まった溶岩の下の真実〜消せないコードが埋めた退路〜

「いつか使う」「消すと壊れるかも」で堆積したデッドコードと溶岩流——Devel::Coverによるカバレッジ分析、Perl::Criticの誤検出回避、段階的除去とバージョン管理への信頼でコードの地層を発掘するコード探偵ロックの推理。

社内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件の警告が出ます。どれが本当に不要で、どれが必要なのか、判断がつかなくて」

ロックさんは画面を覗き込んだ。スクロールしなかった。最初の画面に映ったコードだけで、すでに何かを見つけたようだった。

1
2
3
4
5
6
7
8
9
package OrderProcessor;
use Moo;
use v5.36;

has order_id       => (is => 'ro', required => 1);
has items          => (is => 'ro', required => 1);
has customer       => (is => 'ro', required => 1);
has legacy_tax_rate => (is => 'ro', default => sub { 0.08 });
has discount_table => (is => 'lazy');

legacy_tax_rate

ロックさんはその属性名を声に出した。

「これは何のための属性ですか」

「消費税が8%だった頃の税率です。佐々木さんが10%対応をしたときに、念のため残したと聞いています」

「念のため」

「はい。……使われてはいません」

ロックさんは頷いた。まだ判断は下さない。スクロールを示唆して、私は画面を進めた。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
sub compute_total ($self) {
    my $subtotal = 0;
    for my $item ($self->items->@*) {
        $subtotal += $item->{price} * $item->{qty};
    }

    # 🪦 到達不能: if (0) デバッグブロック
    if (0) {
        warn "DEBUG: subtotal = $subtotal";
        warn "DEBUG: items = " . scalar($self->items->@*);
    }

    # 🪦 コメントアウトされた旧実装(消費税8%時代)
    # my $tax = $subtotal * 0.08;
    # $subtotal += $tax;
    # warn "tax (8%): $tax";   # 山田さんのデバッグ用

    my $tax = $subtotal * 0.10;

    my $discount = $self->_apply_discount($subtotal);

    return $subtotal + $tax - $discount;
}

「ここにも層がある」ロックさんはモニターに指を近づけた。触れはしない。「if (0) は山田さんのものですか」

「おそらくは。障害調査のときにデバッグ出力を仕込んで、戻すときに if (0) で無効化したんだと思います。消すのが怖かったのか、面倒だったのか」

「コメントアウトされた旧税率の計算は」

「佐々木さんです。消費税改定のときに新しいロジックを書いて、古い方はコメントアウトしました。コミットメッセージには『消費税8%→10%対応』とだけ」

ロックさんは立ち上がり、部屋を一周した。段ボール箱の列を見渡してから、私の方を向いた。

「長谷川さん。この部屋の段ボール箱と、あなたの OrderProcessor は同じ病気にかかっている」

「……何の病気ですか」

「溶岩流だ。Lava Flow。熱いうちは流動的だったコードが、冷えて固まり、誰も動かせなくなった。触ると壊れるかもしれない。だから触らない。触らないから、その上にまた溶岩が流れる。層が重なり、やがて退路が埋まる」

正確な比喩だった。

「さらに続きを見せてもらえますか」

私はスクロールした。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 🪦 旧メソッド: 本来の calculate_total(使われていない)
sub calculate_total ($self) {
    my $subtotal = 0;
    for my $item ($self->items->@*) {
        $subtotal += $item->{price} * $item->{qty};
    }
    return $subtotal;
}

# 🪦 亡霊の around: calculate_total を修飾しているが、
#    compute_total にリネーム済みなので実質的に無意味
around calculate_total => sub ($orig, $self, @args) {
    warn "calculate_total called";
    return $orig->($self, @args);
};

calculate_totalaround 修飾子が残っています。元のメソッドは compute_total にリネームされたのですが、around だけが……」

「亡霊だ」

ロックさんの声が低くなった。

「存在しないはずのメソッドに取り憑いている。いや——正確に言えば、calculate_total 自体がまだ残っているから around は動作する。だがどこからも呼ばれていない。死体と、その死体を見張る亡霊。二重のデッドコードだ」

「消していいですか」

「待ちたまえ。まだ全容が見えていない」

えん罪を暴く

ロックさんは Perl::Critic の出力を印刷した紙の束を手に取った。

「78件のうち、本当のデッドコードはいくつだと思いますか」

「わかりません。だから困っているんです」

「静的解析には限界がある。特にMooを使ったコードでは」

ロックさんは紙の束をめくった。指が止まったのは、_build_discount_table の警告だった。

1
ProhibitUnusedPrivateSubroutines: Private subroutine/method '_build_discount_table' declared but not referenced

「これは」

「Perl::Critic が未使用メソッドだと警告しています。でも——」

「見せたまえ」

私は該当箇所を画面に出した。

1
2
3
4
5
6
7
8
has discount_table => (is => 'lazy');

sub _build_discount_table ($self) {
    return {
        gold     => 0.1,
        platinum => 0.2,
    };
}

ロックさんは腕を組んだ。

「長谷川さん。_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 は、こうなった。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package OrderProcessor;
use Moo;
use v5.36;

has order_id       => (is => 'ro', required => 1);
has items          => (is => 'ro', required => 1);
has customer       => (is => 'ro', required => 1);
has discount_table => (is => 'lazy');

sub _build_discount_table ($self) {
    return {
        gold     => 0.1,
        platinum => 0.2,
    };
}

sub compute_total ($self) {
    my $subtotal = 0;
    for my $item ($self->items->@*) {
        $subtotal += $item->{price} * $item->{qty};
    }

    my $tax = $subtotal * 0.10;

    my $discount = $self->_apply_discount($subtotal);

    return $subtotal + $tax - $discount;
}

sub _apply_discount ($self, $subtotal) {
    my $tier = $self->customer->{tier} // 'standard';
    my $rate = $self->discount_table->{$tier} // 0;
    return $subtotal * $rate;
}

1;

テストは全パス。計算結果はリファクタリング前と完全に一致する。

	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 ビルダーの保護

推理のステップ

  1. 静的解析(Perl::Critic)を実行する: ProhibitUnusedPrivateSubroutines でデッドコード候補を洗い出す。ただし Moo の _build_* メソッドは誤検出される可能性がある
  2. カバレッジ分析(Devel::Cover)で裏を取る: テストスイートを実行し、0% カバレッジのサブルーチンを特定する。静的解析とカバレッジの両方で「未使用」と判定されたものが有力候補
  3. 呼び出し元を grep で確認する: ソースツリー全体を検索し、そのメソッドを呼んでいるコードが本当にないか確認する
  4. git log で意図を調査する: コメントアウトや unused コードがなぜ残されたか、コミットメッセージから経緯を読み取る(Chesterton’s Fence)
  5. 1件ずつ削除し、テストを実行する: 削除のたびにテストスイートを走らせ、問題がないことを確認する。まとめて消さない
  6. 削除理由をコミットメッセージに記録する: なぜ削除したかの判断根拠を git の履歴に残す

ロックより

溶岩が流れるのは、地面が熱いからだ。開発の熱気がコードに注がれ、冷めたあとに固まったものが残る。それ自体は自然なことだ。問題は、固まった溶岩を誰も片づけないまま、その上に次の溶岩を流すことにある。

消すことは、壊すことではない。消す前に証拠を集め、1つずつ鑑定し、1つずつ処理すれば、壊れるリスクはコントロールできる。テストが防壁になり、バージョン管理が安全網になる。「いつか使うかもしれない」は、バージョン管理がすでに引き受けている。

溶岩に埋もれた化石の中には、確かに価値あるものがある。だが、それは地上に出して初めてわかることだ。

comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。