Featured image of post コードバーテンダー【God Class】一つの樽にすべてを詰め込んだ禁断のブレンド〜最後のテイスティング〜

コードバーテンダー【God Class】一つの樽にすべてを詰め込んだ禁断のブレンド〜最後のテイスティング〜

God Classは一つの巨大クラスに全責務を詰め込んだアンチパターン。シリーズ最終回、常連客が11回の夜で学んだ「匂い」で自社コードを診断し、Perl+Mooの委譲パターンで分割する。

嵐の前の静けさ

十二杯目の夜。

いつもの時間ではなかった。少し遅い。会社を出たのが遅かったのではなく、路地裏の角で一度立ち止まったからだ。

エンジニアの加藤くんから受けた報告が、まだ頭の中をぐるぐる回っている。消費税率の改定対応——先週の話の続きだ。バグを追いかけたチームが行き着いた先は、一つの巨大なクラスだった。OrderProcessorクラス。1,200行。メソッドが40以上。社内のほとんどのモジュールから参照されている。加藤くんは「すべての問題がここに集まっています」と言った。

重い木の扉に手をかけた。11回押してきた扉。今夜で——何回目だろう。数えなくてもわかる。12回目だ。

押した。いつもの軋み。いつもの空気。

バーには誰もいなかった。ゲスト客がいない夜は初めてだ。マスターがいつもの場所に立っていて、カウンターが磨き上げられていた。いつも以上に。まるで今夜のために磨いたみたいに。

「いらっしゃいませ」

穏やかな声。11回聞いた声と同じで、同じでないような気がした。

メニューを見なかった。見なくてよかった。

「今夜は——私が選びます」

マスターが一瞬だけ、目の奥に何かを浮かべた。10回通えば気づけなかった類の変化。11回通ったから——いや、12回目だからわかる。温かいものが奥にある。名前はつけられない。

「かしこまりました」

棚を見た。グレンファークラス、ジョニーウォーカー グリーン、ボウモア、アードベッグ——全部知っている。名前だけじゃない。どの夜に出されたか、何の話をしていたか、その一杯が何を照らしていたかを、覚えている。

直感で一本を指した。銘柄は重要ではない。選んだことが重要だった。

マスターが静かにグラスに注いだ。受け取って、一口含む。

視線が自然に落ちた。取り置きボトル。真正面。先週——いや、11回目の夜からそのまま。麻布がずれて中が見えている。

逃げられないほど、目の前にある。

「マスター」

声が思ったより静かだった。

「このボトルのこと——聞いてもいいですか」

取り置きボトルの開封

マスターの手が取り置きボトルに伸びた。

12回通って、このボトルに触れるマスターを見るのは初めてだった。通うたびに少しずつ位置が変わり、麻布がずれ、琥珀色がちらつき——気づけばいつも視界の端にあった。一度手を伸ばしかけたとき、「まだ、早いですよ」と静かに止められたことがある。

今夜、マスターは止めなかった。

麻布を外す所作は、いつもウイスキーをサーブするときと同じ丁寧さだった。布がカウンターに落ちて、中身が——全部、見えた。

琥珀色。でも以前ちらりと見えた色よりもずっと濁っている。複数の原酒が混ざり合って、どの個性も識別できない。甘さと苦味と煙と青さが溶け合って、一つの「重い」色になっていた。

反射的に鼻を近づけた。11回の夜で育った鼻が反応する。甘い——でも雑味が強い。一つ一つの香りが喧嘩している。潰し合っている。どこかで嗅いだ匂いが、何層にも重なって、何一つはっきりしない。

「たくさんの匂いがするのに——一つも、はっきりしない」

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

「こちらは——11回の夜にお出しした原酒を、同じ瓶に注ぎ足したものでございます」

11種。11回の夜。11杯のウイスキー。

「一つの瓶に、すべてを」

「はい。グレンファークラス、ジョニーウォーカー、ボウモア、グレンドロナック、白州、アードベッグ——これまでの夜にお出ししたすべてを」

マスターが一拍置いて、言った。

「これは——あなたの会社のシステムです

心臓が跳ねた。

比喩だ——でも比喩じゃない。11種の原酒を一つの瓶に注ぎ足した。11の問題を一つのクラスに注ぎ足した。

「OrderProcessorクラス、のこと——ですか」

マスターが頷いた。その頷き方は「はい」という肯定だけとは違っていた。何が違うのか言葉にはできないが、11回の夜のすべてが、今この瞬間のためにあったのだと——そう言っているように見えた。

11の記憶

マスターがカウンターの下からテイスティングノートを取り出した。

何度も見ていた。マスターが営業中に何かを書き留めていた、あの革表紙のノート。「お客様の好みを記録しています」と聞いたことがある。ウイスキーの好みを、と。

開かれたページ。

ウイスキーの好みではなかった。

1ページ目——日付。そして、私の言葉。

「36とか72とか、謎の数字がいっぱいあるのよね。動いてるからいいのよね」 —— Magic Numbers — 定数化・型制約

私の目が揺れた。これは——初めての夜に漏らした言葉だ。バーカウンターに肘をついて、新人くんの話を笑いながら聞いていた夜。他人事だと思っていた夜。

2ページ目。

「うちのOrderクラス、顧客情報も在庫情報も全部持ってて。便利よね」 —— Feature Envy — 責務の切り離し

3ページ目。

「田中さんが書いたコード、3年前から残ってて——怖くて消せないですよね」 —— Dead Code — 不要コードの除去

4ページ目。

「基底クラスの半分を空でオーバーライドしてるって言ってたの。そういうものなんでしょ?」 —— Refused Bequest — 継承より合成

5ページ目。

「なんでも一度マネージャークラスを通す設計にしてるの。安全だからって」 —— Middle Man — 委譲の適正化

6ページ目。

「メールアドレスも電話番号も全部文字列で持ってる」 —— Primitive Obsession — 値オブジェクト・型制約

7ページ目。

「手順書って、ステップが増えたら困らない? うちは三十あるんだけど」 —— Temporal Coupling — 不変設計・ビルダー

8ページ目。

「矢印がずらっと並んでたわ。うちのエンジニアは当たり前だって言ってたけど」 —— Law of Demeter — 委譲による結合度低減

9ページ目。

「前のCTOが全部設定してて……辞めてからは誰もわからない部分がたくさんあるの。引き継ぎ資料もない」 —— Service Locator — 依存性注入

10ページ目。

「『将来必要になる』って作った抽象化層が3つあって。もう3年、一度も使われてない」 —— Speculative Generality — YAGNI

11ページ目。

「消費税率の変更で15ファイルも修正が必要になったの。エンジニアがおかしいって顔してたわ。……私も、おかしいと思った」 —— Shotgun Surgery — 責務の集約

ページをめくるたびに、11回の夜が蘇った。バーの席で、ウイスキーのグラスを傾けながら、自分がいかに無防備に会社のことを漏らしていたか。——そして、マスターがその一つ一つに名前を付けていたこと。

12ページ目は——空白だった。

「ここから先は」

マスターの声が、いつもより少しだけ柔らかかった。

「あなたが、お書きください」

なぜ最初から言ってくれなかったの?

沈黙が落ちた。カウンターの木目の筋が、いやに鮮明に見えていた。

「最初から——わかっていたんですか」

「はい」

その一言が、11回分の夜の重さを持っていた。

初めての夜、「36とか72とか謎の数字がいっぱいあるの」と笑いながら言ったとき。2回目の夜、「Orderクラスが全部持ってて便利よね」と他人事の顔で言ったとき。3回目の夜、「田中さんのコード、怖くて消せない」と困った顔で言ったとき。——全部、聞いていた。聞いて、名前を付けて、ノートに書いた。

なのに、一度も言わなかった。「あなたの会社のコードに問題がありますよ」と。

手が微かに震えた。怒りではない。11回分の時間への、惜しさ。もっと早くわかっていれば。もっと早く手を打てていれば。

「なぜ——最初から言ってくれなかったんですか」

マスターがグラスを磨く手を止めた。12回通って、マスターが手を止めるのを何度も見てきた。でも今夜のそれは違った。グラスをカウンターに置いて、こちらを向いた。正面から。

「ウイスキーの味は——自分の舌でしか、学べません」

その言葉を聞いた瞬間、何かが——落ちた。胸のどこかで、ずっと宙に浮いていたものが。怒りが溶けて、溶けた後に残ったのは、「ああ」という感覚だった。説明されて理解したのではない。11回の夜を自分の足で歩いたから、この一言の重みがわかる。

10回目の夜を思い出した。マスターが「私も誰も注文しないカクテルをメニューに載せていた」と言った夜。あの人にも、自分の舌で学んだ夜があったのだろう。

長い息を吐いた。

「——動いてるだけじゃ、足りなかった

口からこぼれた言葉に、自分で驚いた。初めての夜に「動いてるからいいのよね」と言った自分が、遠い。3回目の「とりあえず動いてるし」も、5回目の「動いてるうちは大丈夫でしょ」も、6回目の「動いてる……わよね?」も、9回目の「動いてるけど……」も。先週の夜にはこの言葉自体が出てこなくなっていた。

そして今夜、最後の形になった。

テイスティング——自分の言葉で

マスターが濁ったボトルを私の前に置いた。

「——いかがですか。あなたの会社のシステムを」

いつもはゲスト客のコードに対してマスターがやっていたこと——テイスティング——を、私がやる番なのだ。コードの行は読めない。エンジニアの言葉では語れない。でも、11回の夜で育った「鼻」がある。

濁ったボトルに鼻を近づけた。目を閉じた。

「このシステムには——名前のない数字が散らばっている」

グレンファークラスの夜が蘇る。105という数字だけの名を持つウイスキー。

「36は無料トライアルの日数。72はセッションタイムアウトの秒数。でもコードには、ただ数字だけが書いてある」

マスターが静かに頷いた。言葉が次から次へとこぼれた。11回の夜で嗅いだ匂いが、一つずつ名前を持って浮かび上がってくる。

他人のデータに手を伸ばすクラス。呼ばれないまま残った3年前のコード。半分空のオーバーライド。何もせず転送するだけのマネージャー。型のない文字列。順番を間違えたら壊れる初期化。ドットの連鎖。辞めたCTOしか知らない設定。誰も使わない抽象化層——。

どの匂いにも、あの夜のウイスキーの記憶が重なっていた。

「そして——消費税率の変更で15ファイル。一つを変えたら、全部に影響する」

目を開けた。

「全部、このクラスの中にある。一つの樽に、全部詰め込んでしまった」

マスターが頷いた。

「God Class、と呼ばれています。一つのクラスにすべてを注ぎ足した——濁ったブレンド」

God Class。神のクラス。一つのクラスがすべてを知り、すべてを制御し、すべての変更がそこに流れ込む。

「11回分の問題が、全部ここに」

「いいえ」

マスターの声が静かに返った。

「11回分の答えも、全部ここに」

ブレンド——原酒を分ける

マスターが一枚の紙をカウンターに置いた。書かれていたのは、コードだった。

「あなたの会社のOrderProcessorクラスを——縮小したものとお考えください」

 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
# Before: God Class — 一つの樽にすべてを詰め込んだ
package OrderProcessor;
use v5.36;
use Moo;
use Types::Standard qw(ArrayRef Str Num);

has items          => (is => 'ro', isa => ArrayRef[Num], required => 1);
has customer_email => (is => 'ro', isa => Str, default => sub { '' });
has customer_name  => (is => 'ro', isa => Str, default => sub { '' });

# 税計算
sub tax_rate ($self) { 0.08 }
sub subtotal ($self) {
    my $sum = 0;
    $sum += $_ for $self->items->@*;
    return $sum;
}
sub tax ($self) {
    return int($self->subtotal * $self->tax_rate);
}
sub total ($self) {
    return $self->subtotal + $self->tax;
}

# 割引
sub discount ($self) {
    my $st = $self->subtotal;
    return $st >= 10000 ? int($st * 0.1)
         : $st >= 5000  ? int($st * 0.05)
         :                0;
}

# 在庫確認
sub check_stock ($self) {
    return scalar $self->items->@* > 10
        ? { ok => 0, message => 'Insufficient stock' }
        : { ok => 1, message => 'Stock available' };
}

# バリデーション
sub validate ($self) {
    my @errors;
    push @errors, 'No items' if !scalar $self->items->@*;
    push @errors, 'Invalid email' if $self->customer_email !~ /\@/;
    return { valid => !@errors, errors => \@errors };
}

# 通知送信
sub send_confirmation ($self) {
    sprintf 'Order confirmed for %s: total %d yen',
        $self->customer_name, $self->total;
}

エンジニアの言葉ではわからない。でも、11回の夜で覚えた目で見ると——

「税の計算と、割引と、在庫確認と、バリデーションと、通知——全部、同じ場所にある」

「はい。一つの樽に5種類の原酒を注ぎ足したようなものです。一つの味を変えたいだけなのに、樽ごと開けなければならない」

マスターが新しい紙を出した。

「同じ原酒です。ただし今度は——適切に分けます」

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# After: 原酒を分ける — 各クラスが単一の責務を持つ

# 税率の管理
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);
}

「前回——バランタインの夜に、修理屋さんが書いたコード」

あの夜のゲスト客を思い出した。消費税率の変更で15ファイルを修正した、20代の保守担当エンジニア。あの子が書き直した TaxPolicy が、ここでも使われている。

「はい。あの夜の答えが、今夜の設計の一部になります」

マスターがさらに紙を出した。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 割引の管理
package DiscountPolicy;
use v5.36;
use Moo;
use Types::Standard qw(Num);

has high_threshold => (is => 'ro', isa => Num, default => sub { 10000 });
has high_rate      => (is => 'ro', isa => Num, default => sub { 0.1 });
has low_threshold  => (is => 'ro', isa => Num, default => sub { 5000 });
has low_rate       => (is => 'ro', isa => Num, default => sub { 0.05 });

sub apply ($self, $amount) {
    return $amount >= $self->high_threshold ? int($amount * $self->high_rate)
         : $amount >= $self->low_threshold  ? int($amount * $self->low_rate)
         :                                    0;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 在庫確認
package InventoryChecker;
use v5.36;
use Moo;
use Types::Standard qw(Num);

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

sub check ($self, $item_count) {
    return $item_count > $self->threshold
        ? { ok => 0, message => 'Insufficient stock' }
        : { ok => 1, message => 'Stock available' };
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# バリデーション
package OrderValidator;
use v5.36;
use Moo;

sub validate ($self, %args) {
    my @errors;
    my $items = $args{items} // [];
    my $email = $args{email} // '';
    push @errors, 'No items'      if !@$items;
    push @errors, 'Invalid email'  if $email !~ /\@/;
    return { valid => !@errors, errors => \@errors };
}
1
2
3
4
5
6
7
8
9
# 通知
package OrderNotifier;
use v5.36;
use Moo;

sub send_confirmation ($self, %args) {
    sprintf 'Order confirmed for %s: total %d yen',
        $args{name} // 'Customer', $args{total} // 0;
}

「そして——これらを、一つの注文として束ねます」

 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
62
63
64
65
66
67
68
69
70
# Order: 原酒を適切に調合するブレンダー
package Order;
use v5.36;
use Moo;
use Types::Standard qw(ArrayRef Str Num InstanceOf);

has items          => (is => 'ro', isa => ArrayRef[Num], required => 1);
has customer_email => (is => 'ro', isa => Str, default => sub { '' });
has customer_name  => (is => 'ro', isa => Str, default => sub { '' });

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

has discount_policy => (
    is      => 'ro',
    isa     => InstanceOf['DiscountPolicy'],
    default => sub { DiscountPolicy->new },
);

has inventory => (
    is      => 'ro',
    isa     => InstanceOf['InventoryChecker'],
    default => sub { InventoryChecker->new },
);

has validator => (
    is      => 'ro',
    isa     => InstanceOf['OrderValidator'],
    default => sub { OrderValidator->new },
);

has notifier => (
    is      => 'ro',
    isa     => InstanceOf['OrderNotifier'],
    default => sub { OrderNotifier->new },
);

sub subtotal ($self) {
    my $sum = 0;
    $sum += $_ for $self->items->@*;
    return $sum;
}

sub tax   ($self) { $self->tax_policy->apply($self->subtotal) }
sub total ($self) { $self->subtotal + $self->tax }

sub discount ($self) {
    $self->discount_policy->apply($self->subtotal);
}

sub check_stock ($self) {
    $self->inventory->check(scalar $self->items->@*);
}

sub validate ($self) {
    $self->validator->validate(
        items => $self->items,
        email => $self->customer_email,
    );
}

sub send_confirmation ($self) {
    $self->notifier->send_confirmation(
        name  => $self->customer_name,
        total => $self->total - $self->discount,
    );
}

「……つまり——」

言葉を探した。11回の夜で学んだ言葉で。

「原酒を種類ごとに分けて、それぞれの瓶に入れる。税のことは税の瓶が知っている。割引のことは割引の瓶が知っている。注文は——それぞれの瓶から、必要なだけ注いで調合する。ブレンダーのレシピ帳に従って」

「はい。——同じ原酒でも、設計次第で味は変わります」

マスターの声が穏やかだった。いつもの穏やかさと同じで、同じでないような。

「消費税率を変えたいなら、TaxPolicyの1行だけ。割引のルールを変えたいなら、DiscountPolicyだけ。在庫のしきい値を変えたいなら、InventoryCheckerだけ。——散弾銃で壁中に穴をあけるのではなく、一丁のライフルで一つだけ撃つ」

Mooの has と型制約が、原酒を正しい瓶に入れるための仕組みだということが——言葉ではなく、11回分の夜の蓄積で、腑に落ちた。

再ブレンド

マスターが棚からボトルを一本ずつ降ろした。

グレンファークラス。ジョニーウォーカー グリーン。ボウモア。グレンドロナック。白州。アードベッグ。ラスティネイルの原酒。余市。タリスカー。終売蒸留所のボトル。バランタイン。——そして、空のブレンディンググラス。

11本。11回の夜。

「同じ原酒です」

マスターがブレンディンググラスに原酒を一つずつ加えていく。量を計り、順序を守り、それぞれの個性が活きるバランスを指先で探っている。カウンターの上の11本のボトルと、マスターの手元のブレンディンググラスと、テイスティングノートが、同じ直線上に並んでいた。

出来上がったブレンドは——濁ったボトルとは全く違っていた。透明感のある琥珀色。

「——いかがでしょうか」

受け取った。鼻を近づける。

11種の香りがする。でも一つにまとまっている。それぞれの原酒の個性が潰し合わずに共存している。グレンファークラスの甘さ、ボウモアの煙、アードベッグの力強さ、白州の爽やかさ——全部がそこにあるのに、一つの味として調和している。

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

言ったあとに気づいた。同じ言葉を——11回目の夜に、バランタインを飲んで言った。でもあの夜は「なぜまとまるのか」がわからなかった。今夜はわかる。原酒が適切な量で、適切な順序で、適切な設計のもとに調合されているから。

一つの瓶にすべてを注ぎ足した濁ったブレンドと、11種の原酒を適切に調合したオリジナルブレンド。同じ材料。同じ量。設計だけが違う

小さく笑った。何がおかしいのか自分でもよくわからない。でも——12回分の重みを含んだ笑いだった。

ラストオーダー

テイスティングノートの空白のページに、もう一度指先で触れた。紙の手触りが冷たい。

「ここから先は——私が書くんですね」

マスターが穏やかに頷いた。

「はい」

それだけだった。それだけで十分だった。

立ち上がった。コートを羽織った。いつもの動作。でも今夜は——重さが違う。11回分の夜を置いて、12回目の夜を持って帰る。

「マスター」

振り返りかけて——やめた。

言いたいことは、もう全部言った。11回分の夜のすべてが、テイスティングノートの中にある。残りの空白ページは、自分で書く。

扉に手をかけた。押した。路地裏の夜風が頬に触れた。

ペンが紙を擦る、かすかな音が聞こえた気がした。扉が閉まりかける隙間から。マスターが何を書いたかは見えない。

振り返らなかった。

路地裏は静かだった。遠くで車のエンジン音がして、どこかの店のドアが閉まる音がして、それからまた静けさが戻った。

そして私は今夜、自分のコードの匂いを、初めて自分の鼻で嗅いだ。


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

本日の銘柄: マスターのオリジナルブレンド お客さまの症状: 神のクラス(God Class)

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

一つのクラスが「Manager」「Processor」「Handler」という名前を持ち、税計算も在庫確認も通知もバリデーションもすべて引き受けているなら、このアンチパターンを疑いましょう。クラスの説明に「そして」が3つ以上連なるとき——それは一つの樽にすべてを注ぎ足した濁ったブレンドです。

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

God Classは単に「大きい」のではありません。複数の無関係な責務が一箇所に集中することで、すべての変更がこのクラスに流れ込みます。Magic Numbers、Feature Envy、Dead Code、Shotgun Surgery——11のアンチパターンは、God Classという一つの巨大な樽の中で互いに雑味を生み合っていたのです。

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

Single Responsibility Principle——一つのクラスが変更される理由は一つだけ。Mooの hasInstanceOf で責務ごとの専門クラスに委譲し、God Classを薄いオーケストレーション層に変えます。税率のことは TaxPolicy が知り、在庫のことは InventoryChecker が知る。同じ原酒でも、設計次第で味は変わります。

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

  • Single Responsibility Principle(変更理由を一つに閉じる)
  • Extract Class(大きなクラスから責務を抽出する)
  • Moo::Role / Composition(継承ではなく合成で設計する)

「ウイスキーの味は——自分の舌でしか、学べません」

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