見覚えのある雑居ビルの階段を上った。
3週間前、僕は引数リストを印刷した紙——ロックさんに「パピルス」と呼ばれた巻物——を握りしめて、この階段を駆け上がったのだ。あの日、Builder パターンを教わり、「全クラスに Builder を追加します!」と意気揚々に叫んで帰った。
今日は巻物じゃなくてノートPCだけ。足取りは前回ほど速くない。恥ずかしさの分だけ、重い。
「レガシー・コード・インベスティゲーション(LCI)」のプレートは3週間前と変わらず、くすんだまま掲げてある。もう怪しいとは思わない。怪しいのは最初からわかっている。
ドアを開けた。
デスクの上に、エナジードリンクの空き缶が3段に積み上げられていた。一番下に太い缶、真ん中に細い缶、一番上にミニ缶。ロックさんは椅子にふんぞり返ったまま、その構造物をピンセットでつついている。
「おや、巻物の君か」
缶から目を上げないまま言った。
「ケンです。……もう訂正しても無駄ですよね」
「名前は覚えているよ、ワトソン君。巻物の方が印象的だというだけだ。——で、次は何だ? パピルスからロゼッタストーンにでも昇格したか?」
「いえ、今回は紙じゃなくてコードです。またやらかしました」
ロックさんはピンセットを置き、缶のタワーを指さした。
「見たまえ。この構造物は下から順に開けなければ崩壊する。一番上のミニ缶を先に取ると、バランスが崩れて全部倒れる。便利だろう?」
「それ、ただのゴミタワーでは……」
「順序が大事な構造物だ。逆から手をつけたら崩壊する。……それが今日の事件と無関係だとは言わないよ」
僕は一瞬黙った。3週間前なら「何言ってんだこの人」と心の中で突っ込んでいただろう。今日は違う。この人の奇行には必ず意味がある——ことがある。ないこともあるが。
現場検証:時限装置の構造
ノートPCを開き、問題のコードを見せた。
「前回 Builder を教わったあと、社内の他のコードも見直してみたんです。そしたら配信エンジンにこういうクラスがあって……」
【Before】呼び出し順序に依存する DeliveryEngine
| |
「init → configure → start の順番で呼ばないと動かないんです。先週テスト環境で配信設定を作るとき、configure をうっかり飛ばして start を呼んでしまって——」
「また事故か」
「未遂です! テスト環境だったので! でも本番だったらと思うと……」
ロックさんが画面に目を落とした。_initialized、_configured のフラグを指先でなぞる。
「これは……時限装置だよ、ワトソン君」
「時限装置?」
「init という名の安全装置を解除し、configure という名の起爆準備を完了し、start で爆発させる。正しい順序でなければ不発弾になる」
「爆弾って……プログラムですよね?」
「不発弾は一番危険な爆弾だ。今回は croak でエラーになったから気づけた。だがもしフラグチェックがなかったら? campaign_id が undef のまま配信が始まっていたかもしれない。エラーにすらならず、黙って壊れる。サイレントな不発弾だ」
| |
「でもロックさん、これって前回の Builder で解決できるんじゃないですか? init と configure をメソッドチェーンで繋げば——」
ロックさんが首を振った。
「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つ目の鍵。不完全な状態のオブジェクトを、そもそも存在させない」
| |
「init() と configure() が消えた……?」
「消えたのではない。コンストラクタに吸収された。campaign_id と target は required => 1 だ。これらなしには new が完了しない。つまり new が成功した時点で、オブジェクトは完全な状態だ」
「でも budget や ad_format はオプションですよね。全部 required にしたら不便じゃ……」
「default があるだろう。デフォルト値があるということは、それは必須ではないということだ。しかし campaign_id と target にデフォルトはあるかね?」
「ない……です」
「ならば required だ。設計とはそういうことだよ」
そして全属性が is => 'ro'——読み取り専用になっている。
「あ、rw が全部 ro になってる。前回のときは Builder の内部属性は rw でしたよね?」
「あちらは Builder だ。設定途中の状態を保持する必要があるから rw にした。しかし完成品の DeliveryEngine は違う。完成品の状態は、生成後に変更されてはならない。前回の事件でも Campaign 本体は ro だっただろう?」
そう言われて前回を思い出した。確かに Builder は rw だったが、Campaign 自体はすべて ro だった。あのときは気にしていなかったけれど、それ自体が意図的な設計だったのか。
「でも全部 ro にしたら、実行中に設定を変えたいときどうするんですか。配信エンジンって、途中で予算を変更したりしません?」
「変えるな。新しく作れ」
ロックさんは一言で切り捨てた。
「budget を変えたければ、新しい DeliveryEngine を budget だけ変えて作り直す。元のオブジェクトは壊さない。これを不変オブジェクトという。変更のたびに新品を作れば、古いオブジェクトの状態が汚染される心配はない」
鍵2:BUILD 検証(複数属性間の不変条件)
「2つ目の鍵。属性同士の関係を縛る」
| |
「これって前回の build() メソッドのバリデーションと同じですか?」
「本質は同じだ。build() はBuilder 側の門番。BUILD は Moo が用意したオブジェクト側の門番。二重の防壁だ。しかし今回のように属性が5つ程度なら、Builder を介さずとも BUILD だけで十分な場合がある」
「え、じゃあ Builder いらないんですか?」
「3つ目の鍵を見せよう」
鍵3:Builder パターン(段階的構築)
「属性が増えた場合や、設定を段階的に組み立てたい場合には、前回の友人に再登場してもらう」
| |
「あ、前回の CampaignBuilder と同じ構造ですね。$self を返してチェーンする……Fluent Interface ですか」
「よく覚えているじゃないか」
ロックさんが僅かに口角を上げた。称賛とも皮肉とも取れる表情だ。
「だが一つ区別しておきたまえ。Fluent Interface はメソッドチェーンの見た目の話だ。Builder はオブジェクト生成の責任の話だ。見た目が似ていても、目的が違う」
使い方はこうなる。
| |
「前回は『引数が多すぎる問題を Builder で解決した』。今回は『順序依存を Builder で解決した』。同じ Builder なのに解決する問題が違う……」
「Builder の本質は『完全な状態でなければ生成しない』という門番だ。引数地獄であれ、順序地獄であれ、門番の仕事は変わらない。不完全な申請は通さない。それだけだ」
ふと、前回の最後にロックさんに言われたことを思い出した。
「前回、ロックさんは『蝶を捕まえるのに大砲を使うな』って言いましたよね。今回はどっちですか? 大砲? 蝶?」
「いい質問だ」
ロックさんが缶タワーの残骸を片付けながら言った。
「属性が5つのこのクラスなら、鍵1と鍵2——required と BUILD だけで十分だ。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でテストを実行した。
| |
画面に結果が並んだ。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 が成功した時点でオブジェクトは完全かつ不変。フラグによる防御(対症療法)が不要になった。 |
推理のステップ
- 時限装置を見つける: メソッド名に
init,setup,configure,startが含まれるクラスを探す。これらは Temporal Coupling のシグナルだ。 - 必須データを
requiredにする:newが完了した時点でオブジェクトが完全な状態になるように、必須の属性にrequired => 1とis => 'ro'を指定する。 BUILDで不変条件を検証する: 複数属性間の関係(budget は正の数、end_date は start_date より後、など)をBUILDメソッドに集約する。- 必要に応じて Builder を導入する: 属性が多い場合や段階的な構築が必要な場合に限り、Builder パターンで Fluent Interface を提供する。
build()がバリデーションの最終関門となる。 - 過剰適用を避ける: 属性が少ないクラスに Builder を導入するのは過剰設計。
required+BUILDで十分なケースを見極めること。
ロックより
ケン。3週間前、君は Builder を万能の武器と信じて帰っていった。今日、同じ武器が別の事件を解くのを見て、「なぜ解けるのか」を問い始めた。
Temporal Coupling の正体は、不完全な状態のオブジェクトが存在できてしまう設計にある。init の後に configure を呼ばなければならないのは、new が仕事を怠っているからだ。コンストラクタが責任を全うすれば、フラグもいらない。順序もいらない。オブジェクトは生まれた瞬間から完全であるべきだ。
7つの事件を終えて、1つだけ伝えておこう。コードの悪臭には種類こそ多いが、根はいつも同じだ。コードが自分の責任を知らないこと。責任の境界が曖昧なコードは、必ず隣の領域に手を伸ばし、呼び出し順序に依存し、不完全な状態で漂流する。
鼻を鍛えたまえ。次はきっと、自分で気づけるはずだ。
