「そもそも、なんで変数の型が勝手に変わるんですか!? 他の言語じゃありえないのに!」
私の叫び声は、雑居ビルの一室に響き渡った。 ここは「レガシー・コード・インベスティゲーション(LCI)」。熱暴走ギリギリのPCの排熱と、飲みかけのエナジードリンクの香りが充満する怪しげな探偵事務所だ。 私は最近、型に厳密な別システムへのAPI連携(JSON出力)を担当することになった中堅エンジニア。だが、この数日、原因不明のエラーに完全に心を折られかけていた。
「落ち着きたまえ、ワトソン君」
奥の回転椅子から、時代錯誤なツイードのジャケットを着た男――コード探偵ロックが、ゆっくりとこちらを向いた。手には、キーボードの『Enter』キーだけをやたらと強く叩き抜いた痕跡がある使い込まれたHHKBが握られている。
「君の主張は分かった。だが、変数は勝手に変わったりなどしない。君たちが彼らを『裸のまま街(システム)を歩かせている』から、見知らぬ誰かに帽子を被らされたり、靴を履かされたりして、元の姿が分からなくなっているだけなのだよ」
ロックはニヤリと笑った。
「今日は、その『偽造された身分証(Primitive Obsession)』の正体を暴き、彼らに絶対脱げない『制服』を着せてやろうじゃないか」
I. 現場検証:コードの指紋

「まずは現状のコード(現場)を見せてもらおうか。ホシはどのようにして、その姿を変えているのかな?」
私はノートPCを開き、問題のコードをロックに見せた。 ただのユーザーデータをJSONにエンコードして出力するだけの、ごく平均的なPerlのコードだ。
| |
「連携先からは、『日によって年齢が文字列になってる』とか『電話番号が数値になっていて先頭の0が消えてる』って怒られるんです。ログ出力のために文字列結合しただけなのに、なんでJSONの出力結果まで変わっちゃうんですか!」
ロックは満足げに頷いた。
「初歩的なにおい(コードスメル)だよ、ワトソン君。Perlのスカラー変数($var)は、ただの『箱』だ。文字列を入れれば文字列になり、計算すれば数値になる。ここまでは君も知っているだろう?」
「ええ、便利ですよね」
「だが、それがJSONモジュールのような厳格な審査官(シリアライザ)の前に出た時、事件は起きる。箱の中身が『最後にどう扱われたか(内部フラグ:IV/NV/PV)』によって、出力される型が決まってしまうからだ」
ロックは立ち上がり、ホワイトボードに書き殴った。
「君のコードの変数は、言うなれば『誰でも手書きできる紙切れの身分証』だ。どこかで誰かが + 1 しただけで『私は数値です』という顔になり、. で文字列結合された瞬間に『私は文字列です』と言い張る。これこそが、Primitive Obsession(プリミティブ型への執着)が引き起こす悲劇さ」
なるほど。変なところで型が変わる原因は、私が良かれと思って組み込んでいたログ出力や、ちょっとした計算のせいだったのか……。
II. 推理披露:鮮やかなリファクタリング

「じゃあ、どうすればいいんですか? 出力する直前に全部 + 0 したり "" . したりして、強制的に型を合わせる(キャストする)ハックでも入れます?」
「馬鹿なことを言うもんじゃない! そんな場当たり的な対処は、絆創膏の上から絆創膏を貼るようなものだ」
ロックは呆れたようにため息をついた。
「彼らに、絶対に脱げない、偽造不可能な「制服(Value Object)」を着せるんだ。そうすれば、どこをどう歩こうが、審査官には正しい姿として認識される」
ロックの手が、猛烈な勢いでHHKBを叩き始めた。
Value Object の定義(制服の作成)
「まずは年齢(Age)と電話番号(PhoneNumber)、そしてIDのための専用クラス(VO)を作る。今回は Moo の力を借りよう」
| |
| |
「ただの文字列や数値を保持するためだけのクラスですか? なんだか大げさな気もしますが……」
私が首を傾げると、ロックはチッチッと指を振った。
「大げさなものか。このクラスには2つの重要な責務がカプセル化されている。1つ目は BUILD による生成時のバリデーション(身元保証)。マイナスの年齢や不正な電話番号は、そもそもこのシステムに存在できなくなる。
そして2つ目。これが今回の事件の切り札、TO_JSON メソッドだ」
コンテキストの簡略化と安定化
ロックはさらにコードを書き換え、元の処理を置き換えた。
| |
「JSONモジュールに convert_blessed オプションを渡すと、モジュールはオブジェクトに対して TO_JSON メソッドを呼び出し、その戻り値をシリアライズの対象にする。
この TO_JSON の中で + 0(数値強制)や "" .(文字列強制)を行えば、途中で誰が文字列結合しようが計算しようが、出力される瞬間の型は、そのクラス自身が責任を持って保証するというわけだ」
sequenceDiagram
participant App as アプリケーション
participant VO as User::Age (Value Object)
participant JSON as JSONモジュール
App->>VO: new(value => 25)
VO-->>App: (オブジェクト)
App->>App: ログ出力等の操作(内部フラグ変動)
App->>JSON: encode($user)
JSON->>VO: TO_JSON()
Note over VO: return $self->value + 0
VO-->>JSON: 25 (厳密な数値)
JSON-->>App: '{"age":25}'
III. 解決:事件の終わり
私は息を呑んだ。
たかが文字列一つ、数値一つをクラスにするなんて「大げさ」だと思っていた。 しかし、至る所に散らばっていた「型を合わせるためのハック」や「同じ正規表現での重複したバリデーション」が、すべて1つの小さなクラス(Value Object)に綺麗に収まっているのを見せつけられ、それまでの自分の考えがいかに浅はかだったかを思い知った。
どこでどう使われようとも、その変数は「自分は数値の年齢だ」「自分は文字列の電話番号だ」という絶対的なアイデンティティを保ち続ける。もう他のシステムが異常終了する恐怖に怯える必要はないのだ。
「プリミティブ型は便利だが、それは無法地帯の便利さだよ。意味のあるデータには、意味のある名前を与え給え」
ロックが冷めたエナジードリンクを飲み干し、キザな台詞で事件を締めくくった。
「……なるほど。そういうことだったんですね!」
私の胸に、遅れてきた雷のようなインスピレーションが落ちた。 そうだ。生のデータがこれほどまでに脆弱で扱いづらいものならば、逆に言えば、すべてのデータに強固な『制服』を着せてしまえばいい。システム内のすべての変数が、自分自身の正しさを保証してくれる世界——それこそが、バグのない完璧なユートピアではないか! 生来の几帳面さが、パズルの最後のピースがはまったことで大爆発を起こした。
「つまり、ユーザー情報を構成するこの『名前』も、この『フラグ』も、『備考欄』も……! 全部 Value Object のクラスを作っちゃえば、もう絶対にバグらない完璧なシステムになりますね!! さっそく100個くらいクラス作ってきます!!」
勢いよく立ち上がり、目を輝かせる私を見て、ロックは深く頭を抱えて呻いた。
「……極端から極端へ走るのは君の悪い癖だ、ワトソン君。何事にも限度というものがある。すべての路地裏に信号機を立てる必要はないのだよ……」
探偵の調査報告書
| 容疑(アンチパターン) | Primitive Obsession(プリミティブ型への執着) |
|---|---|
| 真実(パターン) | Value Object(値オブジェクト) |
| 証拠(効果) | 型の固定、不正な値の混入防止とバリデーションの集約、シリアライズ(JSON化)の振る舞いのカプセル化 |
推理のステップ
- ドメインの概念を見つけ出す: 「ただの文字列」「ただの数値」ではなく、それがシステム上で「電話番号」なのか「年齢」なのかを見極める。
- 専用のクラス(VO)を定義する:
Mooなどを用いてクラス化し、生成(BUILD)時に厳格なバリデーションを行う。 - 振る舞いをカプセル化し、不変にする:
TO_JSONのような特定の出力フォーマットに対する振る舞いや、値の比較ロジックをクラス内に閉じ込める。外部から値が上書きされないよう、アトリビュートは Read-Only(ro)にする。
ロックより
今回は「JSON出力時の型のブレ」という、Perl特有とも言える泥臭い事件にお目にかかれて実に楽しかったよ。 Value ObjectはGoFのデザインパターンというわけではないが、ドメイン駆動設計(DDD)などにおいて極めて強力な「におい消し」となるアイテムだ。
だが忘れないでくれたまえ。何でもかんでもクラス化すればいいというものではない。「その概念が、システム内で独立した振る舞いや制約を持つべきか」を見極めることこそが、真の探偵(プログラマー)の仕事なのだから。
次なる「コードのにおい」が漂う時まで、しばしのお別れだ。
