Featured image of post コードドクター【Mediator】全身感染症候群〜仲介者の処方箋〜

コードドクター【Mediator】全身感染症候群〜仲介者の処方箋〜

「もうダメだ」

俺は付箋だらけのホワイトボードの前で頭を抱えた。赤いマーカーで殴り書きされた「Delivery対応TODO」の文字列が、まるで血文字のように見える。

モニター3台に映るPerlのコード。そのどれもが、どこかしらで赤い波線を吐いている。チャットツールには同僚のタカシから「デリバリー対応いつ終わる?」のメッセージが3回連続で来ていた。

俺——仲村翔太、32歳。飲食テック系スタートアップでPM兼エンジニアをやっている。元ホールスタッフ。包丁は握れるがキーボードの方が最近は速い。自信はあった。人間関係のトラブルなら何でも仲裁できる。キッチンとホールが揉めたら俺が間に入る。バイトの新人が泣いたら俺が話を聞く。チームの潤滑油。それが俺だ。

——のに。

コードになると、なんでこうなるんだ。

新しいセクション「デリバリー」を追加しようとしただけだ。たったそれだけのはずだった。なのに Waiter.pm を修正し、Kitchen.pm を修正し、Bar.pm を修正し、Cashier.pm を修正し——もう何が壊れて何が直ったのかわからない。

コンコンコン。

ドアをノックする音がした。タカシが催促に来たかと思って勢いよく開けたら、見知らぬ男が立っていた。

無表情。黒い鞄。白衣ではないが、どこか医者っぽい雰囲気。その隣には、にこやかに微笑む助手らしき人が控えている。

「どちら様ですか? 営業でしたら今ちょっと——」

「大丈夫ですよ、ここはコード診療所です……あ、いえ、往診ですね」

助手が柔らかく遮った。名札には「ナナコ」と書いてある。

男はこちらの許可を待たず、モニターを一瞥し、すでに部屋の中に入っていた。

……なにこの人。不審者?

でもナナコさんがいるから大丈夫なのか——という判断基準もどうかと思いつつ、俺はドアを閉めた。

触診

男——ドクターと呼ぶらしい——は俺のデスクに近づくと、腕を組んでモニターを見つめ始めた。

沈黙。

何か言ってくれないと不安になるんだが。

「あの、これ、レストランのオーダー管理システムなんですよ」

俺は場を持たせるように喋り始めた。

「俺、元々ホールスタッフだったんで、リアルなフローがわかってて。注文をウェイターが受けて、キッチンに伝えて、バーにドリンク注文伝えて、会計に追加して——」

ドクターは微動だにしない。キーボードにも触れず、ただコードをスクロールしている。

「あの……先生? 聞いてます?」

ドクターがモニターを指差した。

「全身感染だ」

「え?」

ナナコさんが穏やかに歩み寄ってきた。

「各モジュールが全て直接繋がっていて、1つを動かすと全部に影響が出る状態ですね。お料理で言うと、一つの食材を変えただけで全メニューのレシピを書き直さなきゃいけない状態です」

「あー! それそれ! デリバリーを追加しようとしたら4ファイル全部触らなきゃいけなくて……」

ドクターがエディタの use 文を指でなぞった。Waiter.pm の冒頭——

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package Waiter;
use v5.36;

sub new($class, %args) {
    return bless {
        name     => $args{name} // 'ウェイター',
        kitchen  => $args{kitchen},
        bar      => $args{bar},
        cashier  => $args{cashier},
        # TODO: デリバリー対応... ここにも追加しなきゃ...
    }, $class;
}

そして Kitchen.pmBar.pmCashier.pm も全て同じように、互いのオブジェクト参照を抱え込んでいる。

「20本」

ドクターがぼそりと呟いた。

ナナコさんがホワイトボードに軽く線を引き始めた。5つのモジュールが互いに全方向で結ばれた図。

「5つのモジュールが互いに全部参照し合っているので、接続が5×4で20本になっているんです。これが全身感染……N×N依存症候群ですね」

俺はその図を見て、ようやく全体像が見えた気がした。スパゲッティだ。しかも麺が20本絡まっている。

診断

ドクターが process_order メソッドを映し出した。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
sub take_order($self, $order) {
    if ($order->{type} eq 'food') {
        $self->{kitchen}->prepare($order);
    }
    elsif ($order->{type} eq 'drink') {
        $self->{bar}->prepare($order);
    }
    elsif ($order->{type} eq 'both') {
        $self->{kitchen}->prepare($order);
        $self->{bar}->prepare($order);
    }

    # 会計にも直接通知
    $self->{cashier}->add_to_bill($order);
}

「癒着」

一言だった。

「このメソッド、キッチン・バー・会計と3つのモジュールを直接呼び出していますよね」ナナコさんが補足する。「臓器同士が癒着している状態です。1つの臓器を取り出したくても、全部を一緒に動かすしかありません」

「いや、でも全部必要なんですよ。ウェイターがキッチンに伝えて、バーにもドリンク注文伝えて——」

「仲村さん」

ナナコさんが少し前のめりになった。

「普段のお仕事で、キッチンとホールの間で揉め事があったらどうされます?」

唐突な質問だった。

「え? 俺がまとめるに決まってるじゃないですか。両方の話を聞いて、必要な情報だけ伝えて、全員が自分の仕事に集中できるようにしますよ」

ナナコさんが微笑んだ。

それですよ。あなたが普段やっていること、そのものです

「…………えっ?」

「キッチンがホールに直接怒鳴るよりも、間にマネージャーが入ったほうがスムーズですよね? コードも同じです。全員が全員に直接話しかけるのではなく、間に1つ仲介者を置くんです」

目からウロコが落ちた。

比喩じゃない。本当に視界がクリアになった気がした。

ナナコがホワイトボードのN×N依存図を指し示し、仲村が目を見開く瞬間。ドクターは腕を組んで静かに頷いている

「あー!! 俺がやってること! オーダーマネージャーみたいなやつを……コードにも置けばいいのか!」

ドクターが小さく頷いた。

「そうだ」

外科手術

ドクターが俺のキーボードに手を伸ばした——と思ったら、鞄から自分のキーボードを取り出して接続し始めた。こだわりがあるらしい。

まず新しいファイル OrderMediator.pm を作り始める。

 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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package OrderMediator;
use v5.36;

sub new($class, %args) {
    return bless {
        sections => {},
    }, $class;
}

# セクションを登録する
sub register($self, $name, $section) {
    $self->{sections}{$name} = $section;
    $section->set_mediator($self);
    return $self;
}

# セクション間の通知を仲介する
sub notify($self, $event, $data = {}) {
    my @results;

    if ($event eq 'food_order') {
        if (my $kitchen = $self->{sections}{kitchen}) {
            push @results, $kitchen->prepare($data);
        }
        if (my $cashier = $self->{sections}{cashier}) {
            $cashier->add_to_bill($data);
        }
    }
    elsif ($event eq 'drink_order') {
        if (my $bar = $self->{sections}{bar}) {
            push @results, $bar->prepare($data);
        }
        if (my $cashier = $self->{sections}{cashier}) {
            $cashier->add_to_bill($data);
        }
    }
    elsif ($event eq 'combo_order') {
        if (my $kitchen = $self->{sections}{kitchen}) {
            push @results, $kitchen->prepare($data);
        }
        if (my $bar = $self->{sections}{bar}) {
            push @results, $bar->prepare($data);
        }
        if (my $cashier = $self->{sections}{cashier}) {
            $cashier->add_to_bill($data);
        }
    }
    elsif ($event eq 'delivery_order') {
        if (my $kitchen = $self->{sections}{kitchen}) {
            push @results, $kitchen->prepare($data);
        }
        if (my $delivery = $self->{sections}{delivery}) {
            push @results, $delivery->dispatch($data);
        }
        if (my $cashier = $self->{sections}{cashier}) {
            $cashier->add_to_bill($data);
        }
    }

    return \@results;
}

「これがオーダーマネージャー……仲介者ですね」ナナコさんが解説する。「各セクションからの通知を受け取って、必要な相手にだけ伝える。仲村さんが普段やっていることと、まったく同じです」

ドクターは既存のモジュールを次々と書き換え始めた。驚くほど速い。

まず全セクション共通の基底クラス Section.pm を作る。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package Section;
use v5.36;

sub new($class, %args) {
    return bless {
        name     => $args{name} // '不明',
        mediator => undef,
    }, $class;
}

sub name($self)     { $self->{name} }
sub mediator($self) { $self->{mediator} }

sub set_mediator($self, $mediator) {
    $self->{mediator} = $mediator;
}

そして Waiter.pm が——こうなった。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package Waiter;
use v5.36;
use Section;
use parent 'Section';

sub new($class, %args) {
    my $self = $class->SUPER::new(%args);
    $self->{name} //= 'ウェイター';
    return $self;
}

# 注文を受けて Mediator に通知するだけ
sub take_order($self, $order) {
    my $event = $order->{type} eq 'food'  ? 'food_order'
              : $order->{type} eq 'drink' ? 'drink_order'
              : $order->{type} eq 'both'  ? 'combo_order'
              :                             'food_order';

    return $self->mediator->notify($event, $order);
}

「消えた……」

思わず声が出た。

kitchenbarcashier への直接参照が全部消えている。ウェイターは Mediator だけを知っている。

Kitchen.pm も同じだった。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package Kitchen;
use v5.36;
use Section;
use parent 'Section';

sub new($class, %args) {
    my $self = $class->SUPER::new(%args);
    $self->{name} //= 'キッチン';
    return $self;
}

sub prepare($self, $order) {
    my $item = $order->{item} // '不明な料理';
    return "Kitchen: $item を調理中";
}

バーも、会計も、同じ構造だ。誰も他のセクションを直接知らない。自分の仕事だけに集中している。

ここで俺は気を利かせてコーヒーを3人分淹れてきた。ドクターは人のオフィスだというのにデスクを完全に占領しているし、ナナコさんもずっと立ちっぱなしだ。

「ありがとうございます」ナナコさんが受け取り、ドクターの分をモニターの横に置いた。

ドクターはコードに集中していたが、ふとコーヒーの匂いに気づいて手を止めた。カップを見て、ナナコさんの方をちらりと見る。

——ほんの少し、口角が上がった気がした。

俺はぎょっとした。この人、さっきまで般若みたいな顔でコードを斬り刻んでいたのに、ナナコさんに対してだけ別人のようだ。もしかして、あの二人——

「さて、テストも書き換えましょうか」

ナナコさんは何事もなかったかのようにホワイトボードに向き直った。ドクターも即座にコードに没頭している。

……気のせい、か?

術後経過

テストを実行する。

全件通過。緑の文字がターミナルに流れる。

「お、通った!」

だが、ドクターはまだ手を止めない。新しいファイルを作り始めた——Delivery.pm

「え、デリバリーも!?」

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package Delivery;
use v5.36;
use Section;
use parent 'Section';

sub new($class, %args) {
    my $self = $class->SUPER::new(%args);
    $self->{name} //= 'デリバリー';
    return $self;
}

sub dispatch($self, $order) {
    my $item    = $order->{item}    // '不明';
    my $address = $order->{address} // '不明な住所';
    return "Delivery: $item を $address へ配送手配";
}

そして Mediator への登録を1行追加するだけ。

1
$mediator->register(delivery => $delivery);

テスト実行。通過。

俺は自分の目を疑った。

「……嘘だろ。Waiter も Kitchen も Bar も Cashier も、何も触ってない……」

ドクターが立ち上がった。

「5本だ」

ナナコさんが頷く。

「20本あった依存が5本に。そして新セクションの追加は Mediator への登録1行だけ。これが仲介者パターンの効果です」

5本。たった5本。あの20本のスパゲッティが、5本のきれいな放射線になった。全員がオーダーマネージャー——Mediator——だけを見ている。まるで、俺がチームをまとめるときと同じ構図だ。

「先生……ありがとうございます」

俺は頭を下げた。

「俺、自分がいつもやってることをコードに持ち込めばよかったんですね」

ドクターは鞄に自前のキーボードを丁寧にしまいながら——

「感謝は、このコードに」

ナナコさんがドアを開けた。

「今後セクションが増えても、Mediator を育てていけば大丈夫です。仲村さんがチームをまとめるように、ね」

二人の背中がドアの向こうに消えた。

俺はモニターに映るスッキリしたコードを眺めた。依存線が5本しかない。新しいセクションを追加しても、既存コードには一切触れなくていい。

——俺、コードの Mediator にもなれるかもな。

タカシへのチャットに、こう返した。

「デリバリー対応、完了。1行で済んだ」


処方箋まとめ

症状適用すべき経過観察
モジュール間の直接参照がN×Nに膨張している
新コンポーネント追加時に既存全ファイルの修正が必要
メソッド引数にオブジェクト参照が3つ以上連なる
コンポーネントが2〜3個で、今後増える予定がない
通信パターンが1対1で固定されている

治療のステップ

  1. 感染経路の特定: 全モジュール間の use / 参照を洗い出し、依存グラフを描く
  2. 仲介者(Mediator)の設計: 通信ルーティングを一元管理するクラスを新規作成する
  3. 基底クラスの導入: 全セクション共通の Section クラスを作り、Mediator への参照を持たせる
  4. 直接参照の切除: 各モジュールから他モジュールへの直接参照を削除し、Mediator 経由に書き換える
  5. テスト実行: リファクタリング後のテストを全件通過させる
  6. 新セクション追加で検証: 新コンポーネントを Mediator に登録するだけで動作することを確認する

助手より

仲村さん、お疲れさまでした。人と人の間を取り持つ才能を、コードの世界にも持ち込んでいただけたら嬉しいです。全員が全員を知っている必要はありません。信頼できる仲介者が1人いれば、チームは回るんです——コードも、人も。

——ナナコ

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