I. ロビーの邂逅
スマホの画面に「LCI レガシー・コード」と打ち込んだところで、検索結果を開く前に手が止まった。
知り合いのテックリード——別の会社で基盤チームを率いている男だ——が先月の飲み会で言っていた。「コードの診断サービスをやってる変な人がいる。探偵ごっこみたいなことをするんだけど、腕は確かだ」。酔った勢いの与太話だと思って聞き流していたが、先週の深夜対応のあと、その言葉が妙にひっかかっている。
自社ビルの1階ロビー、昼休みのソファ。弁当を食べ終えて、ぼんやりとスマホをいじっていたところだった。
「その消火器の配置は6メートル間隔だね。悪くないが、エレベーターホール側の7番目が欠けている」
独り言が聞こえた。ロビーの壁際に立つ男が、消火器を凝視している。コートを着ている。夏前のオフィスビルで、コートを。
男が振り向いた。スマホの検索結果に表示された写真の顔と一致した。
「あの——LCIの方ですか」
「ロック。コードの探偵だ」
男はコートのポケットから缶のエナジードリンクを取り出し、プルタブを開けた。ロビーで。
「別のクライアントとの打ち合わせ帰りでね。このビルの防火設備の配置が気になって——」
「すみません」俺はスマホを見せた。「ちょうど、あなたに連絡しようとしていたところなんです」
ロックさんは俺のスマホ画面を一瞥し、それからゆっくりと俺の顔を見た。鼻がかすかに動いた。
「elsif のにおいがする。5段……いや、6段に近いな」
「は?」
「テックリードは金庫番に似ている」ロックさんはソファの反対側に腰を下ろした。「金庫を開け閉めする回数が多いほど、手に油のにおいが染みつく。君の場合は、鍵穴の数が多すぎるにおいだ」
「……鍵穴の数。うん、まあ、当たってるかもしれない」
「ワトソン君、まず金庫の中身を見せてくれたまえ」
ワトソン。いきなり来た。知り合いのテックリードが言っていた、「探偵ごっこ」とはこういうことか。
「ワトソン……いいよ、好きに呼んでくれ。名前より中身のほうが大事だろ」
俺はノートPCをカバンから取り出し、ロビーのソファでコードベースを開いた。
II. 金庫の中の散弾痕
通知システム。俺のチームが内製開発した、社内向けのイベント通知基盤だ。
「最初はメールとSlackだけだった。半年前の話だ」
俺は NotificationFormatter クラスを画面に映した。format_message メソッド。当初はシンプルな if/else だった。
「ところが、四半期ごとに新しいチャネルが増えていく。Webhook、SMS、Teams。そのたびに elsif を足した。そして先週——」
「先週?」
「Webhook対応のリリースで、既存のSlack通知のフォーマットが壊れた。深夜1時に障害報告が来て、3時間かけてホットフィックスを当てた」
ロックさんはエナジードリンクを一口飲み、画面を見た。
| |
「一斉送信の broadcast メソッドにもチャネル名をハードコードしてある」
| |
ロックさんはしばらく黙っていた。それからエナジードリンクの缶を膝の上に置いて、低い声で言った。
「金庫の蓋を開けるたびに、中のものを全部取り出して並べ直しているのかね」
「そうなんだよ。新しいものを一つ入れたいだけなのに、既存の中身まで触らないといけない」
「犯人は明白だ」ロックさんは画面を指さした。「オープン・クローズド原則違反。拡張に開かれ、変更に閉じているべきコアが、機能を追加するたびに腹を切り開かれている。散弾銃手術——shotgun surgery だよ、ワトソン君。一発撃つと、弾が5つの分岐すべてに当たる」
「OCP か。知識としては知ってるんだが、具体的にどう『閉じる』のかがわからなかった。閉じたら拡張できないだろ?」
「金庫を閉じたまま、鍵穴だけを標準化するのだよ。プラグインという名の鍵を、外から差し込めるようにする」
III. 鍵穴の設計図
ロックさんは俺のノートPCを借り受けると、エディタに新しいファイルを開いた。
「解法は3つの部品で構成される。鍵穴の仕様、鍵そのもの、そして鍵師だ」
部品1: プラグインインターフェース(鍵穴の仕様)
「まず、鍵穴——すなわちプラグインのインターフェースを定義する」
| |
「Moo::Role は Perl の世界でインターフェースと実装を兼ねる仕組みだ。requires で契約を定義する。この鍵穴に合う鍵は、channel_name と format の2つのメソッドを必ず持っていなければならない」
「インターフェースか。Java でいう interface に近い?」
「近いが、もう少し筋肉質だ。Role は before、after、around といったメソッド修飾子を使って、合成先のクラスの振る舞いを拡張できる。インターフェースに筋肉がついたもの、と思ってくれればいい」
「で、requires を満たさなかったらどうなるんだ?」
「鍵穴と鍵の形が合わなければ、ドアは開かない。Role をクラスに合成する時点で即座にエラーだ。テスト以前の問題だよ。つまり、壊れたプラグインは存在すら許されない」
部品2: 具象プラグイン(鍵)
「次に、各チャネルをプラグインとして独立させる。今まで elsif の中にあったロジックを、それぞれ別の鍵にする」
| |
「なるほど。今まで elsif の中にあった処理が、そっくりプラグインのクラスに引っ越しただけか。ロジック自体は変わっていない」
「そう。引っ越し先が変わっただけだ。だが、その引っ越しが決定的に重要なのだよ。なぜだかわかるかね」
「……それぞれ独立したファイルだから、Slackのコードを触ってもEmailに影響しない?」
「正解だ。分離されたことで、影響範囲が閉じた。先週の事故——Webhook追加でSlackが壊れた——は、もう起きようがない」
部品3: コア(金庫本体)
「そして最後に、金庫本体だ。if/elsif を全て取り除いた、清浄なコア」
| |
「ちょっと待ってくれ」俺はコードを上から読み直した。「format_message に if が一つもないぞ」
「そう。find_plugin がチャネル名に対応するプラグインを探し、そのプラグインの format を呼ぶ。チャネルが何であるかはコアの関心事ではなくなった」
「じゃあ、6つ目のチャネル——たとえばDiscord——を追加するときは?」
「Plugin::Discord クラスを書いて、register_plugin で登録するだけだ。コアのコードは一行たりとも触らない」
「一行も?」
「一行もだ。broadcast もチャネル名をハードコードしていないだろう? 登録済みプラグインを順に回すだけだ。金庫を閉じたまま、鍵を足す。これがPlugin Pattern の核心だよ」
around と before/after の使い分け
「ところで、さっき Role の話で before、after、around が出てきたが、全部 around でよくないか?」
「around は金庫室の鍵そのものを預かる行為だ」ロックさんはエナジードリンクの缶を揺らした。「元のメソッドを呼ぶか呼ばないかまで制御できる。入退室の記録を取りたいだけなら、受付に before/after を置けばいい。権限の最小化は防犯の基本だよ、ワトソン君」
| |
「なるほどな。ログを取りたいだけなら before/after で十分で、入出力を加工したいときだけ around を使うと」
「そういうことだ。不必要に強い権限を渡さないこと。プラグインの設計も同じだ」
プラグインの障害隔離
「もう一つ聞いていいか。新人が書いたプラグインにバグがあったとして、コア全体が止まったりしないか?」
「eval で隔壁を建てる。一つの金庫室で爆発が起きても、隣の部屋には延焼しない設計だ」
| |
「Slack のプラグインが壊れても、Email と Webhook は送信される。障害が局所化されているから、影響範囲を見積もれる——テックリードとして、これが一番ありがたいんじゃないか」
「ありがたい。深夜1時の障害報告で『全チャネル停止』と『Slack だけ停止』は天と地の差だ」
条件のオブジェクト化との接続
「……そういえば」俺はふと思い出した。「知り合いが、『条件をオブジェクトにしたら楽になった』と言っていた。Specification とか何とか」
ロックさんの口元がかすかに持ち上がった。
「条件の拡張と機能の拡張は、同じ思想の表裏だ。条件を if に書く代わりにオブジェクトにする。機能を elsif に書く代わりにプラグインにする。どちらも、コアを開かずに外から足す。鍵穴を増やすか、部屋を増やすかの違いだね」
IV. 金庫の検証
俺はプラグインのテストを書いた。まず各プラグインの単体テスト。それからコアとの統合テスト。
| |
テストが走った。全件グリーン。
「……新チャネル追加で、コアのテストを一切触っていないな」
「プラグインのテストだけを書けばいい。金庫の構造試験と、鍵の動作試験は別物だ」
俺は画面を見つめた。先週の深夜対応が頭をよぎる。あのとき壊したのはSlackのフォーマット処理で、原因はWebhookの分岐を追加した際の隣接行の編集ミスだった。プラグインなら、Webhookのファイルを触ってもSlackのファイルには手が届かない。物理的に壊しようがない。
「上出来だ」俺は言った。「明日のスプリントレビューで提案できる」
ロックさんはノートPCを俺に返し、ソファから立ち上がった。
「ワトソン君、一つだけ忘れるな」
「なんだ?」
ロックさんはコートの襟を正した。
「迷宮は、出口から抜けるものじゃない。入口を設計し直すんだ」
自動ドアが開き、ロックさんの背中がビルの外に消えた。エナジードリンクの空き缶だけが、ソファの肘掛けに残されていた。
「……入口の設計し直しか」
俺はノートPCを閉じ、スマホのカレンダーを開いた。明日のチームミーティングのアジェンダに一行書き足す。
「リファクタリング提案: プラグインアーキテクチャ」
探偵の調査報告書
| 容疑(アンチパターン) | 真実(パターン) | 証拠(効果) |
|---|---|---|
OCP違反(機能追加のたびにコアの if/elsif を変更) | Plugin Pattern(Moo::Role によるプラグインインターフェース + 動的登録) | コアを変更せずに新チャネル追加が可能。影響範囲がプラグイン単位に局所化 |
| 散弾銃手術(1つの変更が複数分岐に波及) | プラグインの分離(各チャネルが独立したクラス) | チャネル間の依存がゼロ。一方の変更が他方に影響しない |
| ハードコードされたチャネルリスト | 登録ベースの動的チャネル管理 | broadcast がプラグイン一覧から自動生成。リスト管理の手動更新が不要 |
推理のステップ
- プラグインインターフェースを定義する:
Moo::Roleでrequires 'channel_name',requires 'format'を宣言。全プラグインが満たすべき契約を定める - 既存の分岐をプラグインに分離する:
elsifごとの処理を独立したクラスに移動。with 'NotificationPlugin'でインターフェースを消費 - コアからif/elsifを除去する:
find_pluginでチャネル名からプラグインを検索し、formatに委譲。コアはチャネルの種類を知らない - 障害隔離を組み込む:
evalでプラグイン呼び出しを包み、一つの障害が全体に波及しないようにする - テストを分離する: コアのテストとプラグインのテストを独立させ、プラグイン追加時にコアのテスト変更を不要にする
ロックより
ワトソン君。金庫を頑丈にすることと、金庫を開けやすくすることは矛盾しない。標準化された鍵穴さえあれば、金庫を閉じたまま中身を増やせる。それが Plugin Pattern の本質だ。
君のチームが来週新しいチャネルを追加するとき、触るのはプラグインのファイルだけだ。コアには一切の指紋を残さない。深夜の障害対応が減ることを祈っているよ。
金庫を閉じたまま鍵を増やせるようになった君なら、もう迷宮に惑わされることはないだろう。
