Featured image of post コードバーテンダー【Middle Man】水を足しすぎたハイボール〜薄まる責務〜

コードバーテンダー【Middle Man】水を足しすぎたハイボール〜薄まる責務〜

Middle Man(中間者)とは何か? Perl+Mooのコード例で、全メソッドを転送するだけの空っぽのクラスの問題と、Remove Middle Man・選択的委譲(handles)による解決策を物語形式で解説します。

マスターがグラスに氷を入れている。

席に着いたばかりだった。いつもの席。5回目ともなると椅子の高さにも慣れて、腰を下ろした瞬間にカウンターと自分の位置がぴたりと合う。

白いラベルに緑の文字。棚から降ろされたボトルが、カウンターの上で暖色の照明を受けている。

「今夜は軽やかに」

注文していない。何も言っていない。なのにマスターはもうボトルを傾けている。

琥珀色——というほど濃くない。白州。淡くて明るい、若葉のような色。グラスの中に注がれたそれに、マスターが炭酸水をゆっくり加える。泡が立ち上り、氷がかすかに鳴った。

「……わかるんですか、今日の気分」

マスターが穏やかに微笑んだ。

「お顔を見れば」

嬉しかった。メニューを見て「おすすめを」と言っていた頃が遠い。通っているなあ、と思う。

口をつけると、軽い。爽やかだ。先週のグレンドロナックの重厚さとはまるで違う。柑橘の香りが鼻を抜けて、泡と一緒に消えていく。

「おいしい。これは——白州ですか」

「はい。ハイボールにすると、白州の青みのある香りが軽やかに立ちます」

カウンターの端に、あのボトルがある。麻布をかぶった取り置きボトル。前回よりまた近い。確実に、私の座っている側に寄っている。もう聞かない。聞いたところではぐらかされるだけだから。ただ「近いな」と思う。

来店——炭酸が立てる泡

扉がきっちりと開いた。

「きっちり」としか言いようのない開け方だった。力まず、乱暴でもなく、ちょうど人ひとり分の幅を確保して、まっすぐ入ってくる。ジャケットの袖をきれいにまくった男性。革の手帳を小脇に抱えている。

マスターが声をかけた。

「いらっしゃいませ。今夜は何をお召し上がりになりますか」

「ジンソーダをお願いします。——すみません、実は設計の相談で来たんですが」

丁寧だけど無駄がない。会議の冒頭で「今日のアジェンダは」と切り出しそうな、そういう人だ。心の中で「マネージャーさん」と呼んだ。仕切りが上手そうな雰囲気。会議室にいそう。

マネージャーさんはジンソーダを一口飲んで、革の手帳を開いた。付箋が几帳面に貼ってある。

「私はもともとエンジニアだったんですが、3年前にPMに転向しました。ただ、今もアーキテクチャレビューには参加していまして」

なるほど、エンジニア出身の管理職か。うちの会社にもいるタイプだ。

「弊社のフレームワークでは、ドメインクラスが外部サービスを直接呼ばないように、すべて Manager クラスを経由する設計にしています」

思わず口を挟んだ。

「あ、それうちもやってる! うちの開発チーム、なんでも一度マネージャークラスを通す設計にしてるの。安全だからって」

マネージャーさんの目が輝いた。

「おお、御社もですか! やっぱりそうですよね。たとえば OrderService を使うときは OrderManager を通す。間に一枚噛ませるのが定石です」

間に一つクラスを挟んで整理する——前回の委譲の話でぼんやり理解した仕組みだ。同じことをやっていると聞いて、なんだか嬉しい。

「わかるわー、私もそういう仕事してるから。取引先とエンジニアの間に立って仲介するの」

自分で言って、妙にしっくりきた。「間に立つ」仕事をしている人間として、マネージャーさんの話は他人事じゃない。

マネージャーさんが嬉しそうにうなずいた。けれどすぐに表情を曇らせた。

「ただ……最近、新人に言われたんです。『この Manager クラス、何もしてなくないですか?』って」

マネージャーさんがタブレットを取り出して、コードを見せてくれた。

 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
package OrderManager;
use v5.36;
use Moo;
use Types::Standard qw(InstanceOf);

has _service => (
    is       => 'ro',
    isa      => InstanceOf['OrderService'],
    required => 1,
);

# すべてのメソッドがただの転送
sub create_order ($self, $data) {
    return $self->_service->create_order($data);
}

sub cancel_order ($self, $id) {
    return $self->_service->cancel_order($id);
}

sub find_order ($self, $id) {
    return $self->_service->find_order($id);
}

sub list_orders ($self) {
    return $self->_service->list_orders;
}

sub update_status ($self, $id, $status) {
    return $self->_service->update_status($id, $status);
}

「見てください、5つのメソッドすべてが OrderService ときれいに対応しています。統一された窓口です」

嬉しそうだった。仲間を見つけたからか、新人に言われる前の自信が戻っている。

「右から左にきれいに流れてるのね」

「そうなんです。シンプルで、一貫性がある」

私もうなずいた。「間に立って整理するって大事よね。全部直接やりとりしたらカオスだもの」

マスターが静かにうなずいた。「おっしゃる通りです。中間者が必要な場面はございます。——問題は、この中間者が必要かどうかです」

テイスティング——薄まるハイボール

マスターがカウンターの下からもう一つグラスを取り出した。

氷を入れる。白州を注ぐ。そこまでは私のハイボールと同じだった。しかし炭酸水を注ぐ手が——止まらない。

注いでいる。まだ注いでいる。

グラスの中のウイスキーの色がどんどん薄まっていく。淡い琥珀から、透き通った黄色へ。黄色から、ほとんど透明へ。

「ハイボールは、ウイスキーの個性を炭酸が引き立てる飲み方です。——しかし、炭酸を足しすぎれば」

グラスの中は、ただの炭酸水にしか見えなかった。

「中身が、消えます」

マネージャーさんの手帳を持つ手が止まった。

「お客さまの OrderManager は、このグラスと同じ状態でございます。中を通る度に、何も加えずに通過させている。ウイスキーの色——つまりクラスの責務が、薄まって消えています」

「この状態を、Middle Man と呼びます」

なぜ問題なのか

マスターが続けた。

「Middle Man が問題になる理由は、3つございます」

「まず、認知的負荷の増加です」

聞き慣れない言葉だった。

「コードを読む人が OrderManager を開きます。しかし実装はありません。OrderService に転送しているだけです。読む人はもう一段掘って OrderService を開かなければなりません。クラスが増えるほど、たどる道が長くなる。迷路の中に空部屋がある状態です」

マネージャーさんが眉をひそめた。「迷路の空部屋……」

「次に、変更の二重コストです。OrderService に新しいメソッドが追加されるたびに、OrderManager にも同じ転送メソッドを手書きしなければなりません。一箇所の変更が二箇所の変更になります」

マネージャーさんが嘆息した。「先月 archive_order を追加したとき、Manager にも丸写しのメソッドを書かされました」

「最後に、偽の安心感です。『Manager を通しているから安全だ』と思い込んでいても、何もチェックしていない Manager は安全を担保していません。鍵のかからないドアが付いているだけでございます」

私はうなずいた。「わかる。うちの会社のSlack経由の承認フローみたい。全部通すけど何もチェックしてないの」

言ってから、自分の会社のことをまた漏らしていることに気づいた。最近多い。ゲスト客の話を聞くたびに、うちのことが口から出る。

マスターが薄まったグラスを静かに下げた。

炭酸を足すたびに薄まっていく。やがて、元のウイスキーの味は誰にもわからなくなります」

中間者が必要なとき

マネージャーさんが反論した。さすが元エンジニア、論理で返してくる。

「しかし、カプセル化は重要な設計原則ではないですか。内部の実装を隠すことで——」

マスターが穏やかに遮った。

「おっしゃる通りです。カプセル化は重要でございます。——ですが、OrderManager は何を隠していますか?」

マネージャーさんが口を開きかけて、閉じた。画面のコードを見返している。確かに——全部、受け取ったものをそのまま渡しているだけだ。何も変えていない。何も止めていない。何も隠していない。

「カプセル化が効果を発揮するのは、内部の実装詳細を隠すときです。隠すべきものがないのに中間者を置いても、それはただの迂回路でございます」

私が口を挟んだ。「じゃあ、間に立つのが全部ダメってわけじゃないのね? 鍵がちゃんとかかるドアなら意味がある、ってこと?」

「ええ。バリデーション、ログ記録、アクセス制御——中間者が付加価値を提供しているなら、それは Middle Man ではありません。必要な中間者です」

ブレンド——炭酸の量を見極めるブレンダーの仕事

マスターが口を開いた。

「解決策は、不要な中間者を取り除くことです」

解決策1: Remove Middle Man(直接呼び出し)

「最もシンプルな方法は、OrderManager を経由せず、OrderService を直接使うことです」

1
2
3
4
5
6
# Before: OrderManager を経由
my $mgr = OrderManager->new(_service => $svc);
$mgr->create_order($data);

# After: OrderService を直接使う
$svc->create_order($data);

マネージャーさんが戸惑った顔をした。「それだけですか? でもカプセル化が——」

OrderManager は何も隠していませんでした。右から左に渡しているだけです。カプセル化の名目で中間者を置いても、隠すべきものがなければ、それはただの迂回でございます」

解決策2: 選択的委譲(固有ロジックのあるメソッドだけ残す)

「もし OrderManager に固有のロジックを持つメソッドと、ただの転送メソッドが混在している場合は、handles で整理できます」

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package OrderManager;
use v5.36;
use Moo;
use Types::Standard qw(InstanceOf);

has _service => (
    is       => 'ro',
    isa      => InstanceOf['OrderService'],
    required => 1,
    handles  => [qw(find_order list_orders)],  # 単純転送は handles で
);

# 固有のロジックがあるメソッドだけ手書き
sub create_order ($self, $data) {
    die "name is required" unless $data->{name};
    return $self->_service->create_order($data);
}

handles は Moo が自動で委譲メソッドを生成します。手書きの転送コードが消える分、コードが減り、意図が明確になります。手書きで残すのは、バリデーションやログなどの付加価値を持つメソッドだけです」

私が思わず言った。「ちょうどいい炭酸の量ってことね。足しすぎたら薄まるし、足さなきゃストレートのまま」

マスターがわずかにうなずいた。

「まさに、そのとおりでございます」

前回褒められたときは「見事な例え」と言われた。今回は短いうなずきだけ。でも穏やかな目が「わかっている」と言っていた。——気がした。

前回の委譲との対比

マネージャーさんが考え込んでいた。

「先ほどの handles ですが——これを全メソッドに適用したら、結局同じことになりませんか?」

マスターの声がわずかに力を帯びた。

「鋭いご質問です。handles全メソッドを委譲してしまえば、それはまた Middle Man になります。転送コードが手書きか自動かの違いだけで、構造的には変わりません。——道具は、使い方次第でございます」

私はふと前回のことを思い出した。前回は handles で「必要なものだけ選ぶ」という話だった。今回は逆に「全部委譲したら意味がない」。同じ道具なのに。

「ねえマスター。前に来たとき、handles は選ぶための道具だって聞いたけど——全部選んだらダメってこと?」

「ええ。あのときの Refused Bequest は《もらいすぎたものから必要なものだけ選ぶ》問題でした。今回の Middle Man は《何も付け加えずに通すだけ》の問題です。同じ道具でも、選ぶ数を間違えれば、また別の問題が生まれます」

なぜ問題が消えるのか

マスターが指を三本立てた。

「3つの問題がどう変わるか確認しましょう」

「まず、認知的負荷変更の二重コスト。中間者そのものを取り除けば、読む人は OrderService を直接開くだけで済みます。迷路から空部屋が消え、変更も一箇所で完結します」

「そして偽の安心感。選択的委譲で OrderManager を残す場合——残ったメソッドにはバリデーションが入っています。鍵がかかっている。残ったものには、存在理由があるのです」

マネージャーさんが目を閉じた。考えを整理しているようだった。やがて目を開けて、静かに言った。

「つまり——中間者がいること自体が問題なんじゃなくて、何もしていない中間者が問題なんですね」

マスターがうなずいた。

「ええ。中間者に価値があるかどうか。それが判断の軸でございます」

ラストオーダー——間に立つということ

マネージャーさんが立ち上がった。革の手帳を閉じる。来たときのテキパキした足取りが、少しだけゆっくりになっていた。

「考えてみれば、PMの仕事もそうです。ただ右から左に伝えるだけなら、自分がいる意味がない。自分だから付けられる価値があるから——間に立つ意味がある」

独り言のようだった。小さく会釈をして、扉に向かう。

その背中に、マスターが静かに声をかけた。

炭酸の量を見極めることも、ブレンダーの大切な仕事でございます

マネージャーさんが一瞬だけ振り返って、小さくうなずいた。そして扉の向こうに消えていった。

マスターがカウンターの上のグラスを下げた。マネージャーさんのジンソーダと、あの薄まったハイボール。

帰り支度をして扉に向かう。路地裏に出ると、初夏の風がぬるい。歩きながら、さっき自分が言った言葉を反芻した。

「取引先とエンジニアの間に立って仲介するの」

取引先の要望をそのままエンジニアに伝えて、エンジニアの状況をそのまま取引先に返して。私は——右から左に流しているだけじゃないだろうか。

考えて、やめた。考えたところで今夜は答えが出ない。でも考えたこと自体が、たぶん何かの変化なのだ。

ふと、口をついて出た。

「まあ、回ってるんだから大丈夫か」

でもその言葉は妙に軽かった。ハイボールの炭酸みたいに、するっと抜けていった。


🥃 マスターのテイスティングノート

本日の銘柄: 白州ハイボール
お客さまの症状: 中間者(Middle Man)

ノージング(香り)── 問題の検知

クラスのメソッドの大半が、別のクラスの同名メソッドにそのまま転送されていたら、Middle Man を疑いましょう。引数の変換もバリデーションもログもない——ただ右から左に流しているだけのメソッドが並んでいたら、そのクラスは中身が消えかけています。

パレット(味わい)── 問題の本質

Middle Man は3つの問題を引き起こします。コードを読む人に余計な一段を掘らせる認知的負荷、委譲元への変更が二箇所に波及する二重コスト、そして「Manager を通しているから安全」という偽の安心感です。何もチェックしていないクラスは、安全を担保していません。

フィニッシュ(余韻)── 解決の方針

シンプルな解決策は Remove Middle Man——中間者を取り除いて直接呼び出しに戻すこと。中間者に一部固有のロジックがある場合は、handles で転送メソッドを自動化し、手書きで残すのは付加価値のあるメソッドだけに絞ります。handles で全メソッドを委譲すれば、それはまた Middle Man の再現です。

ペアリング(相性の良いパターン)

  • Facade パターン(複数サービスの統合が目的なら Middle Man ではない)
  • Law of Demeter(過度なメソッドチェーン回避と Middle Man のバランス)
  • Refused Bequest(前回。委譲の「量」を見極める対の問題)

「炭酸の量を見極めることも、ブレンダーの大切な仕事でございます」

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