ザザッ、と大きな金属ざるが冷水に沈む。 氷の浮かんだ澄んだ水の中で、茹で上がったばかりのそばがキュッと締められていく。
「はい、出前一丁! 急ぎで頼むよ!」 「あいよ、冷やしたぬき二丁、すぐに出る!」
初夏の陽気が心地よい昼下がり。私とシェフは、出前専門店「出前そば・やぶきた」の配送センターの厨房を訪れていた。今日はプライベートで冷やしそばを食べに来たのだが、ちょうどランチタイムのピーク。配送車が忙しく出入りし、厨房の中は熱気と冷やしそばの清涼感ある香りで満ちていた。
私たちは邪魔にならない片隅でそばをすすりながら、厨房の様子を眺めていた。 厨房の真ん中には、そばを冷やすための「大きな冷水槽(タライ)」が置かれている。職人たちは、大釜から引き上げたそばをその大タライの中に次々と投げ入れ、一斉に手を入れてヌメリを取り、氷を足してそばを締めている。
大勢の手が同じ大タライの水に突っ込まれ、氷が足され、水が濁っていく。 その無駄のない活気ある動きをじっと観察していると、ある違和感が私の胸に浮かび上がってきた。
だが、私の思考を遮るように、配送センターの片隅に置かれたPCラックの前で、一人の若いエンジニアが大きなため息をついた。
本日の持ち込み素材:のれん分けされた偽物のスープ
ため息の主は、「やぶきた」のシステム開発を担当しているソウスケさん(29)だった。額に汗をびっしょりとかき、手元にはノートPCを抱えている。
「どうしてだ……。設定マスタは『1つ』にしたはずなのに、なぜテストを並列で動かすと、関係ないはずのテストが次々と落ちるんだ……?」
ソウスケさんは、配送エリアと配送料金を判定するシステムの不具合に悩まされていた。
「やぶきた」では、出前の注文が入るたびに、配送先の住所や時間帯に応じて配送料を計算する。しかし、配送エリア判定モジュール(DeliveryZone)や料金計算モジュールが、それぞれの中で勝手に MyConfig->new を実行して設定ファイルを読み込んでいた。
そのため、お昼の忙しい時間帯に設定マスタが更新された際、読み込みのタイミングがズレてしまい、「配達エリア外なのに注文を受けてしまう」といった注文不整合のバグが多発していたのだ。
ソウスケさんは、設定オブジェクトがあちこちで new されるのが原因だと見抜き、本で学んだ Singleton(シングルトン) パターンを適用して見事にバグを解決した。
| |
呼び出し側のモジュールは、それぞれ new するのをやめ、この唯一のインスタンスを指し示すように書き換えた。
| |
「これで、システム内の設定オブジェクトは完全に『たった1つの真実(Source of Truth)』になりました。マスタの同期ズレは完璧に防げたんです!」
ソウスケさんは真面目な顔で、しかし焦りながら説明する。
「でも……今度はテストコードが全滅なんです。テスト環境(env => 'test')での動作を確認するテストを動かした後、別のテストで本番環境の動作を確認しようとすると、前のテストの『test』という設定が残ってしまって、テスト結果が汚染されて落ちてしまう。並列でテストを動かすと、もう何が起きているのか分かりません……」
私はソウスケさんのコードと、先ほどから見ていた厨房の大タライの光景を重ね合わせ、ハッとした。
「あの、ソウスケさん。それって、さっきのそばを締める『大タライ』と同じじゃないですか?」
ソウスケさんが不思議そうに私を見る。
「あの大タライ、みんなで一斉にそばを投げ込んで洗っていますよね。もし一人の職人さんが『もっと冷たくしよう』と思って大量に氷を入れたら、隣で洗っている別のそばの締め具合まで変わっちゃいます。誰かがそばのヌメリを残したままにしたら、タライ全体の水が濁って、他のそばまでヌルヌルになってしまう。 ソウスケさんのテストも、1つの設定ファイルをみんなで同時に触っているから、あるテストが足した隠し味(設定変更)が、隣のテストの鍋に入り込んで、全体の味が狂っちゃっているんじゃないですか?」
ソウスケさんは息を呑んだ。
「その通りです……。Singletonにしたせいで、メモリ上の設定インスタンスが完全に『共有のグローバル変数』になってしまった。だから、あるテストが設定を書き換えると、別のテストにまでその変更が引き継がれて、テスト同士が干渉し合ってしまうんだ……」
「外部から依存関係が見えない『暗黙の依存』になっているから、テストのときに設定をテスト用に差し替える(モック化する)こともできません……」とソウスケさんは肩を落とした。
「でも」とソウスケさんは眉をひそめて問いかけた。「マスタを1つにするには、Singletonにするしかないじゃないですか! なぜこれがダメなんですか?」
素材を見る目:暗黙の依存と状態汚染の匂い
シェフはガコンとそばの器を置くと、ヤンさんの時と同じように静かに歩み寄ってきた。
「『たった一つの正解』を求めすぎて、自分で自分の首を絞めたな」
シェフはソウスケさんの画面を指差した。
「お前がやらかしている仕込みミスは、『グローバル状態への暗黙の依存(Implicit Global Dependency)』と、それに伴う『共有状態の汚染(Shared State Pollution)』だ。Singletonは一見、設定を1つにまとめられて便利に見える。だが本質は、どこからでもアクセスできて書き換えられる『ガラの悪いグローバル変数』と変わらない」
シェフはさらに腕を組んで解説を続ける。
「前回の『Flyweight(フライウェイト)』を覚えているか? あれはメモリ節約のために『読み取り専用(immutable)』のオブジェクトを共有した。だからどれだけ共有しても、中身が書き換わって他の処理を壊す心配はなかった。 だが、今回の Singleton は違う。お前は『書き換え可能(mutable)』な状態をグローバルに保持しちまった。だから、並列テストを動かしたときに、お互いのメモリ空間を書き換え合ってテストが全滅するんだ」
「仕込みの順番が逆なんだよ」 シェフは包丁の背でトントンと調理台を叩いた。 「タレを1つに絞ることと、全員が同じ大釜(グローバル空間)に直接手を突っ込むことは別だ。必要なものは、外から『器』に盛って明示的に手渡してやる。それがプロの仕込みだ」
包丁を入れ直す:DIによる「器の仕込み」
シェフは、大タライの横から空の「小さなざる」をいくつか持ってきた。
「そばは大タライに直接ぶち込むんじゃない。一人前ずつ『個別のざる(インスタンス)』に入れろ。そして、そのざるに必要な分だけの『冷水(設定オブジェクト)』を外から注いで締めるんだ」
シェフは実際にそばをざるに取り、冷水を注いで締めてみせた。
「モジュール自身に『タレ(設定)を取りに行かせる』のをやめろ。注文という『お皿(コンテキスト)』が運ばれるときに、最初から必要な設定を器に盛って一緒に渡してやるんだ。この技法を『依存性注入(Dependency Injection:DI)』と呼ぶ」
「依存性注入……」ソウスケさんがその言葉を繰り返す。
「そうだ。のれん分けの店が本店の奥まで勝手にタレを盗みに入ったら泥棒だろ。本店が自ら、それぞれの店(モジュール)の器にタレを注ぎ分けてやる。そうすれば、テストの皿にはテスト用のタレを安全に注げるし、お互いの味が混ざることもない」
依存性注入(DI)の設計
Singleton パターンはクラス内部でインスタンスの単一性を強制しますが、DI では「単一性の管理」をオブジェクト自身から剥がし、システム全体の構成を行う「外部(呼び出し元やDIコンテナ)」に委ねます。
これにより、クラス自体は疎結合になり、テスト時には簡単にモックや別のインスタンスへ差し替えることが可能になります。
Before(改善前)と After(改善後)のオブジェクト構成を可視化すると、以下のようになります。

仕込み直し(Afterコード)の実装
まず、MyConfig を Singleton ではない、通常の不変クラスとして再定義します。デフォルト値を維持しつつ、属性を is => 'ro'(読み取り専用)にすることで、後から勝手に設定が変更されるリスク(ミュータビリティの害)を防ぎます。
| |
次に、依存する側の DeliveryZone クラスをリファクタリングします。内部で MyConfig->instance を直接呼び出すのをやめ、コンストラクタで明示的に config を受け取るようにします。
| |
完成:独立したざるで締まるそば
「これなら……!」 ソウスケさんの目が輝いた。
「モジュール内部からグローバル呼び出しが完全に消えました! これなら、テストごとに個別の MyConfig インスタンスを new して渡だけで済みますね!」
「そういうこと!」と私は大きく頷いた。「大タライに直接そばを突っ込むのをやめて、一人ずつ独立した『マイざる』でそばを締めるイメージです。他の人がざるの水をどうしようと、自分のざるの水(設定インスタンス)は完全に独立しているから、絶対に味が混ざることはありません!」
ソウスケさんはさっそく、リファクタリング後の並列テストコードを実行した。
| |
しかし、ソウスケさんは嬉しそうにしつつも、ふと懸念を口にした。
「でも、これだと config を使うすべてのモジュールを生成するときに、バケツリレーのように毎回 config => $config を手渡しし続けなければならず、コードが煩雑になりませんか?」
シェフはフッと鼻で笑った。
「バケツリレーが嫌なら、システムの入り口(コントローラや起動スクリプト)で一回だけ盛り付けるか、配膳係(DIコンテナ。依存オブジェクトを自動で解決して配ってくれる便利なフレームワークの仕組みだ)を用意して自動で配らせればいい。そんなことより、手渡しを隠して暗黙のグローバル状態にする方が、よっぽど厨房を腐らせる原因になる」
テストランナーの画面には、すべてのテストケースが緑色の ok で埋め尽くされていた。並列で同時に実行しても、テスト同士が干渉して落ちるフレーキーテストは完全に姿を消していた。
「動いた……! テストが一切落ちません! しかも、設定の差し替えが劇的に楽になりました!」 ソウスケさんは立ち上がり、深く頭を下げた。「最高の仕込みです。これで自信を持ってリリースできます!」
シェフはエプロンを外すと、ソウスケさんの肩を叩いた。 「器を分ける(コンテキストを分離する)のが、プロの仕込みだ。しっかりそばを締めて、美味い出前を届けてこい!」
「はい!」 ソウスケさんは晴れやかな笑顔で配送車のドライバーたちに指示を出し始めた。 配送センターからは、冷たく引き締まったそばを載せた配送車が、夏の風を浴びて次々と街へ飛び出していった。私たちの目の前のテーブルに運ばれてきた冷やしそばも、角が立ち、驚くほど冷たく引き締まっていて、最高の喉越しだった。
シェフの仕込み工程表
| 問題(調理ミス) | 技法(パターン) | 効果(仕上がり) |
|---|---|---|
| 共有状態の汚染(Shared State Pollution) Singletonにより設定オブジェクトをグローバル共有した結果、並列テストで状態が互いに書き換わり、テストが落ちていた。 | 依存性注入(DI) Singletonによる暗黙のグローバル参照を排除し、必要な依存オブジェクト(Config)をコンストラクタから明示的に受け取る設計に変える。 | テスト容易性と疎結合の獲得 テストごとに独立した Config インスタンスを生成・注入できるようになり、状態汚染が消滅。モックへの差し替えも容易に。 |
工程
- 暗黙の依存の特定:
各モジュール(
DeliveryZone)の内部で、直接MyConfig->instanceなどの Singleton メソッドを呼び出して依存している箇所を洗い出す - Singleton の解除:
MyConfigクラスからインスタンスのグローバル保持(instanceメソッドなど)を削除し、通常のクラスに戻す - 明示的な属性(引数)の定義:
DeliveryZoneの Moo アトリビュートにhas config => (is => 'ro', required => 1)を追加し、外部から注入される口を用意する - 依存関係の注入(手渡し):
モジュールを new する際、外部(起動スクリプトやコントローラ)から
MyConfigインスタンスを引数として手渡すように呼び出し側を修正する。テストコードでは、テスト用の個別設定を生成して注入する
シェフより
「たった一つの正解」に全員が群がるな。一見便利に見える Singleton も、使い所を間違えれば全員の皿を汚す毒になる。
何がどこに依存しているのか、コソコソ隠さずに表の器に盛り付けろ。外部から明示的に手渡してやる(DI)だけで、設計の風通しは劇的に良くなり、テストの皿も汚れなくなる。器を綺麗に分けることこそが、プロの仕込みってもんだ。
