「コードが、時限爆弾なんですよね、たぶん」
その言葉を聞いたとき、私はカウンター席でノートを読み返していました。私はこの「コード食堂」で見習いとして働く、駆け出しのエンジニアです。前の2件の仕込み直しで書き留めたメモを眺めていたのですが、「attach、detach、notify」という言葉をどこかで聞いた気がして探していたところでした——そんな言葉はどこにもなかったのですが。
この記事で学ぶこと
この記事は、注文ステータスが変わるたびに焼き場・揚げ場・ドリンクバーへ手動で逐次通知するコードを、Observerパターンで整理する話です。Perlのコードを少しずつ仕立て直していきます。
| 学ぶこと | ひとことで言うと |
|---|---|
| Observerパターン | 変化を見張る側が、Subject(被観察者)の状態変化を自動で受け取る仕組み |
| 密結合の手動通知 | Subject が通知先全員の名前を知っており、追加のたびに Subject を修正する問題 |
Moo::Role の requires | 「このメソッドを持つことを約束しろ」という型紙を定義する方法 |
attach / detach | Observer が自ら登録・解除することで、Subject は誰が聞いているかを知らなくてよくなる仕組み |
| 開放閉鎖原則(OCP) | 既存をいじらず、追加だけで機能を増やせる状態 |
対象読者は、次のような人を想定しています。
- PerlとMooの基本(
has、new、with)がなんとなく分かる - 通知先が増えるたびに同じ箇所を直し続けるコードに、なんとなく不安を感じたことがある
- 第1作のStrategyパターン・第2作のStateパターンを読んで、「次は何が来るのか」と気になっている
技術スタックは Perl / Moo です。コードはすべて手元で動かし、テストが通ることを確認しています。なお本文中のモジュールは要点を抜き出して示しているため、実際にファイルへ保存するときは Perl の作法として末尾に 1; を加えてください。
今日の持ち込み素材
開店前の静かな店内に、入ってきたのは同年代に見える女性でした。スーツではなく、シャツと細身のパンツという実務的な格好。視線が合うと軽く会釈して「——予約はしていないんですが」と言いました。「どうぞ」と伝えてシェフを呼びに行こうとすると、シェフはすでに厨房から出てきていました。音で気づいていたようでした。
お客さんが椅子に座り、ノートPCを開いてから、最初に言った言葉が冒頭のものです。「コードが、時限爆弾なんですよね、たぶん」。
シェフが一瞬だけ手を止めました。「——まだ爆発していないのに来た?」とシェフが言うと、お客さんが「はい」と短く答えました。シェフは短い間を置いてから「珍しいな」と独り言のように言い、「見せてもらえますか」と続けました。
お客さんがスクリーンを向けました。スタートアップで飲食店向けのオーダー管理システムを開発しているのだそうです。注文のステータスが変わるたびに、厨房の各ステーション——焼き場、揚げ場、ドリンクバー——へ通知するコードを書いた。動いている。でも先週、バースタンドを追加したとき、通知の呼び出しを3か所すべてに書き忘れそうになった。テストで気づいたから事なきを得たけれど。「次のステーションを足すたびにこれをやるのかと思ったら、怖くなって」。
私は「壊れた後で来る人」しか見ていなかった。「壊れる前に来る人がいるんだ」と思ったが、うまく言葉にはできなかった。
コードはこういうものでした。
| |
「動いています、今は」とお客さんは言い足しました。
素材を見る目
シェフがコードをしばらく見ていました。無言でした。それから、カウンターに置いていたペンを持ち、コードのある1か所を指しました。
「ここが、全員の名前を持ってますね」
お客さんが「そうです。今は3つです。来月、配膳モニターも繋ぐ予定で——増えます」と答えました。
「足すたびに、ここを直すことになる」とシェフが言いました。お客さんが「直し忘れたら、通知が届かない」と引き取ると、シェフはただ「そう」と言いました。
一瞬、シェフがなぜか私のほうを見ました。「料理で言ったら、どういうことだと思う?」
思いがけず問われて、私は少し考えました。前の2件の仕込み直しで、なんとなく比喩の当て方がわかってきた気がしていたので。「——走って伝えに行く、みたいな感じですか?注文が変わるたびに、焼き場に走って、揚げ場に走って」。
シェフは何も答えませんでした。ただ、返事の代わりにコンロに火をつけました。正解だったのか外れだったのか、わかりませんでした。でも、シェフが続きを始めたのでそれでよかったのだと思いました。
なぜ、これは壊れやすいのか
このコードのアンチパターンは、密結合の手動通知チェーンです——Subject(状態変化を起こす側)が、通知先全員の名前を直接知っている状態のことです。
具体的に言うと、OrderManager はいま grill_station・fryer_station・drink_bar の3つを属性として持ち、update_status の中でそれぞれを手で呼んでいます。
新しいステーション(たとえばバースタンド)を追加するとき、必ず2か所を変更しなければなりません。
has bar_station => ...という属性をOrderManagerに追加するupdate_statusの中に$self->bar_station->on_order_update(...)を書き加える
どちらか1か所でも忘れれば、バースタンドには通知が届きません。しかもこの「通知漏れ」は実行時になるまで発覚しません。
問題の本質は「変更箇所が2つに分散していること」ではなく、「変更の必要が OrderManager に生まれること」そのものです。新しいステーションを追加するという作業が、通知先とは関係のない OrderManager の改修を引き起こす。これが密結合の症状です。
仕込み直し
シェフがカウンターの隅のメモ帳を引き寄せて、書き始めました。お客さんが前のめりで見ています。私もつい後ろから覗き込みました。
最初に書いたのはこれでした。
| |
お客さんが「requires 'update' というのは——?」と聞きました。
シェフは書く手を止めないまま、「これを持っていることを約束しろ、という印。持っていなければ、with 'OrderObserver' した時点で怒る」と言いました。
資格証みたいなもの——と思った。料理で言うと、食品衛生責任者の資格がなければ厨房に立てない、みたいな。正しいかどうかは確認できなかったが、なんとなく腑に落ちた。
次にシェフが書いたのが、新しい OrderManager でした。
| |
Before の OrderManager と比べると、has grill_station・has fryer_station・has drink_bar がすべて消え、代わりに _observers というリストだけが残っています。update_status の中身も、「全員の名前を呼ぶ」ではなく「リストにある誰かの update を呼ぶ」に変わっています。
そして各ステーションは、こうなります。
| |
使う側では、こうなります。
| |
核心の問い
シェフがペンを置いたとき、お客さんが少し前のめりのまま聞きました。
「でも——attach を呼ぶのは誰かがやらなきゃいけない。それって、結局どこかが全員を知っている、という状態じゃないですか?」
シェフが向き直りました。感情を出さない顔でしたが、一拍置いてから答えました。
「attach を呼ぶのは——各ステーションの方です。OrderManager が GrillStation の名前を知るんじゃなく、GrillStation が自分から attach を呼ぶ。新しいステーションを追加するときは、新しいクラスを書いて、その中で attach を1行書く。OrderManager のコードは、触らなくていい」
お客さんがしばらく止まりました。それからゆっくりと「——変更の場所が変わる、ということですか」と言いました。
シェフが「そう」と言いました。
なぜそれで問題が消えるのか
ここがこのパターンの核心です。
Before のコードでは、「バースタンドを追加する」という変更が OrderManager の中に入り込んでいました。OrderManager はバースタンドのことを知っていなければならなかったからです。
After のコードでは、OrderManager は誰が _observers のリストに入っているかを知りません。知っているのは「update メソッドを持つ誰か」だということだけです。バースタンドを追加するとき、OrderManager のコードは1行も変わりません。
| |
「変更の場所が変わる」というお客さんの言葉は正確でした。変更は「新しいステーションを書く場所」だけに限定される。これが、密結合が解消されたということです。
また、with 'OrderObserver' を書いて update を実装し忘れた場合は、クラスを定義した時点でエラーになります。通知漏れのリスクが「実行時の沈黙」から「定義時のエラー」に変わるのも、Moo::Role の requires の効果です。
構造の変化を図で見る
Before: 密結合(OrderManager が全員を知っている)
classDiagram
class OrderManager {
+grill_station
+fryer_station
+drink_bar
+update_status(order_id, status)
}
class GrillStation {
+on_order_update(order_id, status)
}
class FryerStation {
+on_order_update(order_id, status)
}
class DrinkBar {
+on_order_update(order_id, status)
}
OrderManager --> GrillStation : 直接呼ぶ
OrderManager --> FryerStation : 直接呼ぶ
OrderManager --> DrinkBar : 直接呼ぶ
After: Observer パターン(OrderManager は Observer を知らない)
classDiagram
class OrderObserver {
<<Moo::Role>>
+update(order_id, status)*
}
class OrderManager {
-_observers
+attach(observer)
+detach(observer)
+update_status(order_id, status)
}
class GrillStation {
+update(order_id, status)
}
class FryerStation {
+update(order_id, status)
}
class DrinkBar {
+update(order_id, status)
}
OrderObserver <|.. GrillStation : with
OrderObserver <|.. FryerStation : with
OrderObserver <|.. DrinkBar : with
OrderManager o--> OrderObserver : notify
GrillStation ..> OrderManager : attach
FryerStation ..> OrderManager : attach
DrinkBar ..> OrderManager : attach
依存の矢印の向きが変わっていることに注目してください。Before では OrderManager が各ステーションへ向かって矢印が伸びていました。After では、各ステーションが OrderManager に向かって「自分を登録する」矢印になっています。これが「変更の場所が変わる」の構造的な意味です。
試食合格
テストを実行しました。全項目グリーンでした。お客さんが画面を見て、静かに「これで、新しいステーションを追加しても OrderManager は変えなくていい」と、確認するように言いました。
シェフが「BarStation を作って、attach する。それだけ」と言いました。
お客さんが「このパターンに名前はありますか」と聞きました。
「Observerパターン——観察するものの設計パターン、とでも言うか。変化を見張る側が、Subject(被観察者)の状態変化を自動で受け取る」とシェフが答えました。
お客さんが礼儀正しく礼を言い、PCを閉じました。立ち上がりかけて、一瞬手を止めて「——最初からこう書けばよかった」と言いました。
「最初からは難しい。大事なのは、次から書けることだ」とシェフが言いました。
お客さんは小さくうなずいて出ていきました。ドアが閉まった後、しばらく店内が静かでした。
「最初からは難しい」——シェフは、お客さんのことを言った。でも、なぜか私のことを言われた気がした。私は今日まで、ここで見たことをメモするだけだった。今日も、メモするだけだった。——でも、シェフがさっき私に問いを向けたとき、自分なりの比喩が出てきた。「走って伝えに行く」。合っていたのかはわからない。でも、何か聞けばよかった気がした。なんと聞けばよかったのかは、まだわからないのだけれど。
シェフの仕込み工程表
| 問題(調理ミス) | 技法(パターン) | 効果(仕上がり) |
|---|---|---|
| Subject が通知先全員の名前を直書きしている | Observerパターン(requires 'update' ロール) | Subject は通知先を知らない。notify するだけでよい |
| 通知先を追加するたびに Subject を2か所改修する | Observer が attach で自ら登録する | 変更箇所が新しいクラスのみに限定される |
| 通知漏れが実行時まで発覚しない | Moo::Role の requires で update の実装を強制 | ロール使用時点(クラス構築時)にエラーになる |
工程
- Observer のインターフェースを Moo::Role で定義する。
requires 'update'を書く - Subject(通知元)から通知先の属性(
has grill_station等)をすべて取り除く。代わりに_observersリストだけを持つ - Subject に
attach/detach/update_status(各updateを呼ぶ)を実装する - 各 ConcreteObserver に
with 'OrderObserver'を追加し、updateを実装する - 利用側で Observer を生成し、
attachで Subject に登録する - 新しい Observer を追加するときは、新しいクラスを書いて
attachを呼ぶだけ。Subject のコードには触らない
シェフより
厨房に注文票を貼るピンチがある。注文が変わったら、票を差し替えるだけだ。焼き場も揚げ場も、自分で票を確認しに来る——誰かが走って伝えに行く必要はない。Observerパターンは、そういう仕込みだ。Subject は「変わった」と告げるだけでいい。誰が聞いているかは、聞く側が決める。
