Featured image of post コードバーテンダー【Service Locator】隠し味を誰も知らない〜記録なき秘伝〜

コードバーテンダー【Service Locator】隠し味を誰も知らない〜記録なき秘伝〜

Service Locatorは依存関係を隠蔽し、テストを困難にするアンチパターン。Mooのコンストラクタインジェクションで依存を明示し、コード自身が自分の必要なものを語る設計へ。

ノンエイジの夜

九杯目の夜。

いつもの席で、スマホの画面をスクロールしている。社内Wikiに貼られたシステム構成図。エンジニアが描いた箱と矢印の図。色分けされた四角形、実線と点線の矢印、英語の略称。ほとんど読めない。

でも「読めない」と認識していること自体が、半年前の自分にはなかったことだ。あの頃は「エンジニアの図」として素通りしていた。今はわからないなりに、「箱が多い」「矢印が複雑」くらいは感じる。

マスターがカウンター越しにちらりとこちらを見た。私はスマホを閉じた。見られたから、ではない。今夜はウイスキーを飲みに来たのだと、自分に言い聞かせて。

「今夜のお冷やをお持ちしますね」

水を差し出してから、マスターは棚に手を伸ばした。何を出してくるか、待つ。もう「おまかせで」とは言わない。マスターが選ぶもの、その理由を知りたいと思うようになった。

差し出されたボトルは、黒いラベルに波打つような図柄が描かれている。

「タリスカーでございます」

「タリスカー……年数が書いてない?」

ボトルのラベルに、数字がない。最初の夜にグレンファークラスの「105」を見たときは何も思わなかった。いつから私は、ラベルの数字を探すようになったのだろう。

「ノンエイジ——年数を公表しない仕立てでございます。何年熟成したかは、作り手だけが知っている」

グラスに琥珀色の液体が注がれる。一口。荒い。舌の上で潮風と黒胡椒が暴れる。先週の余市の凛とした潮気とは違う。もっと野生的で、じりじりする。

「……強い。でも嫌じゃない。先週の余市と同じ潮の香りなのに、全然違う」

「産地の違いでございます。余市は北海道の海、タリスカーはスコットランドのスカイ島。同じ潮風でも、育った場所が違えば成り立ちが違う」

しみじみと飲む。年数が書いてない。匿名の強さ。この酒がいつから熟成されていたのか——知りたいけれど、ラベルには書かれていない。

視線がカウンターの上を滑る。取り置きボトル。前回と同じ位置かと思ったが、いや、また少し近い。三席分だった距離が、二席分と少しに見える。

前回手を伸ばして「まだ、早いですよ」と止められた。今夜は伸ばさない。伸ばしたい気持ちはあるが、あの穏やかな声を覚えている。そしてもう一つ——今夜は別のことが頭を占めている。

麻布の端から覗く琥珀色。年数の書いてないタリスカーと、麻布をかぶったボトル。どちらも中身を教えてくれない。

扉がゆっくり開いた。

黒いフリースの上着に、くたびれたリュック。目の下に疲労の影がある。席を選ぶ視線がゆるやかにカウンターを撫でて、端に座った。

一呼吸おいてから。

「ウイスキーを一杯と、相談を一つ、お願いできますか」

無駄を削った頼み方だった。急いでいるのではない。体力を節約している人の話し方。

心の中で「リーダーさん」と呼ぶことにした。チームを引っ張る人特有の、疲れの中にも背筋が伸びた姿勢。

「いらっしゃいませ。まずはお好みをお聞かせください」

「辛口で。甘くないものを」

マスターが棚から別のボトルを取り出し、グラスに注いだ。リーダーさんが一口飲んで、少しだけ肩の力が抜けるのが見えた。

PCを開く動作がゆっくりだった。前の週のネクタイさんのような丁寧さとも、その前の手順書さんの急ぎとも違う。「見せなければならない」ではなく「見てもらいたい」という切実さ。

「テックリードをやっているんですが——テストが書けなくて困っています」

一拍。

「正確に言うと、僕が書けないんじゃなくて、設計がテストを許さないんです」

画面にコードが映った。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package App::Notifier;
use v5.36;
use Moo;
use App::ServiceLocator;

sub notify ($self, $user_id, $message) {
    my $user_repo = App::ServiceLocator->resolve('user_repository');
    my $mailer    = App::ServiceLocator->resolve('mailer');

    my $user = $user_repo->find($user_id);
    $mailer->send($user->email, $message);
    return 1;
}

「この Notifier クラス、コンストラクタの引数を見ても何が必要かわからないんです。user_repositorymailer が必要なんですけど、それはコードの中で ServiceLocator に問い合わせて初めてわかる」

画面を覗く。ServiceLocator->resolve という文字列が目に入った。「resolve」の意味はわからないが、「何かを探しに行っている」感じは伝わる。

「これ……自分で材料を取りに行ってるの?」

「そうなんです。必要なものを自分で取りに行く設計で。冷蔵庫の中から勝手に材料を引っ張り出すみたいな」

あ——と、つい口が動いた。リーダーさんの話を聞いて思い出したのではない。今夜スマホで構成図を見ていたことが引き金になっている。

「取りに行く——あ、ごめんなさい、ちょっと思い出して。うちのシステムも、前のCTOが全部設定してて……辞めてからは誰もわからない部分がたくさんあるの。引き継ぎ資料もない」

「……ええ」

マスターの返事が聞こえた。いつもと同じ穏やかな声。私はリーダーさんの方に意識が向いていて、それ以上は気にしなかった。

リーダーさんが続ける。

「前のテックリードが入れた設計なんです。ServiceLocator というクラスで、アプリケーションの全部の部品——データベース接続、メール送信、ログ出力——を一箇所に登録して、必要になったら名前で取り出す」

「合理的に見えたんだと思います。どこからでも何でも取り出せる。便利そうに聞こえる」

便利。その言葉に既視感がある。前にもここで聞いた。「便利と依存は似た味がしますね」——マスターの言葉だったか。ずいぶん前の夜のことだ。

「でも前任が辞めた今、何をどこに登録すべきかが暗黙知になっている。新人がクラスを使おうとすると、まずコミット履歴を辿って登録処理を探す。見つかっても、なぜその順番で登録するのかは書かれていない」

テイスティング

マスターが画面に目を移した。数秒の沈黙のあと。

「この Notifier のコンストラクタを見ても——必要なものが何一つ見えません」

リーダーさんが頷く。「そうなんです。new するだけなら何のエラーも出ない。でもいざ notify を呼ぶと、内部で ServiceLocator に問い合わせて、登録されていなければ die する」

「お客さまの言うテストが書けないというのは——ServiceLocator にモックを登録しなければならないからでしょうか」

「テストを書くたびに ServiceLocator にモックを登録して、テストが終わったらリセットしなければならない。リセットを忘れると次のテストに影響する。テストの実行順序で結果が変わることもある」

「ちょっと待って」と口を挟んだ。「登録して、リセットして——それって、テストのための手順書が必要ってこと?」

二週前の夜が重なった。手順書さんの「手順書がないと動かないコード」。あのときの話だ。

リーダーさんが少し目を見開いた。「……言い得て妙です。テストのための手順書、確かにそうです。テスト対象が何に依存しているかわからないから、試行錯誤で登録する。登録漏れがあると、テスト実行中に初めてエラーになる」

「前もって何が必要かわからないの?」

「コンストラクタの引数を見ればわかるはずですけど——この設計だとコンストラクタには何も書かれていない。中を全部読んで、resolve を探すしかない」

マスターが静かに言った。

「このパターンを——Service Locator と呼びます。サービスの位置を聞きに行く仕組みです」

リーダーさんが小さく頷く。「パターン……というより、ある種の人たちはアンチパターンと呼んでいますね」

「ええ。便利に見えますが、核心的な問題があります」

マスターが言葉を選んでいるのがわかった。

「この Notifier が必要とするものは、user_repositorymailer の二つです。しかしそれは、コンストラクタに現れていない。依存関係が隠蔽されているのです」

リーダーさんの方を向いた。「隠れてるって——引き継ぎ資料がないのと同じ? 辞めた人の頭の中にだけあった情報?」

苦い笑みが浮かんだ。「……まさにそうです。前任のテックリードの頭の中にだけあった設計判断。ServiceLocator にどのサービスを登録するか、どの順番で登録するか。全部、彼の暗黙知だった」

マスターがタリスカーのボトルを取り上げた。

「タリスカーは年数を公表しません。何年もの原酒をヴァッティングしていますが、その配合は蒸留所のマスタースチルマンだけが知っている」

穏やかな声で。

「素晴らしいウイスキーです。しかし——もしマスタースチルマンが去ったら。配合を記録していなかったら」

リーダーさんの声が低い。「……同じ味は二度と作れない」

「Service Locator も同じです。設計した人がいる間は機能する。部品の場所を知っている人が、Locator に正しく登録してくれる。しかし——その人がいなくなったとき」

「レシピが消える……」

自分の声が、思ったより小さかった。

「でもね」と続けた。「書いておけばいいんじゃないの? ドキュメントに。『このクラスを使うときは先にこれを登録しなさい』って」

「書くことはできます。しかし——」

リーダーさんが引き取った。「書いてあっても——人間って忘れるんですよね。コードを変えたらドキュメントも直す。わかってはいるんです。でも締め切りに追われて、テストを通して、プルリクエストを出して——そのあとでドキュメントを更新する余力が残っている人は、ほとんどいない」

「仕組みで防げないの?」

「仕組みを作ることはできます。でも、ドキュメントが古くなっていても——エラーは出ないんです。コードが動いてしまう。テストも通ってしまう。間違ったドキュメントが残っていることに気づくのは、次にそのドキュメントを信じた誰かが手順通りにやって失敗したときです」

「……結局、読む人が正しいかどうか確認しなきゃいけないなら、書いてないのと変わらない?」

マスターの声はいつもと変わらず穏やかだった。

「コンストラクタの引数は嘘をつきません。引数が必要なら——渡さなければオブジェクトを作れない。コードが自分自身の依存を語る形を作ることが大切です」

二週前の記憶がよぎった。手順書さんの夜に聞いた言葉。「構造に語らせる」。同じことを、別の角度から言っている。

リーダーさんがぽつりと。

「辞めたテックリード——うちでは『あの人しか知らない設定がある』って、みんな言ってました」

グラスを口元に運ぶ途中で、手が止まった。

——「あの人しか知らない設定がある」。私も、さっき同じことを言った。うちのCTOについて。

バーに沈黙が落ちた。リーダーさんも、マスターも、私も、誰も次の言葉を探していなかった。ただ、同じ言葉が二人の口から出たという事実が、カウンターの上に静かに横たわっている。

グラスをカウンターに置いた。タリスカーの潮風が、鼻の奥でじりじりしている。

ブレンド

沈黙を破ったのはマスターだった。

「隠し味は——味わう人に伝わってこそ、意味があります」

リーダーさんが顔を上げた。

「Service Locator の問題は、依存を隠してしまうことです。では——隠さなければいい」

「コンストラクタインジェクション、ですか」

「ええ。必要なものを——コンストラクタの引数として、外から渡す」

マスターがリーダーさんに向き直った。

「Moo には、この考え方に合った仕組みがあります」

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package App::Notifier;
use v5.36;
use Moo;
use Types::Standard qw(HasMethods);

has user_repository => (
    is       => 'ro',
    isa      => HasMethods['find'],
    required => 1,
);

has mailer => (
    is       => 'ro',
    isa      => HasMethods['send'],
    required => 1,
);

sub notify ($self, $user_id, $message) {
    my $user = $self->user_repository->find($user_id);
    $self->mailer->send($user->email, $message);
    return 1;
}

「変わったのは、たった二つです。ServiceLocator への問い合わせが消え、代わりに has で依存を宣言しました。required => 1 をつけることで——渡さなければオブジェクトを作れません」

リーダーさんが画面を見比べている。Before と After を交互に。

resolve がなくなった……。コンストラクタに全部書いてある」

「ええ。Notifier->new を呼ぶとき、user_repositorymailer を渡す必要がある。渡さなければ——Moo がその場でエラーを出します。ServiceLocator に登録されているかを実行時に祈る必要はもうありません」

「前のコードとこのコードで、やってることは同じなんでしょう? メールを送るって」

マスターが頷く。「はい。振る舞いは同じです」

「じゃあ何が違うの?」

レシピが書いてある場所が違うのです」

タリスカーのボトルに手を添えて。

「以前は——レシピは蒸留所のマスタースチルマンの頭の中だけにあった。誰かが聞きに行かなければわからなかった。今は——レシピがボトルのラベルに書いてある。手に取れば誰でも読める」

リーダーさんが呟く。「コンストラクタの引数が、ラベルなんですね」

「ええ。そして HasMethods['find'] という型制約は——『このサービスには find メソッドが必要です』という要件まで明記しています。たとえば、テストのとき——」

リーダーさんの目が動いた。

「テストのときは、find メソッドを持つモックを渡せばいい。ServiceLocator に登録する必要がない。リセットも不要。テスト同士が干渉しない」

マスターが頷いて、テストコードの例を示した。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# テスト用のモック
my $mock_repo = MockUserRepo->new(
    user => { email => 'test@example.com' },
);
my $mock_mailer = MockMailer->new;

# Notifier に直接渡す(ServiceLocator 不要)
my $notifier = App::Notifier->new(
    user_repository => $mock_repo,
    mailer          => $mock_mailer,
);

$notifier->notify(1, "Hello");

「ServiceLocator のリセットがない……。各テストが独立している。テスト順序に依存しない」

「ええ。必要なものがすべて引数に書かれているからです。テストが何を必要としているかは、コードを読めばわかる」

「つまり……引き継ぎのとき、前任者に聞かなくてもコードを見ればわかるってこと?」

リーダーさんがこちらを見た。「そうです。new の引数を見れば、このクラスが何に依存しているかが全部わかる。前任者がいなくても」

「引き継ぎ資料がコードに埋め込まれている……」

マスターが静かに言った。

「設計が——自分自身を説明する。そこに人の記憶を挟まない」

リーダーさんが少し考え込んでから。「でも——各クラスが自分で依存を取りに行かないとしたら、誰がそれを渡すんですか? どこかで配線する場所が必要ですよね」

「ええ。アプリケーションの開始地点——Composition Root と呼ばれる場所で、すべてを組み立てます」

1
2
3
4
5
6
7
8
9
# main.pl(Composition Root)
use App::Notifier;
use App::UserRepository;
use App::Mailer;

my $notifier = App::Notifier->new(
    user_repository => App::UserRepository->new(dsn => $ENV{DB_DSN}),
    mailer          => App::Mailer->new(smtp => $ENV{SMTP_HOST}),
);

「ServiceLocator のように全体に散らばるのではなく、一箇所で組み立てて、あとは渡す」

「レジに一人いれば、あとの店員はレジの中身を知らなくていいってこと?」

リーダーさんが少し驚いたようにこちらを見た。

「……わかりやすいです。そうです、各クラスは自分の仕事だけすればいい。材料がどこから来たかは知らなくていい」

ラストオーダー

リーダーさんがPCを閉じた。ゆっくりと。さっきまでの疲弊が、少しだけ変質している。疲れが消えたわけではないが、疲れの質が変わった。行き場のない疲弊から、やるべきことが見えた人の疲弊に。

「月曜日に——まず Notifier から変えてみます。ServiceLocator を使っているクラスを洗い出すところから」

立ち上がる。マスターに一礼。

「ありがとうございました。……半年間、暗闇の中で手探りでした。入口だけでも見えて、助かりました」

扉のところで一瞬だけ振り返った。視線がこちらに来たような気がして、軽く目を合わせた。それだけだった。リーダーさんは何も言わず、重心の低い足取りで扉を押して出ていった。

バーに静けさが落ちた。タリスカーのグラスに一口分だけ残っている。舌の上にまだ潮風の名残。

「……マスター」

「はい」

「辞めた人しか知らない設定。リーダーさんだけじゃなくて——うちも同じ。うちのシステム、辞めたCTOしか知らない設定がある。引き継ぎ資料もない」

さっきも同じことを言った。でも、あのときは思い出しただけだった。リーダーさんの話をすべて聞いた後で、同じ言葉がまったく違う重さを持っている。

「テストが書けないっていうのも、引き継ぎができないっていうのも——うちの話に聞こえた」

沈黙。自分の言葉の重さに、自分で気づいている。

「……動いてるけど……」

続かない。「動いてるからいい」の文が、途中で止まった。「いいのよね」を続けられない。「わよね?」という疑問形にすらできない。ただ「けど」のまま、宙に漂っている。

間。

「もしかして、うち、深刻?」

マスターはカウンターを拭いている。穏やかに。問いに直接は答えなかった。

「レシピの秘密は——隠すためではなく、伝えるために記録するものでございます」

こちらを見ない。拭く手も止まらない。穏やかだが、この言葉に込められた何かが、空気をわずかに変えた。

答えを待った。待ったが、マスターはそれ以上何も言わなかった。「深刻?」への返答は——ない。

グラスを空にして、カウンターに置いた。

視線が自然にボトルへ行った。取り置きボトル。二席分と少し。前回より近い。毎回近づいている。もう否定できない事実。

前回は手を伸ばした。今夜は伸ばさない。「まだ」と言われたことを覚えているし、今夜は別のことが頭を占めている。

麻布の下の琥珀色。年数の書いてないタリスカー。ラベルに書かれていない情報。隠れた依存。——ボトルにも、何かが隠れているのだろうか。

「おやすみなさい、マスター」

「おやすみなさいませ。お気をつけて」

扉を押した。路地裏の夜風。四月の空気はまだ冷たい。

マスターは答えなかった。「深刻?」と聞いたのに、レシピの話をした。——でもそれは、答えだったのかもしれない。

「伝えるために記録する」。うちのCTOは、何を記録していたのだろう。何を記録しなかったのだろう。

足が重い。いつもの帰り道なのに、今夜は長い。

九回目の夜。私はまだ「深刻」かどうかわからない。でも——「わからない」こと自体が、もう答えなのかもしれない。


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

本日の銘柄: タリスカー(ノンエイジ)
お客さまの症状: サービスロケーター(Service Locator)

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

ServiceLocator->resolve('...') がメソッドの中に埋もれていたら、このアンチパターンを疑いましょう。コンストラクタの引数に何も現れないのに、実行時に突然「登録されていない」と叫ばれる——それは、レシピが蒸留所の外に出ていない証拠です。

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

依存関係がコードの奥に隠蔽されることで、テストは ServiceLocator のグローバル状態に縛られ、テスト間の独立性が失われます。新しいメンバーはコミット履歴を発掘しなければクラスの使い方がわからない。設計者がいなくなったとき、暗黙知と共にレシピが消えるのです。

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

has + required => 1 でコンストラクタに依存を宣言する。HasMethods 型制約で必要なインターフェースを明示する。組み立ては Composition Root の一箇所に集約し、各クラスは「自分が何を必要としているか」をラベルに記す設計へ。

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

  • Dependency Injection(コンストラクタインジェクション)
  • Composition Root
  • Types::Standard の HasMethods による型制約

「レシピの秘密は——隠すためではなく、伝えるために記録するものでございます」

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