深夜3時のPagerDutyアラートから18時間が経った。
僕は小野寺。社内システム部で通知基盤を担当しているエンジニア、30歳。入社7年目にして初めての本番障害を出した——原因は自分のコードだ。
会議室Cのホワイトボードには、障害タイムラインが殴り書きされている。「03:02 PagerDuty発報」「03:08 小野寺対応開始」「03:24 原因特定:notify_payment_failed の Slack defined チェック漏れ」「03:31 ホットフィックス適用」。
社内の受発注システムには、注文・発送・キャンセル・決済失敗の4イベントで通知を飛ばす仕組みがある。通知先はメール、Slack、ログの3種類だが、ユーザーの設定によって「メールだけ」「Slackだけ」「通知なし」と異なる。問題は「通知なし」のユーザーだ。全体の7割を占める。通知先が未設定のユーザーでは notifier が undef になるから、コードのあらゆる場所に if (defined $self->email_notifier) というガードを入れている。4つのメソッド × 3つの通知先 = 12箇所。同じ防衛パターンの繰り返しだ。
昨夜の障害は、決済失敗の通知メソッドで Slack の defined チェックを1箇所だけ書き忘れたことが原因だった。Can't call method "send" on an undefined value——深夜3時にこのメッセージが鳴り響いたとき、僕は自分の注意力の限界を悟った。
ポストモーテムの準備をしているところに、上司の三田さんが会議室に入ってきた。
「小野寺、根本原因の分析を一人でやるな。コードの構造的な問題を見抜くのがうまい人間を知ってる。少し変わった奴だが、腕は確かだ」
「変わった、というのは?」
「まあ、会えばわかる」
10分後。ノックもなしに会議室のドアが開いた。
入ってきた男はショルダーバッグからノートPCと大玉のトラックボール——Kensingtonだ——を取り出すと、挨拶もなく会議テーブルに並べ始めた。名刺もない。PCの天板には「Locke - Code Detective」のステッカーが貼ってある。
(コード探偵?)
男はホワイトボードのタイムラインを一瞥して言った。
「——幽霊退治の記録だね」
「障害対応のタイムラインです。03時02分に——」
「Can't call method "send" on an undefined value。典型的な幽霊事件だよ」
言い当てられた。三田さんから概要を聞いているのか、それともこの手の障害に見慣れているのか。男はトラックボールを手元に引き寄せながら続けた。
「undef は幽霊だ。実体がないのに、コード全体を支配する。おまえたちの12箇所の防衛線は、お祓いの呪文と同じだ。毎回唱えているのに、1回唱え忘れたら深夜3時に起こされる」
「コードを見せたまえ、ワトソン君」
「小野寺です」
訂正したが、男はすでに僕のPCの方を向いていた。
現場検証:12箇所の防衛線
ロックと名乗った男——名刺がないからステッカーで判断するしかない——は僕のPCの前に座ると、許可を求めずにスクロールを始めた。三田さんの「腕は確かだ」がなければ、この時点で追い出していたと思う。
| |
ロックはトラックボールをゆっくり回しながら、画面を追っていた。大きな球体が手のひらの下で静かに回転する。やがて手を止め、画面を指さした。
「——12箇所」
「はい。4メソッド × 3通知先で12箇所です」
「そして13箇所目を書き忘れたから、深夜3時にアラートが鳴った」ロックはホワイトボードのタイムラインに目を向けた。「犯人は undef じゃないよ、ワトソン君」
「小野寺です。2度目ですが」
「犯人は、undef を恐れる その設計 だ。存在しないかもしれない相手に、毎回12回のお伺いを立てている。防衛的プログラミング——それが今回の真犯人だよ」
確かに、同じコードが12回。だが僕にはまだ反論がある。
「でも、通知先が任意設定である以上、undef チェックは安全策として必要でしょう。書き忘れた僕が悪いだけで、設計自体は間違っていない——」
「では聞くが」ロックが振り向いた。「SMS通知を追加したらどうなる?」
「4メソッドに if (defined $self->sms_notifier) を追加して……16箇所になります」
「Webhook通知を追加したら?」
「20箇所」
「LINEは?」
「24——」
言いかけて、口を閉じた。安全策が増殖するたびに、漏れのリスクも増殖する。防衛線を張る行為そのものが、新たな脆弱性を生み出している。
推理披露:幽霊に実体を与えよ(Null Object)
ロックは会議テーブルに置いてあったペットボトルの水を勝手に開けた。僕の水だ。文句を言おうとしたが、すでに一口飲まれていた。
「幽霊を退治する方法は2つある。一つは、幽霊が出るたびにお祓いをする——いまの if (defined ...) だ。だがもう一つの方法がある」
「もう一つ?」
「幽霊に実体を与える」
ロックはPCを開き、コードを書き始めた。トラックボールの大玉がカタカタと小さく音を立てるが、ほとんどキーボードだけで作業していた。
「まず、通知者の契約書を作る」
【After】Notifier ロール(インターフェース)
| |
「Notifier ロールは契約書だ。send メソッドを持つこと——それが通知者の唯一の義務だ」
【After】本物の通知者たち
| |
「ここまでは設計の整理ですよね。でも通知不要のユーザーは? undef を入れないと——」
「入れない」ロックは画面から目を離さず言った。「4人目の通知者を用意する」
【After】NullNotifier — 何もしないが、同じ契約に従う
| |
「……何もしないオブジェクト?」僕は眉をひそめた。「メモリを使ってインスタンスを生成しておいて、何もしないんですか。通知なしユーザーは7割ですよ。7割のユーザーに空のオブジェクトを割り当てるのは——」
「無駄だと?」ロックは初めてこちらに視線を向けた。「ではその “無駄” と、深夜3時のアラートを天秤にかけたまえ。NullNotifier のインスタンスはフィールドを一つも持たない。メモリ消費は実質ゼロだ。一方、defined チェックの漏れは本番障害になる。省くべきコストの見積もりが逆だよ」
反論できなかった。インスタンス1つのメモリコストと本番障害のコストでは、比較にならない。
「だが、もう一つ訊きたいことがありそうだね」
見透かされている。僕は率直に言った。
「defined チェックを漏らしたのは僕のミスです。でもそれはテストの網羅性で防げる問題じゃないですか? すべてのメソッドに『通知なしユーザーで呼んでもエラーにならない』テストを書けば——」
「書けば、何だ?」ロックがトラックボールの球体を指先で弾いた。「テストを書くのも人間だよ。12箇所の defined チェックを書き忘れる人間が、12箇所のテストを書き忘れない保証がどこにある? 人の注意力に依存する設計は、設計とは呼ばない。構造で防げ」
Logger にも同じ手法を適用する。
| |
「そして NotificationService はこうなる」
【After】NotificationService — defined チェック完全消滅
| |
僕は画面を見つめた。
「if (defined ...) が——1つもない」
「default => sub { NullNotifier->new } がすべてを解決している。通知先が指定されなければ NullNotifier が入る。undef はもう存在しない。チェックを忘れるも何も、チェックする必要がなくなった」
classDiagram
class Notifier {
<<Role>>
+send(message)*
}
class EmailNotifier {
+address
+send(message)
}
class SlackNotifier {
+channel
+send(message)
}
class NullNotifier {
+send(message)
何もしない
}
class NotificationService {
+email_notifier
+slack_notifier
+logger
+notify_order_placed()
+notify_order_shipped()
+notify_order_cancelled()
+notify_payment_failed()
}
Notifier <|.. EmailNotifier
Notifier <|.. SlackNotifier
Notifier <|.. NullNotifier
NotificationService --> Notifier : uses
「新しいチャネルの追加も見てくれたまえ」
| |
「SMS を追加しても NotificationService のコードは1行も変わらない。Notifier ロールを実装するだけだ。defined チェックの追加は——」
「ゼロ。追加するチェックが存在しないから」
「そういうことだ」
解決:12箇所の防衛線、消滅
ロックがテストを走らせた。
| |
「Before のテスト3——undef に対して send を呼ぶと即座にクラッシュする。これが昨夜おまえを叩き起こした犯人だ」
「After のテスト1……notifier が指定されていなくても NullNotifier が入っているから、defined は常に真」
「テスト5がポイントだよ。EmailNotifier、SlackNotifier、NullNotifier——すべてが Notifier ロールを実装している。呼び出し側に区別はつかない。本物も幽霊も、同じ契約書にサインしている」
「テスト6——After コードの defined チェック数がゼロ」
ロックはPCを閉じ、トラックボールをバッグに戻し始めた。
「一つだけ」
僕は身構えた。
「Null Object は 何もしないこと が正しい場合にだけ使いたまえ。もし通知不要のユーザーにもデフォルトログを残したいなら、それは NullNotifier ではなく DefaultNotifier の仕事だ。幽霊に実体を与えるのは、幽霊が本当に何もしないときだけだ」
ショルダーバッグを肩にかけ、ドアに手をかけたところで振り返った。
「——深夜3時のアラートは、もう鳴らないだろう。小野寺」
名前で呼ばれたのは、それが初めてだった。僕が何か返す前に、ドアは閉まっていた。
会社のSlackに書いた。「通知基盤の defined チェック撲滅、着手します。Null Object パターンで全箇所の防衛コードを除去します」——深夜3時のアラートは、もう鳴らないはずだ。
探偵の調査報告書
| 容疑(アンチパターン) | 真実(パターン) | 証拠(効果) |
|---|---|---|
防衛的プログラミング(Defensive Programming)。通知先が未設定(undef)のユーザーに対し、4メソッド×3通知先=12箇所に if (defined ...) チェックを散布。1箇所の漏れが本番障害を引き起こし、チャネル追加のたびにチェック箇所が増殖する。 | Null Object パターン。Notifier ロール(インターフェース)を定義し、NullNotifier(何もしない実装)をデフォルト値として注入。undef を排除し、defined チェックをゼロにする。 | 12箇所の if (defined ...) が完全に消滅。チェック漏れによる本番障害のリスクがゼロに。新チャネル追加時もロールを実装するだけで、既存コードの修正不要。 |
推理のステップ
- ロール(インターフェース)を定義する:
Notifierロールでsendメソッドをrequires宣言する。すべての通知者はこの契約に従う。 - Null Object を実装する:
NullNotifierクラスを作り、with 'Notifier'で同じロールを実装する。sendメソッドは何もせずreturn 1するだけ。 - デフォルト値として注入する:
has email_notifier => ( default => sub { NullNotifier->new } )で、未指定時に自動的に Null Object が入るようにする。undefの余地を消す。 - 防衛コードを全削除する: すべての
if (defined ...)を削除する。全属性が必ずオブジェクト(本物または Null Object)なので、チェックは不要。
ロックより
ワトソン君——いや、小野寺。undef は幽霊だ。存在しないのに、おまえのコードを支配し、12箇所の防衛線を張らせ、深夜3時の眠りを奪う。
Null Object の本質は「不在の表現」だ。「何もない」を undef で表すのではなく、「何もしない」オブジェクトで表す。呼び出し側が相手の正体を気にする必要はない——同じ契約に従っているのだから。
ただし、覚えておけ。幽霊に実体を与えるのは、幽霊が本当に「何もしない」ときだけだ。「デフォルトの処理をする」必要があるなら、それは Null Object ではなく Default Object の領域だ。
