Featured image of post コード探偵ロックの事件簿【Observer】連続爆破の真犯人〜1箇所直すと3箇所壊れる呪い〜

コード探偵ロックの事件簿【Observer】連続爆破の真犯人〜1箇所直すと3箇所壊れる呪い〜

「1箇所直すと3箇所壊れる」散弾銃手術に苦しむ後輩が、自信過剰な先輩を連れてLCIに駆け込む。コード探偵ロックがObserverパターンで連鎖爆破の呪いを断ち切る!

「先輩、お願いですから、ちゃんと相談してください」

「だから、俺のコードに問題はねーって言ってんだろ」

私の名前はリン。入社2年目のバックエンドエンジニアだ。隣で不機嫌そうに腕を組んでいるのは、7年目の先輩——タツヤさん。

先輩は腕がいい。それは認める。だけど、先輩が書いた在庫管理システムで先週から不可解な現象が起きている。在庫数を更新するコードを1箇所直すと、なぜかメール通知が止まったり、ダッシュボードが白紙になったりする。まるで地雷原を歩いているような日々だ。

「直すたびに別の場所が壊れるんです。お願いです、一度プロに見てもらいましょう」

「プロって……この怪しい事務所がか?」

「レガシー・コード・インベスティゲーション(LCI)」。雑居ビルの2階にあるこの事務所の看板を見上げて、タツヤさんが眉をひそめた。

ドアを開けると、デスクトップPCの排熱とエナジードリンクのにおいが混ざった空気が押し寄せてきた。奥のデスクで、ヨレヨレのトレンチコートを羽織った男がモニターを見つめている。

「おや。珍しいね、2人組の来客か」

男——コード探偵を自称するロックが、回転椅子をくるりと回してこちらを向いた。

「あの、こちらで——」 「うちのコードに問題はない。帰るぞリン」

タツヤさんが踵を返そうとした瞬間、ロックが薄く笑った。

「ほう。『問題ない』と断言するエンジニアほど、重症の現場を抱えているものだよ。まあ座りたまえ、ワトソン君たち」

「ワトソン? 誰がワトソンだ。俺はタツヤだ」

「いい名前だね、ワトソン君」

タツヤさんの眉間のシワが深くなった。私は先輩の袖を引いて、強引にソファに座らせた。


現場検証:散弾銃の痕跡

「それで、何が起きているんだい?」

「先輩が書いた在庫管理の StockManager なんですが」私はノートPCを開いた。「在庫数を更新する update_stock というメソッドがあって——」

「俺が書いた」タツヤさんが胸を張る。「メール通知もログも、ダッシュボードの更新も、全部ここに集約してある。効率的だろ?」

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#!/usr/bin/env perl
use v5.34;
use warnings;
use utf8;
use feature 'signatures';
no warnings "experimental::signatures";

package StockManager {
    use Moo;

    sub update_stock ($self, $item, $quantity) {
        # 在庫の更新
        my $message = "Stock updated: $item => $quantity";

        # メール通知
        my $email = $self->_send_email($item, $quantity);

        # ログ記録
        my $log = $self->_write_log($item, $quantity);

        # ダッシュボード更新
        my $dashboard = $self->_refresh_dashboard($item, $quantity);

        return {
            message   => $message,
            email     => $email,
            log       => $log,
            dashboard => $dashboard,
        };
    }

    sub _send_email ($self, $item, $quantity) {
        return "[EMAIL] $item is now $quantity";
    }

    sub _write_log ($self, $item, $quantity) {
        return "[LOG] $item: quantity changed to $quantity";
    }

    sub _refresh_dashboard ($self, $item, $quantity) {
        return "[DASHBOARD] Refreshed: $item ($quantity)";
    }
}

ロックはモニターに映し出されたコードを3秒ほど見つめ、それからエナジードリンクを一口飲んだ。

「……ほう」

「な? 一箇所にまとまってて、読みやすいだろ」タツヤさんが得意げに言う。

ロックは首を振った。

「いいや。このメソッドは3つの仕事を知りすぎている。彼は在庫を更新するだけでなく、メールの送り方も、ログの書き方も、ダッシュボードの更新方法も——全部を自分で抱え込んでいる」

「それの何が悪い?」

ロックはエナジードリンクの缶を口元から離して、タツヤさんをちらりと見た。それからゆっくりと、私の方に向き直った。

「ワトソン君——正直な方の。先週、何があった?」

「正直な方って何だよ」タツヤさんが声を上げたが、ロックは完全に無視している。

私は——まあ、正直な方で間違いないので——答えた。

「Slack通知の機能を追加したんです。update_stock の中に _send_slack を書き足したら……メール通知が止まりました」

タツヤさんの頬がわずかに引きつった。

「……あれはリンの書き方が悪かっただけだ」

「でも先輩、その前にも、ダッシュボードのフォーマットを変えたとき、ログの書式まで変わってしまいましたよね?」

沈黙。

ロックが不敵な笑みを浮かべた。

「1発撃つと散弾が四方に飛ぶ。これは散弾銃手術(Shotgun Surgery)のにおいだよ、ワトソン君たち。1箇所の変更が、関係のない場所まで巻き添えにする。そして被害は、修正のたびに広がっていく」


推理披露:鮮やかなリファクタリング

「解決策は——監視網を張ることだ」

ロックがキーボードに手を伸ばした。タツヤさんが身を乗り出す。

「監視網? 俺のコードを監視するつもりか?」

「いいや。君のコードを解放するんだよ。StockManager は今、全員の面倒を見る過保護な親だ。これを『変化を叫ぶだけの発信者』に変える」

1. Observer の約束事

「まず、在庫の変化に反応する側が守るべきルールを定義する。名前は StockObserver::Role ——つまり、『在庫の変化を聞いたら何かするヤツ』の共通インターフェースだ」

1
2
3
4
5
6
7
# -----------------------------------
# 1. Observer Role(共通インターフェース)
# -----------------------------------
package StockObserver::Role {
    use Moo::Role;
    requires 'on_stock_updated';
}

requires ……?」タツヤさんが目を細めた。

「『このRoleを使うなら、on_stock_updated というメソッドを必ず実装しろ』という契約だよ。守らなければ、即座にエラーになる」

2. 反応する者たち

「次に、メール通知、ログ記録、ダッシュボード更新——それぞれを独立した Observer として切り出す」

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# -----------------------------------
# 2. 具体的な Observer 群
# -----------------------------------
package StockObserver::Email {
    use Moo;
    with 'StockObserver::Role';

    sub on_stock_updated ($self, $item, $quantity) {
        return "[EMAIL] $item is now $quantity";
    }
}

package StockObserver::Logger {
    use Moo;
    with 'StockObserver::Role';

    sub on_stock_updated ($self, $item, $quantity) {
        return "[LOG] $item: quantity changed to $quantity";
    }
}

package StockObserver::Dashboard {
    use Moo;
    with 'StockObserver::Role';

    sub on_stock_updated ($self, $item, $quantity) {
        return "[DASHBOARD] Refreshed: $item ($quantity)";
    }
}

「ちょっと待て」タツヤさんが割り込んだ。「これ、俺が書いたコードをバラバラにしただけじゃないか? それぞれが独立して動けるわけがない」

「まあ、最後まで見たまえ」

3. Subject——変化を叫ぶ者

「そして主役の StockManager だ。彼はもう、メールの送り方もログの書き方も知らなくていい。ただ『在庫が変わったぞ!』と叫ぶだけでいい」

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# -----------------------------------
# 3. Subject(通知の発信元)
# -----------------------------------
package StockManager {
    use Moo;

    has observers => (
        is      => 'ro',
        default => sub { [] },
    );

    sub add_observer ($self, $observer) {
        die "Invalid observer" unless $observer->DOES('StockObserver::Role');
        push @{$self->observers}, $observer;
        return $self;
    }

    sub remove_observer ($self, $observer) {
        @{$self->observers} = grep { $_ != $observer } @{$self->observers};
        return $self;
    }

    sub notify ($self, $item, $quantity) {
        my @results;
        for my $obs (@{$self->observers}) {
            push @results, $obs->on_stock_updated($item, $quantity);
        }
        return @results;
    }

    sub update_stock ($self, $item, $quantity) {
        my $message = "Stock updated: $item => $quantity";

        # Observer への通知 — これだけ!
        my @notifications = $self->notify($item, $quantity);

        return {
            message       => $message,
            notifications => \@notifications,
        };
    }
}

「…………」

タツヤさんが黙った。

Observerパターンの概念図。中央のSubjectが通知信号を発信し、メール・ログ・ダッシュボードの各Observerが独立して反応する、フラットでモダンなTechデザイン。

私は画面を凝視していた。あの update_stock の中にぎっしり詰まっていたメール送信もログ記録もダッシュボード更新も——全部消えている。代わりにあるのは、たった1行の $self->notify(...) だけだ。

「あの……ロックさん。これって、新しい通知を追加したいときは——」

「ああ。たとえば Slack 通知を追加したければ、こうするだけだ」

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# Slack通知の追加
package StockObserver::Slack {
    use Moo;
    with 'StockObserver::Role';

    sub on_stock_updated ($self, $item, $quantity) {
        return "[SLACK] $item: $quantity";
    }
}

# 登録するだけ。既存コードには一切触れない。
my $manager = StockManager->new;
$manager->add_observer(StockObserver::Email->new);
$manager->add_observer(StockObserver::Logger->new);
$manager->add_observer(StockObserver::Dashboard->new);
$manager->add_observer(StockObserver::Slack->new);    # 追加!

$manager->update_stock('Keyboard', 150);

「既存の StockManager を1行も書き換えずに……!」

在庫が変わったら『変わったぞ』と叫ぶだけ。誰が聞いているかは知らなくていい。 これが Observer パターンという名の推理の切り札だよ、ワトソン君たち」


事件の終わり:静かになった現場

テストが全てグリーンに変わった。Before版とAfter版の出力は完全に一致し、Observer の追加・削除も思い通りに動いている。

タツヤさんは黙ったまま画面を見つめていた。それから、ぼそりと言った。

「……リン」

「はい」

「次からは、こう書く」

私は少し驚いた。先輩は自分のコードを否定されるのが一番嫌いなはずだ。だけど、今のタツヤさんの目には、悔しさよりも納得の色が浮かんでいた。

「先輩——」

「勘違いするなよ」タツヤさんが視線を逸らした。「探偵に負けたんじゃない。俺のコードが進化しただけだ」

ロックは涼しい顔でコートの襟を立てた。

「さて、報酬の話をしようか。散弾銃のように炸裂するスパイスのカレーを一皿、いただきたいのだが」

「……探偵、お前のコードの腕は認めてやる。だがカレーの味は保証しねーぞ」

タツヤさんの口調は不機嫌そうだったが、表情は少しだけ柔らかくなっていた。

事務所を出るとき、私はふと振り返った。ロックはすでに次のモニターに向かっている。散弾銃——あちこちに飛び散る修正の恐怖。あの爆発の連鎖が、たった1つのパターンで静まり返るとは思わなかった。

「先輩、あのObserverのコード、今度ちゃんと勉強会で共有しましょうね」

「……お前、ほんとに真面目だな」

褒め言葉として受け取っておくことにした。


探偵の調査報告書

容疑(アンチパターン)真実(パターン)証拠(効果)
散弾銃手術(Shotgun Surgery)。1箇所の変更があちこちに波及し、修正するたびに別の場所が壊れる。Observer パターン。状態の変化を「通知」として一元化し、関心のあるオブジェクトが自動的に反応する仕組み。疎結合と拡張性。新しい通知手段の追加は Observer を1つ実装して登録するだけ。既存コードには一切触れない。

推理のステップ

  1. Observer Role の定義: 「在庫の変化に反応する者」が守るべき共通インターフェース(on_stock_updated)を定める。
  2. 具象 Observer の実装: メール通知、ログ記録、ダッシュボード更新をそれぞれ独立したクラスとして切り出す。
  3. Subject(StockManager)の再構築: Observer の登録(add_observer)・削除(remove_observer)・通知(notify)を持たせ、update_stock からは notify を呼ぶだけにする。

ロックより

散弾銃の恐ろしさは、狙った場所だけでなく、広範囲に被害が及ぶことだ。君たちのコードも同じだった——1箇所を直すたびに、予想もしない場所で爆発が起きていた。

Observer は「変化の波及」を制御するための監視網だ。変化を恐れる必要はない。ただし、その変化が誰に届くべきかを、コード自身に判断させたまえ。

それから、ワトソン君——不器用な方の。君の腕は確かだ。ただ、腕が確かな人間ほど、一人で全てを抱え込みがちなものだよ。次は誰かに「任せる」勇気を持つことだ。

comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。