Featured image of post コードバーテンダー【Shotgun Surgery】味を変えたら棚が崩れる〜消費税率の連鎖〜

コードバーテンダー【Shotgun Surgery】味を変えたら棚が崩れる〜消費税率の連鎖〜

Shotgun Surgeryは一つの変更が多数のファイルに波及するアンチパターン。Perl+MooでTaxPolicyクラスに税率を集約し、変更を1箇所に閉じ込める設計を物語形式で解説します。

バランタインの夜

十一杯目の夜。

いつもの席に座っている。何か考えてきたわけではない。いつものように来た。いつものように重い扉を押して、いつものようにカウンターの木目に指を滑らせて、いつものように座った。

でも——今夜のバーは静かだ。

BGMの音量が違うのかもしれない。空調の音が違うのかもしれない。わからない。ただ、空気の密度が違う。10回通って身体に染みついた「いつもの空気」のほんの少し外側にいる感じ。

マスターが棚に手を伸ばした。

「17」。

ボトルに刻まれた数字が目に入った。ラベルの数字を見る癖がついたのは、いつからだろう。最初の夜は「おすすめを」としか言えなかった。6杯目の夜に自分で選んで失敗して、8杯目で「なぜこれを選んだのか」が気になって、9杯目で「書かれていないこと」に気づいた。10杯目は終売蒸留所のボトルに「もう作れない」という言葉を重ねた。

今夜は——数字が目に入ることが、もう自然になっている。

グラスに琥珀色の液体が注がれる。

「バランタイン。17年熟成でございます」

一口。

——甘い。花のような。でもその奥に何十もの味が重なっている。蜂蜜、バニラ、かすかに煙。柑橘の皮が鼻に抜けて、その後に木の実のような丸い余韻。これまでの10杯のどれとも違った。どの味もはっきりしているのに、喧嘩していない。

「……たくさんの味がする。でも一つにまとまっている」

マスターが静かに頷いた。

「40を超える蒸留所の原酒を、一つのボトルに調合しております。——一つの味を変えれば、全体のバランスが変わります」

そうだろうな、と思った。これだけの味が調和しているのなら、一つを動かせば全部が動く。でもそれが今夜の私に何を意味するのかは、まだわからなかった。

視線がカウンターの上を滑る。

取り置きボトル。

——真正面

呼吸が止まった。

先週は一席分離れていた。先々週は三席分だった。それが今夜は、グラスを置いた目の前のラインに——麻布のボトルがある。

麻布がずれている。中の琥珀色がはっきりと見えた。バランタインと似た色。けれどバランタインのような調和した透明感はない。もっと複雑で、もっと——濁っている? いや、まだわからない。見える範囲が狭すぎる。

8杯目の夜に手を伸ばして、「まだ、早いですよ」と止められた。あの穏やかな声を覚えている。10杯目の夜に「もう少し待てる自分がいる」と思った。

今夜は——目をそらせなかった。でも手は伸ばさない。

扉が開いた。

来店——修理屋さん

若い女性が入ってきた。

ステッカーだらけのノートPCが覗くバッグを肩にかけている。目の下に疲れの影。でも姿勢は悪くない。折れていない人の背中。

カウンターを見回して、私から一席空けて座った。

「……あの、ウイスキーは詳しくないんですけど、何かおすすめはありますか?」

胸の奥が、小さく跳ねた。

10回前。最初の夜。——このカウンターに座って、同じことを言った気がする。「おすすめを」。あの時の自分と重なるような声。でも結びつかない。ただ、既視感だけが残った。

マスターが棚からもう一本ボトルを選び、ゲスト客にグラスを出した。彼女が一口飲んで、少しだけ肩の力が抜けるのが見えた。

バッグからPCを取り出す仕草が、どこか修理工場で工具箱を開ける人に似ていた。心の中で「修理屋さん」と呼ぶことにした。

「あの……見てもらってもいいですか。コードの話なんですけど」

マスターが穏やかに頷く。

修理屋さんがPCの画面をこちらにも見えるように傾けた。

「保守チームで働いてるんですけど——消費税率の改定対応を任されたんです」

画面にコードが映っている。数字が見える。0.08。同じ数字が、何行も。

「1箇所直したら、他のファイルとの不整合でテストが壊れたんです。全体を洗い出すのに2日かかりました」

修理屋さんの声は冷静だったが、指先がキーボードの端を撫でていた。

「直すのは簡単なんです。0.080.10 にすればいい。でも……なぜ15箇所に同じ数字が書いてあるのかがわからなくて」

1
2
3
4
5
6
7
8
9
# 税率がリテラルで散在している
package Order;
use v5.36;
use Moo;

sub total ($self) {
    my $subtotal = $self->subtotal;
    return $subtotal + $subtotal * 0.08;  # ← ここ
}
1
2
3
4
5
6
7
package Invoice;
use v5.36;
use Moo;

sub tax ($self) {
    return $self->amount * 0.08;  # ← ここも
}

言葉を失った。

この話を——前にも聞いた

テイスティング——散弾の痕跡

最初の夜のことを思い出していた。

新人くんが「数字だらけのコードが読めない」と困っていた。マスターが「ラベルのないボトルは、中身を知る人がいなくなったとき、ただの液体になります」と言った。あの時は他人事として聞いていた。「36とか72とか謎の数字がいっぱいあるの」——自分で言った言葉。10回前の、軽い言葉。

今、修理屋さんの画面を見ている。0.08 が何行も並んでいる。あの夜の 3672 と、この 0.08 は——同じ種類の問題ではないか。

マスターが修理屋さんのPCに目を向けたとき、私は言っていた。

「修理屋さんも、コードで困ってるの? うちの会社もね——消費税率の変更で15ファイルも修正が必要になったの。エンジニアがおかしいって顔してたわ」

修理屋さんが目を見開いた。「15ファイル! 私のところも同じくらいです」

一拍、置いた。

「……私も、おかしいと思った

自分の口から出た言葉が、自分を驚かせた。「おかしい」——そう感じたのはいつだろう。15ファイル修正の報告を受けた日? このバーで10回の夜を過ごしたどこか? わからない。ただ、今夜この言葉を言えたことだけは確かだった。

マスターが——何も言わなかった。次にゲスト客への対応に移るまでの間が、いつもよりほんの一拍長い。それだけ。

修理屋さんがコードの全体を画面に出した。

「Order、Invoice、Receipt、ShoppingCart、PriceFormatter、Report。それぞれに 0.08 がハードコードされていて。0.080.10 に変えるだけなのに、15箇所あるんです」

私は修理屋さんに聞いた。

「消費税率が変わったら全部直すのって、仕方ないことだと思ってた。——だって実際変わるんだから、全部の書類を直すでしょ?」

修理屋さんが少し考えた。

「書類なら……経理はテンプレートの一箇所を直すだけですよね。税率が書いてあるマスターデータがあって、そこを変えれば全部に反映される」

息を呑んだ。

「——コードはそうなってないの?

修理屋さんが首を振った。「なってないんです。15箇所それぞれに 0.08 って書いてあります」

マスターはこのやりとりに入ってこなかった。カウンターの向こうで静かにグラスを磨いている。

やがてマスターが修理屋さんにグラスを差し出して、穏やかに言った。

「バランタイン17年には40を超える蒸留所の原酒が使われています。ブレンダーが一つの原酒の配合比率を変えた場合——その影響は、ボトル全体の味に波及します」

修理屋さんが画面を見つめたまま頷いた。「……そうです。まさにそれです。一つの数字を変えたら、全部壊れたんです」

マスターが頷くだけで、それ以上何も言わなかった。

ブレンド——味を一箇所で管理する

マスターがバランタインのボトルを手に取った。

「40の蒸留所。それぞれが独自の味を持っています。もし配合比率を変えたいなら……」

言いかけて——止まった。いつもの夜ならすでにコードの話と重ねているはずだ。でも今夜のマスターは、ウイスキーの話からコードへの橋を渡さなかった。

修理屋さんが引き取った。「一箇所で管理すればいい——ってことですよね?」

マスターが微かに笑んだ。「ブレンダーのレシピ帳には、各原酒の比率が一覧で記されています。変更はレシピ帳の一行だけ」

修理屋さんがPCに向き直って、コードを打ち始めた。キーボードを叩く音がバーの静けさに溶ける。

「この……TaxPolicy っていうクラスを作って、税率をここに集めれば——」

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

has rate => (
    is      => 'ro',
    isa     => Num,
    default => sub { 0.10 },
);

sub apply ($self, $amount) {
    return int($amount * $self->rate);
}

sub tax_included ($self, $amount) {
    return $amount + $self->apply($amount);
}

「Order も Invoice も、この TaxPolicy に聞けばいいんです」

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

has tax_policy => (
    is      => 'ro',
    isa     => InstanceOf['TaxPolicy'],
    default => sub { TaxPolicy->new },
    handles => { calculate_tax => 'apply' },
);

sub total ($self) {
    my $subtotal = $self->subtotal;
    return $subtotal + $self->calculate_tax($subtotal);
}

修理屋さんの表情が変わり始めていた。画面を見つめる目に、さっきまでの疲れとは違う光が混じっている。

マスターが水差しからバランタインに一滴加水した。

「配合比率を変えるのはブレンダーの仕事です。他の原酒が、自分で配合比率を決めることはありません」

修理屋さんが頷いた。「つまり、0.080.10 に変えるだけなら——TaxPolicy の1行だけ」

「はい。レシピ帳の一行です」

私はこのやりとりを聞きながら、言葉にならないものを抱えていた。15箇所に同じ数字がある。それがおかしいということは——10回の夜で育った何かが、教えてくれている。でもそれをどう言葉にすればいいのか、まだわからない。

「……なぜそれで問題が消えるの? クラスが増えただけじゃないの?」

修理屋さんが少し考えてから答えた。

「今まで15箇所に 0.08 が散らばっていたのは、15箇所それぞれが税率を知っている必要があったからです。でも本当は、税率を知っているべきは1箇所だけでいい。TaxPolicy に聞けばいい」

修理屋さんの声がわずかに強くなった。

「変更の理由が1箇所に閉じ込められるから、散弾銃のように飛び散らない。——Shotgun Surgery っていうそうです、この問題」

散弾銃。一発撃てば弾がばらばらに飛ぶ。一つの変更が15箇所に飛び散る。

	classDiagram
    class TaxPolicy {
        +Num rate
        +apply(amount) Int
        +tax_included(amount) Int
    }
    class Order {
        +TaxPolicy tax_policy
        +calculate_tax(amount) Int
        +total() Int
    }
    class Invoice {
        +TaxPolicy tax_policy
        +calculate_tax(amount) Int
        +total() Int
    }
    class Receipt {
        +TaxPolicy tax_policy
        +tax_line() Str
        +total() Int
    }

    Order --> TaxPolicy : delegates
    Invoice --> TaxPolicy : delegates
    Receipt --> TaxPolicy : delegates

マスターはこの会話の間、一言もコードの話をしなかった。私はそのことに、まだ気づいていなかった。

ラストオーダー——真正面のボトル

修理屋さんがPCをバッグに戻した。カウンターに手を置いて立ち上がる。

「ありがとうございました。……一箇所にまとめるだけで、全然変わるんですね」

立ち上がりかけて、私のほうを向いた。

「あの——消費税の話。15ファイル、おかしいって思えたのは、すごいことだと思います」

目を丸くした。修理屋さんは軽く頭を下げて、扉のほうへ歩いていった。重い扉が閉まる。

二人きり。

マスターがグラスを下げて、カウンターを拭き始めた。いつもの所作。磨き上げられたカウンターに反射する照明。ボトル棚の琥珀色が揺れる。

——何かが足りない。

足りないのは、言葉だった。

いつもの夜なら、このタイミングでマスターはウイスキーと絡めた一言を口にする。「ラベルのないボトルは」「器を変える勇気も」「配合の順序が」——10回の夜、マスターは必ず最後に何かを残してくれた。技術のことをコードの言葉で語るのではなく、ウイスキーの言葉で静かに差し出してくれた。

今夜は——何も言わない。

カウンターを拭き終えて、マスターが棚のボトルの位置を直している。背中が穏やか。怒っているわけでも、困っているわけでもない。ただ、言わない

「……マスター」

声が小さくなった。

「今夜は——何も、おっしゃらないんですね」

マスターが振り向いた。いつもの穏やかな表情。

「——いかがでしたか。バランタイン」

はぐらかされた。——でも不思議と腹は立たない。10回の夜で、この人の沈黙には意味があることを知っている。

グラスに残ったバランタインをゆっくり飲み干した。40を超える蒸留所の原酒。一つの味を変えれば全体が変わる。——修理屋さんの15ファイルと同じだ。

そして——最初の夜。

「36とか72とか謎の数字がいっぱいあるの。動いてるからいいのよね」

自分で言った言葉を、10回目にして初めて外側から聞いた気がした。あの数字たちと、今夜の 0.08 は——同じ種類の問題だ。名前のない数字が散らばっていて、一つを変えたら全部が壊れる。

「……前にもね——ここで似た話を聞いた気がするの。最初の夜。数字に名前がないって話」

マスターはテイスティングノートを取り出して、何かを書いていた。私には見えない角度。

視線を落とした。

取り置きボトル。真正面。麻布がずれて、中の琥珀色がはっきり見えている。

11回通って、少しずつ近づいてきたボトル。最初の夜はカウンターの端——最も遠い位置にあった。3回目に「動いた?」と聞いた。5回目にマスターが目をやった。6回目に麻布がずれた。8回目に手を伸ばして止められた。10回目に「もう少し待てる」と思った。

今夜は——逃げようがないほど、目の前にある。

バランタインと似た琥珀色。でもバランタインのような透明感はない。もっと複雑で、もっと——濁っている? 40の蒸留所の原酒が調和した味と、この琥珀色の中にある何かは、同じものなのだろうか。

「……これ、もしかして——」

言い終えられなかった。何を言おうとしたのか、自分でもわからない。ただ、10回の夜の全部が、このボトルに集まっている気がした。

マスターはテイスティングノートから顔を上げなかった。


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

本日の銘柄: バランタイン 17年 お客さまの症状: 散弾銃手術(Shotgun Surgery)

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

一つの仕様変更——たとえば消費税率の改定——をきっかけに、何本ものファイルを開かなければならなくなったら、このアンチパターンを疑いましょう。「同じ数字」「同じロジック」が複数の場所に散在していたら、それは散弾銃の弾痕です。

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

15箇所に 0.08 が書いてあるということは、15箇所それぞれが「税率を知っている」と宣言しているということです。しかし税率の責任を負うべきは1箇所だけ。責務が分散しすぎると、一つの変更が散弾のように飛び散ります。これは第1回で扱った Magic Numbers の延長線上にある問題——名前のない数字が散在し、その一つを変えたとき初めて「全部が同じ理由で壊れる」ことが可視化されます。

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

TaxPolicy クラスに税率と計算ロジックを集約し、他のクラスは handles による委譲で税率を参照します。変更はレシピ帳の一行——TaxPolicy の rate をただ書き換えるだけ。散弾銃が一丁のライフルに変わる瞬間です。

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

  • Single Responsibility Principle(変更理由を一つに閉じる)
  • Magic Numbers(第1回)— 名前のないリテラルの散在は Shotgun Surgery の予兆
  • Move Method / Move Field — 散在したロジックを集約するリファクタリング手法

「一つの味を変えれば、全体のバランスが変わります」

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