Featured image of post コード探偵ロックの事件簿【Builder】時限装置の再来〜Temporal Coupling を断つ不変の鍵〜

コード探偵ロックの事件簿【Builder】時限装置の再来〜Temporal Coupling を断つ不変の鍵〜

init→configure→startの順序を間違えると爆発する時限設計——Temporal Couplingをrequired属性・BUILD検証・Builderパターンの三本柱で根治するコード探偵ロックの推理。Builder回の依頼人ケンが再登場するアーク総括回。

見覚えのある雑居ビルの階段を上った。

3週間前、僕は引数リストを印刷した紙——ロックさんに「パピルス」と呼ばれた巻物——を握りしめて、この階段を駆け上がったのだ。あの日、Builder パターンを教わり、「全クラスに Builder を追加します!」と意気揚々に叫んで帰った。

今日は巻物じゃなくてノートPCだけ。足取りは前回ほど速くない。恥ずかしさの分だけ、重い。

「レガシー・コード・インベスティゲーション(LCI)」のプレートは3週間前と変わらず、くすんだまま掲げてある。もう怪しいとは思わない。怪しいのは最初からわかっている。

ドアを開けた。

デスクの上に、エナジードリンクの空き缶が3段に積み上げられていた。一番下に太い缶、真ん中に細い缶、一番上にミニ缶。ロックさんは椅子にふんぞり返ったまま、その構造物をピンセットでつついている。

「おや、巻物の君か」

缶から目を上げないまま言った。

「ケンです。……もう訂正しても無駄ですよね」

「名前は覚えているよ、ワトソン君。巻物の方が印象的だというだけだ。——で、次は何だ? パピルスからロゼッタストーンにでも昇格したか?」

「いえ、今回は紙じゃなくてコードです。またやらかしました」

ロックさんはピンセットを置き、缶のタワーを指さした。

「見たまえ。この構造物は下から順に開けなければ崩壊する。一番上のミニ缶を先に取ると、バランスが崩れて全部倒れる。便利だろう?」

「それ、ただのゴミタワーでは……」

「順序が大事な構造物だ。逆から手をつけたら崩壊する。……それが今日の事件と無関係だとは言わないよ」

僕は一瞬黙った。3週間前なら「何言ってんだこの人」と心の中で突っ込んでいただろう。今日は違う。この人の奇行には必ず意味がある——ことがある。ないこともあるが。

現場検証:時限装置の構造

ノートPCを開き、問題のコードを見せた。

「前回 Builder を教わったあと、社内の他のコードも見直してみたんです。そしたら配信エンジンにこういうクラスがあって……」

【Before】呼び出し順序に依存する DeliveryEngine

 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
package DeliveryEngine;
use Moo;
use v5.36;
use Carp qw(croak);

has campaign_id  => (is => 'rw');
has target       => (is => 'rw');
has budget       => (is => 'rw');
has ad_format    => (is => 'rw');
has frequency    => (is => 'rw');
has _initialized => (is => 'rw', default => sub { 0 });
has _configured  => (is => 'rw', default => sub { 0 });

# Step 1: 必ず最初に呼ぶ
sub init ($self, %args) {
    croak "campaign_id is required" unless $args{campaign_id};
    croak "target is required"      unless $args{target};
    $self->campaign_id($args{campaign_id});
    $self->target($args{target});
    $self->_initialized(1);
    return $self;
}

# Step 2: init の後に呼ぶ
sub configure ($self, %args) {
    croak "Must call init() first" unless $self->_initialized;
    $self->budget($args{budget}       // 10000);
    $self->ad_format($args{ad_format} // 'banner');
    $self->frequency($args{frequency} // 1);
    $self->_configured(1);
    return $self;
}

# Step 3: configure の後に呼ぶ
sub start ($self) {
    croak "Must call init() first"      unless $self->_initialized;
    croak "Must call configure() first" unless $self->_configured;

    return {
        campaign_id => $self->campaign_id,
        target      => $self->target,
        budget      => $self->budget,
        ad_format   => $self->ad_format,
        frequency   => $self->frequency,
        status      => 'running',
    };
}

initconfigurestart の順番で呼ばないと動かないんです。先週テスト環境で配信設定を作るとき、configure をうっかり飛ばして start を呼んでしまって——」

「また事故か」

「未遂です! テスト環境だったので! でも本番だったらと思うと……」

ロックさんが画面に目を落とした。_initialized_configured のフラグを指先でなぞる。

「これは……時限装置だよ、ワトソン君」

「時限装置?」

init という名の安全装置を解除し、configure という名の起爆準備を完了し、start で爆発させる。正しい順序でなければ不発弾になる」

「爆弾って……プログラムですよね?」

「不発弾は一番危険な爆弾だ。今回は croak でエラーになったから気づけた。だがもしフラグチェックがなかったら? campaign_idundef のまま配信が始まっていたかもしれない。エラーにすらならず、黙って壊れる。サイレントな不発弾だ」

1
2
3
4
# configure を飛ばすと実行時エラー
my $engine = DeliveryEngine->new;
$engine->init(campaign_id => 'C001', target => 'age:20-35');
$engine->start;  # 💥 "Must call configure() first"

「でもロックさん、これって前回の Builder で解決できるんじゃないですか? initconfigure をメソッドチェーンで繋げば——」

ロックさんが首を振った。

「3週間前の事件を思い出したまえ。あのときの問題は引数が多すぎることだった。Builder は引数を整理するために導入した。今回の問題は引数の数ではない」

「じゃあ何が問題なんですか」

不完全な状態のオブジェクトが存在しうることだ」

ロックさんが紙を取り出し、図を描き始めた。

	sequenceDiagram
    participant Client
    participant Engine as DeliveryEngine
    Note over Client,Engine: 正しい順序を知らないと壊れる
    Client->>Engine: new()
    Note right of Engine: ❌ 不完全な状態
    Client->>Engine: init(campaign_id, target)
    Note right of Engine: ❌ まだ不完全
    Client->>Engine: configure(budget, ad_format)
    Note right of Engine: ✅ やっと完全
    Client->>Engine: start()
    Engine-->>Client: 配信開始

new した直後は何も持っていない空っぽのオブジェクトだ。init を呼んでも、まだ budget がない。configure を呼んで、やっと完全な状態になる。この中間の『不完全な状態』が存在できてしまうことが、すべての元凶だよ」

「つまり、フラグで順序を守らせているのは……」

「絆創膏だ。傷口を隠しているだけで、傷を治してはいない」

推理披露:不変の鍵

「この事件の解法は3つの鍵で構成される」

ロックさんがエナジードリンクの缶タワーから一番上のミニ缶を取った。タワーは崩壊しなかった。

「あれ、崩れない——」

「缶の中身を入れ替えておいた。一番下が空で、上に行くほど重い。どの順番で取っても安全になるように構造自体を変えたのだよ。順序に依存しない設計。それが今日のテーマだ」

鍵1:required + ro(不完全な状態の排除)

「まず1つ目の鍵。不完全な状態のオブジェクトを、そもそも存在させない

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package DeliveryEngine;
use Moo;
use v5.36;
use Carp qw(croak);
use Types::Standard qw(Str Int Enum);

has campaign_id => (is => 'ro', isa => Str, required => 1);
has target      => (is => 'ro', isa => Str, required => 1);
has budget      => (is => 'ro', isa => Int, default => sub { 10000 });
has ad_format   => (
    is      => 'ro',
    isa     => Enum[qw(banner video native)],
    default => sub { 'banner' },
);
has frequency => (is => 'ro', isa => Int, default => sub { 1 });

init()configure() が消えた……?」

「消えたのではない。コンストラクタに吸収された。campaign_idtargetrequired => 1 だ。これらなしには new が完了しない。つまり new が成功した時点で、オブジェクトは完全な状態だ」

「でも budgetad_format はオプションですよね。全部 required にしたら不便じゃ……」

default があるだろう。デフォルト値があるということは、それは必須ではないということだ。しかし campaign_idtarget にデフォルトはあるかね?」

「ない……です」

「ならば required だ。設計とはそういうことだよ」

そして全属性が is => 'ro'——読み取り専用になっている。

「あ、rw が全部 ro になってる。前回のときは Builder の内部属性は rw でしたよね?」

「あちらは Builder だ。設定途中の状態を保持する必要があるから rw にした。しかし完成品の DeliveryEngine は違う。完成品の状態は、生成後に変更されてはならない。前回の事件でも Campaign 本体は ro だっただろう?」

そう言われて前回を思い出した。確かに Builder は rw だったが、Campaign 自体はすべて ro だった。あのときは気にしていなかったけれど、それ自体が意図的な設計だったのか。

「でも全部 ro にしたら、実行中に設定を変えたいときどうするんですか。配信エンジンって、途中で予算を変更したりしません?」

「変えるな。新しく作れ」

ロックさんは一言で切り捨てた。

budget を変えたければ、新しい DeliveryEnginebudget だけ変えて作り直す。元のオブジェクトは壊さない。これを不変オブジェクトという。変更のたびに新品を作れば、古いオブジェクトの状態が汚染される心配はない」

鍵2:BUILD 検証(複数属性間の不変条件)

「2つ目の鍵。属性同士の関係を縛る」

1
2
3
4
sub BUILD ($self, $args) {
    croak "budget must be positive"    unless $self->budget > 0;
    croak "frequency must be positive" unless $self->frequency > 0;
}

「これって前回の build() メソッドのバリデーションと同じですか?」

「本質は同じだ。build() はBuilder 側の門番。BUILD は Moo が用意したオブジェクト側の門番。二重の防壁だ。しかし今回のように属性が5つ程度なら、Builder を介さずとも BUILD だけで十分な場合がある」

「え、じゃあ Builder いらないんですか?」

「3つ目の鍵を見せよう」

鍵3:Builder パターン(段階的構築)

「属性が増えた場合や、設定を段階的に組み立てたい場合には、前回の友人に再登場してもらう」

 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
package DeliveryEngineBuilder;
use Moo;
use v5.36;
use Carp qw(croak);

has _campaign_id => (is => 'rw');
has _target      => (is => 'rw');
has _budget      => (is => 'rw', default => sub { 10000 });
has _ad_format   => (is => 'rw', default => sub { 'banner' });
has _frequency   => (is => 'rw', default => sub { 1 });

sub campaign_id ($self, $val) { $self->_campaign_id($val); return $self; }
sub target ($self, $val)      { $self->_target($val);      return $self; }
sub budget ($self, $val)      { $self->_budget($val);      return $self; }
sub ad_format ($self, $val)   { $self->_ad_format($val);   return $self; }
sub frequency ($self, $val)   { $self->_frequency($val);   return $self; }

sub build ($self) {
    croak "campaign_id is required" unless $self->_campaign_id;
    croak "target is required"      unless $self->_target;

    return DeliveryEngine->new(
        campaign_id => $self->_campaign_id,
        target      => $self->_target,
        budget      => $self->_budget,
        ad_format   => $self->_ad_format,
        frequency   => $self->_frequency,
    );
}

「あ、前回の CampaignBuilder と同じ構造ですね。$self を返してチェーンする……Fluent Interface ですか」

「よく覚えているじゃないか」

ロックさんが僅かに口角を上げた。称賛とも皮肉とも取れる表情だ。

「だが一つ区別しておきたまえ。Fluent Interface はメソッドチェーンの見た目の話だ。Builder はオブジェクト生成の責任の話だ。見た目が似ていても、目的が違う」

使い方はこうなる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 方法1: 直接コンストラクタ(シンプルな場合)
my $engine = DeliveryEngine->new(
    campaign_id => 'C001',
    target      => 'age:20-35',
    budget      => 50000,
    ad_format   => 'video',
);

# 方法2: Builder(段階的構築が必要な場合)
my $engine2 = DeliveryEngineBuilder->new
    ->campaign_id('C001')
    ->target('age:20-35')
    ->budget(50000)
    ->ad_format('video')
    ->frequency(3)
    ->build;

「前回は『引数が多すぎる問題を Builder で解決した』。今回は『順序依存を Builder で解決した』。同じ Builder なのに解決する問題が違う……」

「Builder の本質は『完全な状態でなければ生成しない』という門番だ。引数地獄であれ、順序地獄であれ、門番の仕事は変わらない。不完全な申請は通さない。それだけだ」

ふと、前回の最後にロックさんに言われたことを思い出した。

「前回、ロックさんは『蝶を捕まえるのに大砲を使うな』って言いましたよね。今回はどっちですか? 大砲? 蝶?」

「いい質問だ」

ロックさんが缶タワーの残骸を片付けながら言った。

「属性が5つのこのクラスなら、鍵1と鍵2——requiredBUILD だけで十分だ。Builder は属性が増えたときの保険だ。過剰適用はしないこと。……前回帰り際に何を叫んでいたか覚えているかね?」

顔が熱くなった。

「……『全クラスに Builder 追加します!』」

「それが過剰適用だ」

	sequenceDiagram
    participant Client
    participant Builder as DeliveryEngineBuilder
    participant Engine as DeliveryEngine
    Note over Client,Engine: 順序不要。build() が門番
    Client->>Builder: new()
    Client->>Builder: campaign_id('C001')
    Client->>Builder: target('age:20-35')
    Client->>Builder: budget(50000)
    Client->>Builder: build()
    Builder->>Builder: バリデーション ✓
    Builder->>Engine: new(完全なパラメータ)
    Engine->>Engine: BUILD() 検証 ✓
    Engine-->>Client: 完全な DeliveryEngine
    Client->>Engine: start()
    Engine-->>Client: 配信開始

解決:安全な起動

ロックさんが僕のPCでテストを実行した。

 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
# Before: 順序を間違えるとエラー
my $engine = DeliveryEngine->new;
$engine->start;  # 💥 "Must call init() first"

# After: 必須項目なしではオブジェクトが生成できない
eval { DeliveryEngine->new() };
# → "Missing required arguments: campaign_id, target"

# After: 不正な値は BUILD で拒否
eval {
    DeliveryEngine->new(
        campaign_id => 'C001',
        target      => 'all',
        budget      => -500,
    );
};
# → "budget must be positive"

# After: Builder も順序を問わない
my $engine2 = DeliveryEngineBuilder->new
    ->budget(30000)           # 順番は
    ->frequency(2)            # どうでもいい
    ->target('geo:tokyo')     #
    ->ad_format('native')     #
    ->campaign_id('C002')     #
    ->build;
my $result = $engine2->start;
# → { campaign_id => 'C002', target => 'geo:tokyo', ... status => 'running' }

画面に結果が並んだ。Before では init を忘れただけで実行時エラーが出ていたコードが、After ではそもそもオブジェクトが作れない構造になっている。configure を忘れるとか、start を先に呼ぶとか、そういう「順序ミス」が概念ごと消滅していた。

configure を忘れるっていう事故が、起きようがなくなってる」

「そうだ。時限爆弾の導火線を切ったのではない。爆弾が組み立てられないようにしたのだ」

「導火線を切るのは……」

「対症療法だ。フラグで順序を守らせるのがそれだ。爆弾の部品を手に入れられない構造にするのが根治だ。required がその鍵で、BUILD が最後の検問で、Builder は受付窓口だ。三段構えで、不完全なオブジェクトはこの世に生まれてこられない」

僕はPCを見つめた。前回の Builder パターンで学んだ「門番」が、今回は別の場所で同じ仕事をしている。パターンは同じでも、解いている問題の根っこが違う。前回は「引数多すぎ」、今回は「順序依存」。でも門番の仕事は一つだけ。不完全な申請を通さないこと。

「前回は『門番を立てる』で、今回は『爆弾の部品を制限する』。言い方は違いますけど、やってることは同じですね」

「同じだ。不正な状態のオブジェクトを存在させない。それだけのことだよ」

PCを閉じた。3週間前のように飛び出す気分ではなかった。もう少し考えたいことがある。

「ロックさん」

「何だ」

「前回、僕は Builder を学んで万能の道具を手に入れた気分で帰りました。全クラスに適用しようとした。でも今日来て、Builder は道具であって目的じゃないってわかりました。大事なのは『なぜそれで問題が消えるのか』を理解することですよね」

ロックさんがエナジードリンクの新しい缶を開けた。泡が小さく弾けた。

「……成長したじゃないか」

「次に変なにおいのするコードを見つけたら、まず自分で嗅ぎ分けてみます」

ロックさんは窓の外を見ていた。

「この3週間で、私のところには7件の悪臭事件が持ち込まれた。他人の皿に手を伸ばす Feature Envy。列車のように連なる Law of Demeter 違反。仕事を丸投げする Middle Man。遺産を拒む Refused Bequest。溶岩に埋もれた Dead Code。名前のない証拠品 Magic Numbers。そして今日の時限装置だ」

少し間が空いた。

「どれも根は同じだよ。コードが自分の責任を知らない。それがすべての悪臭の正体だ」

僕は立ち上がった。前回のように駆け出さない。ドアの前で振り返った。

「ありがとうございました、ロックさん」

ロックさんが缶を傾けたまま、こちらを見た。

「ケン。鼻を鍛えたまえ」

ケン、と呼ばれたのは初めてだった。ワトソン君でも巻物の君でもなく。

少し驚いてから笑って、ドアを閉めた。


探偵の調査報告書

容疑(アンチパターン)真実(パターン)証拠(効果)
Temporal Coupling(時間的結合)。init()configure()start() の暗黙的な呼び出し順序依存。順序を間違えると実行時エラーまたはサイレントな誤動作が発生する。不完全な状態のオブジェクトが存在しうることが根本原因。required 属性 + BUILD 検証 + Builder パターン。必須データをコンストラクタ引数で強制し、複数属性間の不変条件を BUILD で検証し、段階的構築が必要な場合は Builder で build() 時にバリデーションを集約する。不完全な状態のオブジェクトが構造的に存在できなくなった。メソッドの呼び出し順序に関する暗黙の知識が不要になり、new が成功した時点でオブジェクトは完全かつ不変。フラグによる防御(対症療法)が不要になった。

推理のステップ

  1. 時限装置を見つける: メソッド名に init, setup, configure, start が含まれるクラスを探す。これらは Temporal Coupling のシグナルだ。
  2. 必須データを required にする: new が完了した時点でオブジェクトが完全な状態になるように、必須の属性に required => 1is => 'ro' を指定する。
  3. BUILD で不変条件を検証する: 複数属性間の関係(budget は正の数、end_date は start_date より後、など)を BUILD メソッドに集約する。
  4. 必要に応じて Builder を導入する: 属性が多い場合や段階的な構築が必要な場合に限り、Builder パターンで Fluent Interface を提供する。build() がバリデーションの最終関門となる。
  5. 過剰適用を避ける: 属性が少ないクラスに Builder を導入するのは過剰設計。required + BUILD で十分なケースを見極めること。

ロックより

ケン。3週間前、君は Builder を万能の武器と信じて帰っていった。今日、同じ武器が別の事件を解くのを見て、「なぜ解けるのか」を問い始めた。

Temporal Coupling の正体は、不完全な状態のオブジェクトが存在できてしまう設計にある。init の後に configure を呼ばなければならないのは、new が仕事を怠っているからだ。コンストラクタが責任を全うすれば、フラグもいらない。順序もいらない。オブジェクトは生まれた瞬間から完全であるべきだ。

7つの事件を終えて、1つだけ伝えておこう。コードの悪臭には種類こそ多いが、根はいつも同じだ。コードが自分の責任を知らないこと。責任の境界が曖昧なコードは、必ず隣の領域に手を伸ばし、呼び出し順序に依存し、不完全な状態で漂流する。

鼻を鍛えたまえ。次はきっと、自分で気づけるはずだ。

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