Featured image of post コード探偵ロックの事件簿【Builder】果てしなき引数の行列〜コンストラクタの窒息事件〜

コード探偵ロックの事件簿【Builder】果てしなき引数の行列〜コンストラクタの窒息事件〜

引数が15個もあるコンストラクタ、順番を間違えたら本番事故。肥大化した引数リストを「Builderパターン」で美しく解体する、コード探偵ロックの推理。

「助けてください! 僕の書いたキャンペーン生成コード、引数を1個間違えただけで10万円の本番事故が起きたんです!」

僕——広告テック企業に入社したばかりの新人エンジニア・ケン——は、印刷した引数リストの紙を握りしめ、雑居ビルの薄暗い階段を駆け上がっていた。ガラス扉には「レガシー・コード・インベスティゲーション(LCI)」という、控えめに言って怪しすぎる看板が掲げてある。

ドアを開けた瞬間、サーバーラックの排熱とエナジードリンクの甘ったるい残り香が押し寄せてきた。

革張りの椅子にふんぞり返った男——自称「コード探偵」のロックは、手元の古めかしいメカニカルキーボードから一瞬だけ目を上げ、僕が握りしめた紙をじろりと見た。

「おやおや、ワトソン君。その巻物は何だね? まさか君のコードの引数リストか? ……古代エジプトのパピルスかと思ったよ」

「ケンです! あの、これなんですけど……」

僕は構わず紙を広げた。A4用紙に印刷されたコンストラクタの呼び出しコードは、紙の端から端までびっしりと引数で埋まっていた。

現場検証:窒息するコンストラクタ

「見てください。広告キャンペーンの設定オブジェクトを作るコードなんですが……」

【Before】問題のキャンペーン生成コード

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package Campaign;
use Moo;
use Carp qw(croak);

has title          => ( is => 'ro', required => 1 );
has budget         => ( is => 'ro', required => 1 );
has start_date     => ( is => 'ro', required => 1 );
has end_date       => ( is => 'ro', required => 1 );
has target_age_min => ( is => 'ro', default  => 0 );
has target_age_max => ( is => 'ro', default  => 99 );
has target_gender  => ( is => 'ro', default  => 'all' );
has platform       => ( is => 'ro', default  => 'all' );
has ad_format      => ( is => 'ro', default  => 'banner' );
has daily_cap      => ( is => 'ro', default  => 0 );
has region         => ( is => 'ro', default  => 'JP' );
has priority       => ( is => 'ro', default  => 'normal' );

1;

「属性が12個もあるんです。呼び出す側はこうなります」

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
my $campaign = Campaign->new(
    title          => '春の新生活キャンペーン',
    budget         => 100000,
    start_date     => '2026-04-01',
    end_date       => '2026-04-30',
    target_age_min => 20,
    target_age_max => 35,
    target_gender  => 'all',
    platform       => 'mobile',
    ad_format      => 'video',
    daily_cap      => 5000,
    region         => 'JP',
    priority       => 'high',
);

「先週、テスト環境用のキャンペーンを作るとき、budget100000 を入れるべきところを daily_cap に入れてしまって……。テスト用のつもりが本番環境で1日10万円の広告が配信されちゃって……」

僕は思い出すだけで胃が痛くなった。

ロックはエナジードリンクの缶をゆっくりと傾け、一口含んでから言った。

「初歩的なにおいだよ、ワトソン君。このコンストラクタは窒息している。12もの引数を丸呑みにさせられて、消化不良を起こしているのだ」

「消化不良って、プログラムの話ですよね?」

「もちろんだとも。だが問題はそれだけではない。このクラスにはバリデーションが一切ない。budget にマイナスの値を入れても、end_datestart_date より前でも、黙って受け入れてしまう。まるで無人の受付窓口だ。誰でも何でも通してしまう」

1
2
3
4
5
6
7
8
# バリデーションがないため、不正な値でもオブジェクトが生成されてしまう!
my $invalid = Campaign->new(
    title      => '',           # 空文字でもOK
    budget     => -500,         # マイナスでもOK
    start_date => '2026-12-31',
    end_date   => '2026-01-01', # 開始日より前でもOK!
);
# → 何のエラーも出ない……

「うっ……確かに、バリデーションは『あとで入れよう』と思ったまま半年経ってます……」

推理披露:窓口係(Builder)の登場

ロックは僕の手からPCを奪い取り、ターミナルで新しいファイルを開いた。

「君が必要としているのは、窓口係だ。市役所の窓口を想像したまえ。申請者がいきなり12種類の書類を束にして突き出しても、窓口係は受け取らない。一つずつ確認しながら受け付け、最後にまとめて処理するのだよ」

「窓口係……ですか?」

「その名を**Builder(ビルダー)**という。オブジェクトの組み立てを段階的に行い、最後に build() で完成品を渡す。これが今回の推理の切り札だ」

【After】CampaignBuilder クラス

 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
package CampaignBuilder;
use Moo;
use Carp qw(croak);

has _title          => ( is => 'rw', default => '' );
has _budget         => ( is => 'rw', default => 0 );
has _start_date     => ( is => 'rw', default => '' );
has _end_date       => ( is => 'rw', default => '' );
has _target_age_min => ( is => 'rw', default => 0 );
has _target_age_max => ( is => 'rw', default => 99 );
has _target_gender  => ( is => 'rw', default => 'all' );
has _platform       => ( is => 'rw', default => 'all' );
has _ad_format      => ( is => 'rw', default => 'banner' );
has _daily_cap      => ( is => 'rw', default => 0 );
has _region         => ( is => 'rw', default => 'JP' );
has _priority       => ( is => 'rw', default => 'normal' );

# 各設定メソッド($self を返してメソッドチェーンを実現)
sub title ($self, $val)          { $self->_title($val);          return $self; }
sub budget ($self, $val)         { $self->_budget($val);         return $self; }
sub start_date ($self, $val)     { $self->_start_date($val);     return $self; }
sub end_date ($self, $val)       { $self->_end_date($val);       return $self; }
sub target_age_min ($self, $val) { $self->_target_age_min($val); return $self; }
sub target_age_max ($self, $val) { $self->_target_age_max($val); return $self; }
sub target_gender ($self, $val)  { $self->_target_gender($val);  return $self; }
sub platform ($self, $val)       { $self->_platform($val);       return $self; }
sub ad_format ($self, $val)      { $self->_ad_format($val);      return $self; }
sub daily_cap ($self, $val)      { $self->_daily_cap($val);      return $self; }
sub region ($self, $val)         { $self->_region($val);         return $self; }
sub priority ($self, $val)       { $self->_priority($val);       return $self; }

# バリデーション付きの build メソッド
sub build ($self) {
    croak "title is required"      unless $self->_title;
    croak "budget is required"     unless $self->_budget > 0;
    croak "start_date is required" unless $self->_start_date;
    croak "end_date is required"   unless $self->_end_date;

    croak "end_date must be after start_date"
        if $self->_end_date le $self->_start_date;
    croak "target_age_max must be >= target_age_min"
        if $self->_target_age_max < $self->_target_age_min;

    return Campaign->new(
        title          => $self->_title,
        budget         => $self->_budget,
        start_date     => $self->_start_date,
        end_date       => $self->_end_date,
        target_age_min => $self->_target_age_min,
        target_age_max => $self->_target_age_max,
        target_gender  => $self->_target_gender,
        platform       => $self->_platform,
        ad_format      => $self->_ad_format,
        daily_cap      => $self->_daily_cap,
        region         => $self->_region,
        priority       => $self->_priority,
    );
}

1;

ロックが得意げにエンターキーを叩いた。

「注目すべきは2つ。まず、各設定メソッドが return $self自分自身を返すこと。これによってメソッドチェーンが可能になる。そして build() の中にバリデーションを集約していること。窓口で書類を一通り確認してから、初めて完成品を発行するわけだ」

「なるほど……。じゃあ呼び出す側はどうなるんですか?」

【After】メソッドチェーンによる生成

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
my $campaign = CampaignBuilder->new
    ->title('春の新生活キャンペーン')
    ->budget(100000)
    ->start_date('2026-04-01')
    ->end_date('2026-04-30')
    ->target_age_min(20)
    ->target_age_max(35)
    ->platform('mobile')
    ->ad_format('video')
    ->daily_cap(5000)
    ->priority('high')
    ->build();

僕は思わず声を上げた。

「めちゃくちゃ読みやすい……! 何をどの値に設定しているか、コードを読むだけで一目瞭然じゃないですか!」

「その通り。そしてうっかり不正な値を入れようとすると——」

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# 予算がマイナス → build() でエラー!
eval {
    CampaignBuilder->new
        ->title('Test')
        ->budget(-500)
        ->start_date('2026-01-01')
        ->end_date('2026-01-31')
        ->build();
};
# → "budget is required" エラーが発生!

# 終了日が開始日より前 → build() でエラー!
eval {
    CampaignBuilder->new
        ->title('Test')
        ->budget(1000)
        ->start_date('2026-12-31')
        ->end_date('2026-01-01')
        ->build();
};
# → "end_date must be after start_date" エラーが発生!

「もう**『無人の受付窓口』**は閉鎖だ。すべての申請は厳格な窓口係(Builder)を通り、不備があればその場で突き返される」

解決:安心のテスト結果

ロックがテストを実行すると、ターミナルに美しい結果が並んだ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
$ prove -v t/01_example1.t
# Subtest: Problem: Telescoping Constructor
    ok 1 - title should match
    ok 2 - budget should match
    ...
    ok 10 - PROBLEM: negative budget is accepted without validation
ok 1 - Problem: Telescoping Constructor
# Subtest: Solution: Builder Pattern
    ok 1 - title should match via builder
    ok 2 - budget should match via builder
    ...
    ok 8 - should reject missing title
    ok 9 - should reject non-positive budget
    ok 10 - should reject invalid date range
    ok 11 - should reject invalid age range
ok 2 - Solution: Builder Pattern
All tests successful.

「見たまえ。Beforeのコードはバリデーションなしで不正な値を受け入れてしまう。しかしAfterでは、Builderが不正な入力をすべて弾いている。完璧な防護壁だよ」

僕は画面を食い入るように見つめた。引数を15行ぶん羅列していたあの悪夢のようなコードが、メソッド名が意味を語る美しいチェーンに生まれ変わっている。

「これなら僕でも安心してキャンペーンを作れます……! もう引数の順番を間違えて10万円を溶かす心配がない……!」

「当然だよ、ワトソン君。引数の行列に並ぶ時代は終わったのだ。窓口(Builder)が一つずつ丁寧に受け付けてくれるのだからね」

ロックは満足そうにエナジードリンクの最後の一滴を飲み干した。

「ありがとうございます、ロックさん! このBuilderの書き方、完璧に理解しました! 早速、他のオブジェクトにも同じようにBuilderを追加してきます! 全クラスに!」

僕は意気揚々とPCを閉じ、立ち上がった。

「待ちたまえ! 全クラスにだと? 属性が2つや3つのクラスにまでBuilderを被せる気か! それは過剰設計(Over-Engineering)というもうひとつの犯罪だぞ! おい、話を聞きたまえ!」

探偵の悲鳴のような叫びが背中に突き刺さったが、僕の足取りは羽のように軽かった。


探偵の調査報告書

容疑(アンチパターン)真実(パターン)証拠(効果)
Telescoping Constructor(引数地獄)。コンストラクタに大量の引数を直接渡す設計で、可読性が極めて低く、引数の順序ミスや不正値の混入を防ぐ手段がない状態。Builder パターン。オブジェクトの生成プロセスを専用のBuilderクラスに分離し、名前付きのメソッドチェーンで段階的に属性を設定し、build() でバリデーション付きの完成品を出力する設計方式。引数の意味が一目瞭然になり可読性が飛躍的に向上した。build() メソッドにバリデーションを集約することで、不正な状態のオブジェクト生成が構造的に防止された。新しい属性の追加もBuilderにメソッドを1つ追加するだけで既存コードに影響を与えない。

推理のステップ

  1. 肥大化したコンストラクタを特定する: 引数が5個以上あり、呼び出し側のコードが数行にわたるコンストラクタを見つける。
  2. Builderクラスを作成する: 各属性に対応する設定メソッドを持ち、すべてのメソッドが return $self でメソッドチェーンを可能にするBuilderクラスを新設する。
  3. バリデーションを build() に集約する: 必須項目チェック、値の範囲チェック、項目間の整合性チェックなど、すべてのバリデーションロジックを build() メソッドに閉じ込める。
  4. 完成品を返す: バリデーションをすべて通過した場合にのみ、元のクラスのインスタンスを生成して返す。呼び出し側は build() が返すオブジェクトを安心して使える。

ロックより

ワトソン君。「引数が多いなら名前付き引数を使えばいい」と思っているだろう? それは悪くない第一歩だが、まだ足りない。

名前付き引数は「何を渡しているか」を明確にしてくれるが、「渡してはいけないもの」は弾いてくれない。Builderの真価は、オブジェクトの生成という行為そのものに門番を立てることにある。build() というたった1つの関門を通過しなければ完成品は手に入らない。その関門にすべてのルールを集約する。これこそが、堅牢なシステムを作るための初歩的な一手なのだよ。

ただし、くれぐれも忠告しておこう。属性が2つか3つのシンプルなクラスにまでBuilderを導入するのは、蝶を捕まえるのに大砲を使うようなものだ。道具は適材適所で使いたまえ。

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