I. 依頼(事務所への来客)
深夜の「レガシー・コード・インベスティゲーション(LCI)」。 雑居ビルの2階にある探偵事務所のドアを、私はふらつく足取りで押し開けた。
「助けてください。もう3日も家に帰れていません……!」
私の名前はハヤト。外資系決済サービスのAPI連携を担当している入社2年目の若手エンジニアだ。徹夜明けでボロボロの私を出迎えたのは、季節外れのヨレヨレのトレンチコートを羽織った男——自称コード探偵ロックだった。
彼はモニターから視線を外さず、手元の異様にヌルい特大エナジードリンクをストローで啜った。ズズズ、という下品な音が響く。
「ほう。随分と酷い顔をしているね、ワトソン君。目の下のクマが深すぎて、まるで3重にネストされた for ループのようだ」
「ハヤトです。あの、システムで奇妙な不具合が起きていて……」
「聞かせてみたまえ。君を路頭に迷わせている『におい』の正体を」
私は重いノートPCをデスクに置き、震える手でコードを映し出した。
「注文処理でエラーが発生したとき、DBはロールバックできるんですが、外部の『決済API』への課金リクエストが取り消せないんです。『お代は頂いたのに注文はキャンセルされる』という最悪の不整合が起きていて……もう経理からのクレーム電話に出るのが怖いんです!」
II. 現場検証(時間の不可逆性)

ロックは私の書いた決済処理 OrderProcessor クラスを覗き込んだ。
| |
「ご覧の通りです。DBへの保存が失敗したら $self->db->rollback() で戻しています。でも、その直前に走っている charge メソッドは外部APIへのHTTPリクエストなので、DBのトランザクションでは戻らないんです」
私が絶望まじりに説明すると、ロックは深くため息をついた。
「初歩的なことだよ、ワトソン君。君のコードは、コントローラーの中に直接『副作用(API通信)』をベタ書きしている。これは Fat Controller / Hardcoded Actions と呼ばれるにおいだ」
「じゃあ、失敗したときに手動で refund(返金API)を呼ぶコードを足せば……」
「それこそが泥沼への入り口だ! 『もしAが失敗したらBの返金を呼び、もしBが失敗したらAとBの返金を呼ぶ……』そんなエラーハンドリングのバケツリレーを書き始めたら、最後には君の正気ごとシステムがクラッシュする」
ロックは鋭い視線を画面に向けた。
「トランザクションという『魔法のキャンセルボタン』に甘えるのはやめたまえ。起きてしまった過去(外部通信)は、魔法では取り消せない。時間を巻き戻すには、自分が『何をしたか』を正確に記録したタイムマシンを作る必要があるのだよ」
III. 推理披露(実行履歴という名のタイムマシン)
ロックの指がキーボードの上で凄まじいスピードで踊り始めた。
「まず、君が無造作に呼び出していた『決済処理』や『DB保存』というただの「命令」を、独立した「オブジェクト」として切り離す。そして彼らに、実行(execute)と取り消し(unexecute)という2つの振る舞いを義務付けるんだ」
Command Role と具体的な役者の定義
| |
「……命令を、わざわざクラスにするんですか?」 「そうだ。こうすることで、『金額1000円の決済を行った』という事実が、物理的なオブジェクトとして手元に残る」
履歴管理者(Invoker)の召喚
ロックはさらに OrderInvoker というクラスを書き上げた。
「そして、これらのコマンドを順番に見届ける『歴史の記録者(Invoker)』を用意する。彼はコマンドが実行されるたびに、それをスタック(配列)に積み上げていくんだ」
| |
Context の解放

「さあ、ワトソン君。これですべての準備は整った。君のFat Controllerを安全な世界へ導こう」
| |
私は画面を見て息を呑んだ。
あの煩わしかったエラーハンドリングのパズルが、たった1行の $invoker->undo_all() に置き換わっている。
「もし決済(API)だけ成功して、DB保存でエラーになったら……?」
「Invokerの履歴(history)には、Command::Payment が1つだけ積まれている状態だ。undo_all が呼ばれると、積まれた最新の履歴から unexecute(取り消し)が呼ばれる。つまり、確実に返金APIが叩かれるというわけさ」
IV. 解決(事件の終わり)
テスト用の注文データを流し込むと、コンソールに見事な緑色の文字が並んだ。 DBエラーが発生した瞬間、トランザクションのロールバックと同時に、APIへの返金処理が確実に行われたのだ。
「完璧です……! これなら、後から『ポイント付与』や『在庫引当』のAPI呼び出しが増えても、新しく Command::AddPoints や Command::ReserveStock を作って execute_command するだけでいいんですね」
「ご名答。もう手動で複雑なエラーハンドリング関数を継ぎ接ぎする必要はない。各々のコマンドが、自分自身の『行い』と『その償い方(取り消し方)』を知っているのだからね」
ロックは満足げに、空になったエナジードリンクの缶をゴミ箱へ放り投げた。
「コマンドがオブジェクトとして実体化するということは、単に取り消し(Undo)ができるというだけではない。ログとしてDBにシリアライズして保存すれば、もしサーバーがクラッシュしても、再起動後に再実行(Redo)することすら可能なのだよ」
「履歴という名のタイムマシン……。すごい、すごすぎます、ロックさん!」 「フッ。真名で呼んでくれたのは君が初めてかもしれないな。だが忘れないでくれたまえ。過去を正しくやり直すためには、現在の行いを正確に記録する知性が必要なのだ」
私は深々と頭を下げた。これでようやく、自宅のベッドで眠ることができる。経理からの恐怖の電話におびえることもない。
「あの、ロックさん。調査費用はいかほどで……?」 「さてね。君のシステムが失わずに済んだ決済手数料と同じだけの、とびきり美味いピザを頼むとしようか。もちろん、注文を『取り消す』権利は放棄してもらうがね」
私は笑いながら、深夜のピザ屋のメニューを開いた。
探偵の調査報告書
| 容疑(アンチパターン) | 真実(パターン) | 証拠(効果) |
|---|---|---|
| Fat Controller / Hardcoded Actions。コントローラーの中に外部API呼び出し等の「副作用」が直書きされ、エラー発生時の取り消し(ロールバック)や再実行が不可能・複雑化している状態。 | Command パターン。「〜をする」という命令(振る舞い)そのものを1つのオブジェクト(クラス)としてカプセル化し、実行(execute)と取り消し(unexecute)を対にして定義する。 | コマンドをキューやスタックに積むことで、複数ステップの処理に対する安全な Undo (取り消し) や Redo (再実行)、非同期実行などが可能になる。 |
推理のステップ
- Command Roleの定義: 『実行する(
execute)』と『取り消す(unexecute)』のインターフェースを持つRoleを作成する。 - 命令のオブジェクト化: 外部API通信やDB保存など、副作用の伴う具体的な処理をそれぞれ独立したCommandクラスとして切り出す。
- Invokerによる履歴管理: コマンドを直接実行するのではなく、管理役(Invoker)を通じて実行し、実行したコマンドをスタックに保存する。
- 安全なロールバック: エラー発生時は、Invokerのスタックからコマンドを取り出し、逆の順番で
unexecuteを呼び出して状態を復元(補償トランザクション)する。
ロックより
ワトソン君。君が頼っていたデータベースのトランザクションは確かに強力だが、それはシステムという閉じた箱の中でのみ通用する魔法に過ぎない。 一歩でも外(APIやメールなどの外部システム)へ足を踏み出せば、覆水は盆に返らないのだ。
システム設計において『取り消し』を想定することは敗北ではない。それは、世界が不確実であることを受け入れる勇気と知性の証だ。 Commandオブジェクトとして封じ込められた君の「行い」は、きっと未来のトラブルから君自身を救ってくれることだろう。
