Featured image of post コード探偵ロックの事件簿【Magic Numbers / Strings】名前のない証拠品たち〜深夜のタイポは二度鳴く〜

コード探偵ロックの事件簿【Magic Numbers / Strings】名前のない証拠品たち〜深夜のタイポは二度鳴く〜

コード中に散乱するマジックナンバーとマジックストリング。タイポで本番障害を引き起こした若手エンジニアが、コード探偵ロックの推理で use constant と Types::Standard::Enum による型安全な定数化を学ぶ。

深夜3時。蛍光灯だけが白々と点いたオフィスで、私はモニターを睨んでいた。

決済サービスのステータス更新が反映されない。ログを追い、DBを叩き、コードを読み返す。2時間かけてようやく見つけた原因は、たった1文字だった。

1
if ($self->status eq 'actve') {  # 'active' のタイポ

i が抜けている。それだけ。

修正してデプロイし、動作確認を終えたとき、始発電車の音が聞こえた。

翌朝、寝不足の頭でQiitaのリファクタリング記事を読みあさっていると、コメント欄に奇妙な書き込みがあった。

「そういう悩みはコード探偵に相談しろ」

検索すると、「レガシー・コード・インベスティゲーション(LCI)」なる事務所のウェブサイトが見つかった。胡散臭い。だけど、他に手がかりもない。

――そういう経緯で、私はいま、雑居ビルのエントランスでスマホの地図と住所を見比べている。

事務所への来客

三階。ドアの前に立つと、くすんだプレートに「レガシー・コード・インベスティゲーション」と書かれていた。ノックしようとした手が止まる。中から、何か硬いものを分解する音がしていた。

意を決してドアを開けると、デスクの上にキーボードの部品が整然と並べられていた。キーキャップ、スプリング、スタビライザー。飲みかけのエナジードリンク缶がいくつか脇に追いやられている。

「失礼します……Qiitaの、ええと、コメントを見て来たんですが」

部品に集中していた男が顔を上げた。年齢不詳。手にはピンセットを持っている。

「ほう。どんな事件だ?」

事件。

「あ、事件というか……決済サービスの保守を引き継いだんですけど、コードに散らばっている数字や文字列の意味がわからなくて。先日はタイポで障害を出してしまって……」

男はピンセットを置いた。

「意味不明な数字。意味不明な文字列。散在。タイポで障害」

独り言のように繰り返してから、こちらを見た。

「――なるほど。名前のない証拠品が散乱している現場だ。見せたまえ、ワトソン君」

「あ、木村です……」

小声で訂正したが、聞こえていない様子だった。

コードの指紋

ノートPCを開き、問題の PaymentProcessor.pm を画面に映した。ロックさんは椅子ごと寄ってきて、コードを上から下まで眺め始めた。

 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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package PaymentProcessor;
use Moo;
use v5.36;

has amount    => (is => 'ro', required => 1);
has currency  => (is => 'ro', required => 1);
has status    => (is => 'rw', default => sub { 'pending' });
has retries   => (is => 'rw', default => sub { 0 });

sub process ($self) {
    return { success => 0, error => 'invalid amount' }
        if $self->amount <= 0;

    return { success => 0, error => 'unsupported currency' }
        unless $self->currency eq 'JPY'
            || $self->currency eq 'USD'
            || $self->currency eq 'EUR';

    my $timeout = 42;

    if ($self->amount > 10000) {
        $self->status('review');
    }
    else {
        $self->status('active');
    }

    return {
        success => 1,
        timeout => $timeout,
        status  => $self->status,
    };
}

sub retry ($self) {
    if ($self->retries >= 7) {
        $self->status('failed');
        return { success => 0, error => 'max retries exceeded' };
    }

    $self->retries($self->retries + 1);
    return $self->process;
}

sub is_complete ($self) {
    return $self->status eq 'complete';
}

sub is_failed ($self) {
    return $self->status eq 'failed';
}

「この 42 は何だ?」

ロックさんが画面の一点を指した。

「タイムアウトの秒数……だと思います」

「思います?」

「前任者が決めた値で……引き継ぎ資料がなくて」

「前任者に確認は――」

「退職しました」

「……ふむ」

ロックさんは画面をスクロールした。

「ではこの 7 は?」

「リトライの上限です。たぶん」

10000 は?」

「高額取引の閾値……のはずです」

「のはずです」と私が言うたびに、ロックさんの口元がわずかに歪んだ。嘲笑ではなかった。どちらかというと、予想通りの証拠を見つけた刑事のような表情だった。

「では文字列はどうだ。'pending''active''review''complete''failed'。これらは?」

「ステータスの値です。5種類あって……あちこちに散らばっています」

「何箇所?」

数えたことがなかった。grep をかけてみると、'pending' だけで4箇所。'active' は5箇所。'failed' が3箇所。

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

「証拠品が現場に散乱しているのに、一つも証拠袋に入っていない。鑑識課が泣くぞ」

何の話だろうと思ったが、言いたいことはわかった気がする。

「でも……動いてはいるんです。触って壊すのが怖くて」

「壊したのは先日の深夜だろう? タイポ一つで」

返す言葉がなかった。

鮮やかなリファクタリング

証拠品に番号札をつける

ロックさんがエディタを開いた。

「まず、名前のない証拠品に番号札をつけよう」

1
2
3
4
5
use constant {
    TIMEOUT_SECONDS       => 42,
    MAX_RETRIES           => 7,
    HIGH_AMOUNT_THRESHOLD => 10000,
};

42TIMEOUT_SECONDS になる。7MAX_RETRIES に。10000HIGH_AMOUNT_THRESHOLD に。これで法廷で証拠能力を持つ」

「でも……」

「何だ?」

「定数にしても、結局は同じ値が定数定義のところにあるだけですよね? 何が変わるんですか」

ロックさんが一瞬こちらを見た。悪くない質問だったらしい。

「名前のない証拠品と、番号札のついた証拠品。2つの違いは何だ?」

「……名前があれば、何の証拠かわかる」

「それだけか?」

考えた。

「変更するとき、番号札を見れば全部の場所がわかる……?」

「そうだ。42 という数字を探すと、無関係な 42 まで引っかかる。だが TIMEOUT_SECONDS を探せば、タイムアウトに関する箇所だけが浮かび上がる。証拠品の管理は、刻印のない品物の山を漁るより、番号簿を繰るほうが確実だ」

なるほど。定数定義を変えるだけで、全箇所に変更が反映される。散在するリテラルをそれぞれ書き換えるよりずっと安全だ。

試しに "timeout is $TIMEOUT_SECONDS" と書こうとしたら、値が展開されなかった。

「あれ、文字列の中に入れられない……?」

use constant はサブルーチンとして定義される。シジル――$ 記号がないから、文字列の中に直接は埋め込めない。連結するか、sprintf を使うことだ」

1
2
3
4
# 文字列に埋め込む場合
my $msg = "timeout is " . TIMEOUT_SECONDS;
# または
my $msg = sprintf "timeout is %d", TIMEOUT_SECONDS;

「些細な不便だが、代わりにPerl がこの定数をインライン展開してくれる。実行速度の観点では損はない」

証拠品の鑑定――偽物を弾く仕組み

「番号札をつけただけでは足りない。次は、証拠品を鑑定して偽物を弾く仕組みが必要だ」

ロックさんが新しいパッケージを書き始めた。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package PaymentStatus;
use v5.36;
use Types::Standard qw(Enum);

use constant {
    PENDING  => 'pending',
    ACTIVE   => 'active',
    REVIEW   => 'review',
    COMPLETE => 'complete',
    FAILED   => 'failed',
};

use constant ALL_STATUSES => [PENDING, ACTIVE, REVIEW, COMPLETE, FAILED];

our $StatusType = Enum[@{ ALL_STATUSES() }];

1;

EnumTypes::Standard に含まれる型制約で、許可される値のリストを定義する。リストにない値を渡すと、即座に例外を投げる」

「即座に?」

「オブジェクト生成のタイミングでだ。タイポが実行時の条件分岐まで潜伏することはない」

言われるまま、Moo の属性に型制約を適用する。

1
2
3
4
5
has status => (
    is      => 'rw',
    isa     => $PaymentStatus::StatusType,
    default => sub { PaymentStatus::PENDING },
);

isa に渡す。MooType::Tiny のオブジェクトをコードリファレンスとして受け取れるから、特別な設定は不要だ」

「はい……」

「試したまえ。先日のタイポを」

恐る恐る、テストに書いてみた。

1
2
3
4
5
6
eval { PaymentProcessor->new(
    amount   => 5000,
    currency => 'JPY',
    status   => 'actve',   # タイポ!
) };
# => Value "actve" did not pass type constraint ...

実行した瞬間、例外が飛んだ。

「これ……タイポした瞬間にエラーが出るんですね」

自分でも声のトーンが変わったのがわかった。深夜3時の2時間が、この1行で消えた可能性があった。

「だが疑問があるだろう」

「あります。Enumで決め打ちしたら、新しいステータスを追加するとき大変じゃないですか?」

「定数定義の一箇所を変えればいい。ALL_STATUSES に追加し、各所で定数名を使う。散在するマジックストリングを全部探して修正するのと、どちらが大変だ?」

「……一箇所のほうが」

「そういうことだ」

通貨にも鑑定を

同じパターンで通貨の型制約も作った。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package Currency;
use v5.36;
use Types::Standard qw(Enum);

use constant {
    JPY => 'JPY',
    USD => 'USD',
    EUR => 'EUR',
};

use constant SUPPORTED => [JPY, USD, EUR];

our $CurrencyType = Enum[@{ SUPPORTED() }];

1;
1
2
3
4
5
has currency => (
    is       => 'ro',
    isa      => $Currency::CurrencyType,
    required => 1,
);

「通貨もこれで守れるんですね」

もう声は小さくなかった。

リファクタリング後の PaymentProcessor 全体はこうなった。

 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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package PaymentProcessor;
use Moo;
use v5.36;
use Types::Standard qw(Int);
use PaymentStatus;
use Currency;

use constant {
    TIMEOUT_SECONDS       => 42,
    MAX_RETRIES           => 7,
    HIGH_AMOUNT_THRESHOLD => 10000,
};

has amount => (
    is       => 'ro',
    isa      => Int,
    required => 1,
);

has currency => (
    is       => 'ro',
    isa      => $Currency::CurrencyType,
    required => 1,
);

has status => (
    is      => 'rw',
    isa     => $PaymentStatus::StatusType,
    default => sub { PaymentStatus::PENDING },
);

has retries => (
    is      => 'rw',
    isa     => Int,
    default => sub { 0 },
);

sub process ($self) {
    return { success => 0, error => 'invalid amount' }
        if $self->amount <= 0;

    if ($self->amount > HIGH_AMOUNT_THRESHOLD) {
        $self->status(PaymentStatus::REVIEW);
    }
    else {
        $self->status(PaymentStatus::ACTIVE);
    }

    return {
        success => 1,
        timeout => TIMEOUT_SECONDS,
        status  => $self->status,
    };
}

sub retry ($self) {
    if ($self->retries >= MAX_RETRIES) {
        $self->status(PaymentStatus::FAILED);
        return { success => 0, error => 'max retries exceeded' };
    }

    $self->retries($self->retries + 1);
    return $self->process;
}

sub is_complete ($self) {
    return $self->status eq PaymentStatus::COMPLETE;
}

sub is_failed ($self) {
    return $self->status eq PaymentStatus::FAILED;
}

事件の終わり

テストを走らせた。全件パス。

1
2
3
4
5
6
7
8
9
ok 1 - After: 定数が正しく定義されている
ok 2 - After: PaymentProcessor定数が正しい
ok 3 - After: 正常な決済処理(通常金額)
...
ok 8 - After: タイポしたステータスはコンストラクタで拒否される
ok 9 - After: タイポした通貨はコンストラクタで拒否される
ok 10 - After: 無効なステータスへのセッターも拒否される
...
1..16

タイポは型エラーとして即座に弾かれ、タイムアウト秒数やリトライ上限は名前で意味が読み取れる。

自分のエディタを開いた。本番のコードベースの PaymentProcessor.pm に手を入れ始めた。ロックさんは何も言わなかった。キーボードの部品に視線を戻していた。

最初に PaymentStatus パッケージを作り、use constant で5つのステータスを定義する。次に Enum で型制約を生成し、has statusisa に渡す。通貨も同様に。数値リテラルに名前をつけて回る。

どの作業も、やること自体は難しくなかった。

「……もう深夜3時を繰り返さなくて済みそうです」

ロックさんはピンセットでキーキャップを拾い上げ、裏側を確かめていた。

「証拠品に名前をつけるのは、捜査の初歩だ。だが初歩だからといって、誰もがやっているわけではない」

こちらに向けた言葉なのか、独り言なのか、判別がつかなかった。

LCIを出るとき、振り返ると、ロックさんはもうキーボードの組み立てに戻っていた。


探偵の調査報告書

容疑(アンチパターン)真実(パターン)証拠(効果)
マジックナンバーの散在(42, 7, 10000use constant による名前付き定数値の意味の自己文書化、変更箇所の一元化
マジックストリングのタイポリスク('actve'Types::Standard::Enum による型制約無効値をコンストラクタ時に即座に検出
型制約なしの属性Moo isa + Type::Tinyタイポ・無効値の構造的排除

推理のステップ

  1. コード中の意味不明なリテラル値(数値・文字列)を洗い出す
  2. use constant で名前付き定数に変換し、値の意味を刻印する
  3. 文字列リテラルの許容値を列挙し、Types::Standard::Enum で型制約を定義する
  4. Moo の isa 属性に型制約を適用し、コンストラクタとセッターで不正値を弾く
  5. テストを実行し、タイポや無効値が即座に検出されることを確認する

ロックより

証拠品には二種類ある。名前のない品物と、番号札のついた品物だ。前者は現場に転がっているだけの残骸であり、後者は事件を解決する手がかりとなる。

コードに埋まった 42'pending' は、名前のない証拠品だ。それ自体は無害に見える。だが散乱した証拠品は、いつか捜査を誤らせる――先日の深夜のように。

名前をつけろ。型で守れ。証拠品管理の基本を怠った現場から、真実は見つからない。

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