昼前の厨房は静かだった。
シェフが明日の仕込みをしていた。私はそのそばで、野菜の下ごしらえを手伝いながら、包丁の音を聞いていた。
引き戸が開いた。
38歳ほどの男性が入ってきた。スーツではなく、きれいなシャツ姿。「紹介いただいた者です」と言った。落ち着いた話し方だった。「キッチン管理のシステムを書いているんですが、設計の問題がありまして——」
シェフが包丁を置かずに「見せろ」と言った。
男性がPCをカウンターに置いて開いた。GrillStation・FryStation・DrinkCounter——3つのクラスが並んでいる。
GrillStation の has を見た。fryer と drink_counter。FryStation の has を見た。grill と drink_counter。DrinkCounter の has を見た。grill と fryer。
「——全員が全員を知っている」と私は言った。
この記事で学ぶこと
この記事は、「キッチン管理システムの各ステーションが互いのオブジェクトを直接保持し、ステーションを1つ追加・変更するたびに全クラスを修正しなければならない」という問題を、Mediatorパターンで整理する話です。全員が KitchenCoordinator(仲介役)だけを知るスター型構造へ直します。
| 学ぶこと | ひとことで言うと |
|---|---|
| Mediator パターン | 複数のコンポーネントが互いを直接知っている代わりに、全員が1つの仲介者(Mediator)だけを知る。仲介者がすべての通信を制御する |
| n-to-n-coupling | コンポーネント同士が相互参照している状態。コンポーネント数がNのとき、参照の数は最大N×(N-1)。1つを変えると全員に波及する |
| Moo での実装 | MediatorクラスがすべてのコンポーネントをMooの has で保持し、notify メソッドでイベントを受け取って転送する。各コンポーネントは has coordinator だけを持つ |
| 複雑さの集中 | 変更の波及をN対NからN対1に変える。複雑さを消すのではなく、1か所に集める |
対象読者は、次のような人を想定しています。
- PerlとMooの基本(
has、new)がなんとなく分かる - クラスを追加したとき、既存のクラスをいくつも修正した経験がある
技術スタックはPerl / Mooです。コードはすべて手元で動かし、テストが通ることを確認しています。
ステーションを1つ触ると全員に波及する
シェフが「どうなっている?」と男性に聞いた。
「最初は3つのステーションで動いていました」と男性は言った。「GrillStation(焼き場)と FryStation(揚げ場)と DrinkCounter(ドリンクカウンター)です。今年の春、SaladStation(サラダ場)を追加しようとして——既存の3クラスすべてを修正する羽目になりました。それ自体は仕方ないと思っていたんですが、先月 DrinkCounter の動作を少し変えたとき、GrillStation と FryStation の両方に影響が出て。今はステーションを1つ触るたびに、全員を確認しないといけない状態で」
コードを見た。Before の構造はこうなっていた。
| |
シェフが「n-to-n-coupling だ」と言った。「コンポーネントの数がNのとき、参照の数は最大N×(N-1)になる。1つを変えると、それを知っているすべてを確認する必要がある」
男性が「そうです——まさにそれです」と言った。問題に名前がついた、というような顔をした。
セットアップのコードも問題を示していた。GrillStation を1つ動かすだけでも、FryStation と DrinkCounter のインスタンスを用意しなければならない。テストすら全員が必要だった。
| |
SaladStation を追加するとどうなるか。GrillStation に has saladstation を追加して order_ready で呼び出す。FryStation に has saladstation を追加する。DrinkCounter に has saladstation を追加する。3クラスすべてを修正する。それが n-to-n-coupling の実態だった。
ホール担当を立てる
シェフがホワイトボードに向かった。
「全員がコーディネーター——ホール担当だけを知る。ホール担当がすべての連絡を受けて、必要なところへ伝える」
KitchenCoordinator クラスを書いた。全ステーションを has で保持し、notify でイベントを受け取る。
| |
各ステーションから他ステーションへの参照が消えた。代わりに has coordinator だけが残る。
| |
構築の順序は coordinator を先に作り、ステーションを coordinator を渡して作り、最後に coordinator にステーションを設定する。
| |
「これで、SaladStation を追加するときは KitchenCoordinator の notify だけを修正すればいい」と私は言った。「既存の3クラスには手を触れない」
男性が「でも——」と言った。
「SaladStation を追加したら、結局 KitchenCoordinator を修正する必要がある。それはN対N依存と同じではないですか?」
私は一拍置いた。
「違います——ステーションはお互いをもう知らない。焼き場を変えても、揚げ場とドリンクカウンターには影響しない。SaladStation を追加しても、既存のステーションは $self->coordinator->notify(...) を呼ぶだけで——誰が増えたかを知らなくていい。KitchenCoordinator が"誰と誰が話すか"の情報を全部引き受けています。変更は1か所だけです」
男性が「ああ——なるほど」と言った。既存のステーションに触れなくていいことが腑に落ちた顔をした。
シェフが「Mediatorパターン」と言った。
「複数のコンポーネントが互いを直接知っている代わりに、全員が1つの仲介者(Mediator)だけを知る。仲介者がすべての通信を制御する。コンポーネント間の依存がN対NからN対1のスター型に変わる。MooではMediatorクラスが各コンポーネントを has で保持し、notify メソッドがイベントを受け取って転送する」
男性が「なるほど。では——」と続けた。
「KitchenCoordinator にロジックが集中しませんか?ステーションが増えるたびに KitchenCoordinator が大きくなっていく——今度は KitchenCoordinator が神オブジェクトになるのでは?」
私は少し考えた。
「そうです——複雑さを消しているのではなく、1か所に集めています。N対N直接参照では、変更が全コンポーネントに波及する。Mediatorにすると、変更は KitchenCoordinator だけを変えればいい。それはコントロールしやすい」
「ただ——どこまで大きくなったら、KitchenCoordinator を分割するべきか——それは、まだ言えません」
シェフが何も言わなかった。
男性が「分かりました。確かに、波及を抑えるのが先決ですね。試してみます」と言った。少し考える顔をした。
FacadeやObserverとの違い
同じく「一か所に集める」構造のパターンと混同しやすい。
| パターン | 目的 | 依存の向き |
|---|---|---|
| Facade | 複数サブシステムの窓口を一本化(呼び出し元を簡略化) | 外側から内側へ(一方向) |
| Observer | イベント発行・購読(Pub/Sub)。発行者は購読者の種類を知らない | 発行者→購読者(1対N通知) |
| Chain of Responsibility | 複数ハンドラが連鎖して「誰が処理するか」を決める | 順送り |
| Mediator | コンポーネント間の通信を集中制御。N対N依存をスター型に変える | 全員→仲介者→転送先(スター型) |
Facadeは「呼び出す側」を簡略化する。Mediatorは「コンポーネント同士の相互参照」をなくす。目的が異なる。Observerは発行者が購読者の数・種類を知らないが、Mediatorでは仲介者が全コンポーネントを知っている(それが集中のトレードオフでもある)。
試食合格
テストを走らせた。
Before(n-to-n-coupling):
| |
テスト9〜11番——3クラスすべてに手を入れなければ SaladStation は追加できない。これが n-to-n-coupling の具体的なコストだ。
After(Mediatorパターン):
| |
全テスト通過、警告なし。
テスト1〜9番——GrillStation・FryStation・DrinkCounter は coordinator だけを持ち、互いへの直接参照がない。テスト12〜16番——SaladStation を追加した後も既存ステーションは変わらない。GrillStation に salad_station 属性はない。FryStation にも DrinkCounter にもない。KitchenCoordinator だけを変えた。
男性が「既存のステーションを変えなくていいんですね」と言った。少し表情が明るくなった。
「そうです。変更が1か所に集まる」と私が言った。
シェフの仕込み工程表
| 問題(調理ミス) | 技法(パターン) | 効果(仕上がり) |
|---|---|---|
コンポーネント同士が互いを直接 has で保持・呼び出し(n-to-n-coupling)。ステーション追加・変更のたびに全コンポーネントを修正する必要がある | Mediatorパターン:KitchenCoordinator が全ステーションを保持し、notify でイベントを仲介する | 各ステーションは coordinator だけを知る。変更の影響が KitchenCoordinator に集中し、既存ステーションへの波及がなくなる |
工程
- コンポーネント間の参照関係を洗い出す。何が何を
hasで知っているかをリストアップする KitchenCoordinator(Mediator)クラスを作る(use Moo)has grill・has fryer・has drink_counterなど、全コンポーネントをMediatorの属性として保持する(is => 'rw')notify($from, $event, $order_id)メソッドに、イベントに応じた転送ロジックを集める- 各コンポーネントから他コンポーネントへの直接参照(
has fryer・has grillなど)を削除する - 代わりに
has coordinator => (is => 'ro', required => 1)を追加する - コンポーネントが他コンポーネントを呼ぶ代わりに
$self->coordinator->notify(...)を呼ぶ - 構築の順序: coordinatorを先に作り(ステーション未設定)、ステーションを
coordinatorを渡して作り(ro/required)、最後にcoordinatorにステーションを設定(rw)
シェフより
「ステーションは仕事をするだけでいい。誰に声をかけるかは——コーディネーターが決める。全員が全員を知っている必要はない。声をかける相手が1人なら、追加も変更も、その1人を直せばいい」
男性が帰った後、シェフが厨房の作業に戻った。
私は——答えた。「ステーションはお互いをもう知らない。変更は1か所に集まる」。男性は納得してくれた。
でも、シェフは何も言わなかった。
ep15のとき——「お前が答えろ」と言われて、最後まで言えた。シェフが頷いた。今日は——頷かなかった。
答えを言えることと、シェフが頷くことは、違うことなのかもしれない。
「どこまで大きくなったら、KitchenCoordinator を分割するべきか」が言えなかった。そこが——足りなかった部分だろうか。それとも——別のどこかが?
分からなかった。
