深夜3時。蛍光灯だけが白々と点いたオフィスで、私はモニターを睨んでいた。
決済サービスのステータス更新が反映されない。ログを追い、DBを叩き、コードを読み返す。2時間かけてようやく見つけた原因は、たった1文字だった。
| |
i が抜けている。それだけ。
修正してデプロイし、動作確認を終えたとき、始発電車の音が聞こえた。
翌朝、寝不足の頭でQiitaのリファクタリング記事を読みあさっていると、コメント欄に奇妙な書き込みがあった。
「そういう悩みはコード探偵に相談しろ」
検索すると、「レガシー・コード・インベスティゲーション(LCI)」なる事務所のウェブサイトが見つかった。胡散臭い。だけど、他に手がかりもない。
――そういう経緯で、私はいま、雑居ビルのエントランスでスマホの地図と住所を見比べている。
事務所への来客
三階。ドアの前に立つと、くすんだプレートに「レガシー・コード・インベスティゲーション」と書かれていた。ノックしようとした手が止まる。中から、何か硬いものを分解する音がしていた。
意を決してドアを開けると、デスクの上にキーボードの部品が整然と並べられていた。キーキャップ、スプリング、スタビライザー。飲みかけのエナジードリンク缶がいくつか脇に追いやられている。
「失礼します……Qiitaの、ええと、コメントを見て来たんですが」
部品に集中していた男が顔を上げた。年齢不詳。手にはピンセットを持っている。
「ほう。どんな事件だ?」
事件。
「あ、事件というか……決済サービスの保守を引き継いだんですけど、コードに散らばっている数字や文字列の意味がわからなくて。先日はタイポで障害を出してしまって……」
男はピンセットを置いた。
「意味不明な数字。意味不明な文字列。散在。タイポで障害」
独り言のように繰り返してから、こちらを見た。
「――なるほど。名前のない証拠品が散乱している現場だ。見せたまえ、ワトソン君」
「あ、木村です……」
小声で訂正したが、聞こえていない様子だった。
コードの指紋
ノートPCを開き、問題の PaymentProcessor.pm を画面に映した。ロックさんは椅子ごと寄ってきて、コードを上から下まで眺め始めた。
| |
「この 42 は何だ?」
ロックさんが画面の一点を指した。
「タイムアウトの秒数……だと思います」
「思います?」
「前任者が決めた値で……引き継ぎ資料がなくて」
「前任者に確認は――」
「退職しました」
「……ふむ」
ロックさんは画面をスクロールした。
「ではこの 7 は?」
「リトライの上限です。たぶん」
「10000 は?」
「高額取引の閾値……のはずです」
「のはずです」と私が言うたびに、ロックさんの口元がわずかに歪んだ。嘲笑ではなかった。どちらかというと、予想通りの証拠を見つけた刑事のような表情だった。
「では文字列はどうだ。'pending'、'active'、'review'、'complete'、'failed'。これらは?」
「ステータスの値です。5種類あって……あちこちに散らばっています」
「何箇所?」
数えたことがなかった。grep をかけてみると、'pending' だけで4箇所。'active' は5箇所。'failed' が3箇所。
ロックさんは腕を組んだ。
「証拠品が現場に散乱しているのに、一つも証拠袋に入っていない。鑑識課が泣くぞ」
何の話だろうと思ったが、言いたいことはわかった気がする。
「でも……動いてはいるんです。触って壊すのが怖くて」
「壊したのは先日の深夜だろう? タイポ一つで」
返す言葉がなかった。
鮮やかなリファクタリング
証拠品に番号札をつける
ロックさんがエディタを開いた。
「まず、名前のない証拠品に番号札をつけよう」
| |
「42 が TIMEOUT_SECONDS になる。7 が MAX_RETRIES に。10000 が HIGH_AMOUNT_THRESHOLD に。これで法廷で証拠能力を持つ」
「でも……」
「何だ?」
「定数にしても、結局は同じ値が定数定義のところにあるだけですよね? 何が変わるんですか」
ロックさんが一瞬こちらを見た。悪くない質問だったらしい。
「名前のない証拠品と、番号札のついた証拠品。2つの違いは何だ?」
「……名前があれば、何の証拠かわかる」
「それだけか?」
考えた。
「変更するとき、番号札を見れば全部の場所がわかる……?」
「そうだ。42 という数字を探すと、無関係な 42 まで引っかかる。だが TIMEOUT_SECONDS を探せば、タイムアウトに関する箇所だけが浮かび上がる。証拠品の管理は、刻印のない品物の山を漁るより、番号簿を繰るほうが確実だ」
なるほど。定数定義を変えるだけで、全箇所に変更が反映される。散在するリテラルをそれぞれ書き換えるよりずっと安全だ。
試しに "timeout is $TIMEOUT_SECONDS" と書こうとしたら、値が展開されなかった。
「あれ、文字列の中に入れられない……?」
「use constant はサブルーチンとして定義される。シジル――$ 記号がないから、文字列の中に直接は埋め込めない。連結するか、sprintf を使うことだ」
| |
「些細な不便だが、代わりにPerl がこの定数をインライン展開してくれる。実行速度の観点では損はない」
証拠品の鑑定――偽物を弾く仕組み
「番号札をつけただけでは足りない。次は、証拠品を鑑定して偽物を弾く仕組みが必要だ」
ロックさんが新しいパッケージを書き始めた。
| |
「Enum は Types::Standard に含まれる型制約で、許可される値のリストを定義する。リストにない値を渡すと、即座に例外を投げる」
「即座に?」
「オブジェクト生成のタイミングでだ。タイポが実行時の条件分岐まで潜伏することはない」
言われるまま、Moo の属性に型制約を適用する。
| |
「isa に渡す。Moo は Type::Tiny のオブジェクトをコードリファレンスとして受け取れるから、特別な設定は不要だ」
「はい……」
「試したまえ。先日のタイポを」
恐る恐る、テストに書いてみた。
| |
実行した瞬間、例外が飛んだ。
「これ……タイポした瞬間にエラーが出るんですね」
自分でも声のトーンが変わったのがわかった。深夜3時の2時間が、この1行で消えた可能性があった。
「だが疑問があるだろう」
「あります。Enumで決め打ちしたら、新しいステータスを追加するとき大変じゃないですか?」
「定数定義の一箇所を変えればいい。ALL_STATUSES に追加し、各所で定数名を使う。散在するマジックストリングを全部探して修正するのと、どちらが大変だ?」
「……一箇所のほうが」
「そういうことだ」
通貨にも鑑定を
同じパターンで通貨の型制約も作った。
| |
| |
「通貨もこれで守れるんですね」
もう声は小さくなかった。
リファクタリング後の PaymentProcessor 全体はこうなった。
| |
事件の終わり
テストを走らせた。全件パス。
| |
タイポは型エラーとして即座に弾かれ、タイムアウト秒数やリトライ上限は名前で意味が読み取れる。
自分のエディタを開いた。本番のコードベースの PaymentProcessor.pm に手を入れ始めた。ロックさんは何も言わなかった。キーボードの部品に視線を戻していた。
最初に PaymentStatus パッケージを作り、use constant で5つのステータスを定義する。次に Enum で型制約を生成し、has status の isa に渡す。通貨も同様に。数値リテラルに名前をつけて回る。
どの作業も、やること自体は難しくなかった。
「……もう深夜3時を繰り返さなくて済みそうです」
ロックさんはピンセットでキーキャップを拾い上げ、裏側を確かめていた。
「証拠品に名前をつけるのは、捜査の初歩だ。だが初歩だからといって、誰もがやっているわけではない」
こちらに向けた言葉なのか、独り言なのか、判別がつかなかった。
LCIを出るとき、振り返ると、ロックさんはもうキーボードの組み立てに戻っていた。
探偵の調査報告書
| 容疑(アンチパターン) | 真実(パターン) | 証拠(効果) |
|---|---|---|
マジックナンバーの散在(42, 7, 10000) | use constant による名前付き定数 | 値の意味の自己文書化、変更箇所の一元化 |
マジックストリングのタイポリスク('actve') | Types::Standard::Enum による型制約 | 無効値をコンストラクタ時に即座に検出 |
| 型制約なしの属性 | Moo isa + Type::Tiny | タイポ・無効値の構造的排除 |
推理のステップ
- コード中の意味不明なリテラル値(数値・文字列)を洗い出す
use constantで名前付き定数に変換し、値の意味を刻印する- 文字列リテラルの許容値を列挙し、
Types::Standard::Enumで型制約を定義する - Moo の
isa属性に型制約を適用し、コンストラクタとセッターで不正値を弾く - テストを実行し、タイポや無効値が即座に検出されることを確認する
ロックより
証拠品には二種類ある。名前のない品物と、番号札のついた品物だ。前者は現場に転がっているだけの残骸であり、後者は事件を解決する手がかりとなる。
コードに埋まった 42 や 'pending' は、名前のない証拠品だ。それ自体は無害に見える。だが散乱した証拠品は、いつか捜査を誤らせる――先日の深夜のように。
名前をつけろ。型で守れ。証拠品管理の基本を怠った現場から、真実は見つからない。
