原始的執着 — 何でも string と int で表す
メールも金額もユーザーIDも生のstring/intで持つと、なぜ検証が散らばり取り違えが起きるのかが分かる。値オブジェクトで意味を型に乗せる設計の初手まで。
- 原始的執着(Primitive Obsession)は、意味を持つ概念を string や int などの基本型のまま扱い続けること。メール・金額・郵便番号がすべてただの文字列や数値になる。
- 害は2つ。妥当性の検証が使う側のあちこちに散らばること、そして同じ型どうしの取り違え(user_idの位置に order_idを渡す等)をコンパイラが防げないこと。
- 直し方は値オブジェクト(Value Object)。Email や Money のような専用の型を作り、生成時に検証し、その型でしか受け取らないようにする。意味を型に乗せる。
メールアドレスは string、金額は int、ユーザーIDも string。どれも基本型で表せてしまうので、つい生のまま持ち回します。しかしこの「なんでも基本型」が、検証の散乱と取り違えを静かに招きます。
横にスクロール
症状:どう気づくか
- 関数の引数が
(string, string, string, int)のように、基本型ばかりが並ぶ。 - メール形式や金額が負でないかのチェックが、呼び出し側のあちこちに重複している。
- 「引数の順番を間違えて別のIDを渡していた」類のバグが起きる。
function transfer(fromId: string, toId: string, amount: number) { ... }
// 呼ぶ側:順番を間違えても型が同じなので誰も気づかない
transfer(toId, fromId, amount); // from と to が逆!でもコンパイルは通る
なぜ悪いのか
第一に、検証が散らばる。「メールは形式が正しいか」「金額は負でないか」というルールは、その値の性質そのものなのに、基本型で持つと使う側それぞれがチェックすることになります。1箇所でもチェックを忘れれば、不正な値がすり抜けます。
第二に、取り違えを型が防げない。fromId も toId も同じ string なので、引数の順番を間違えても、user_id を期待する場所に order_id を渡しても、コンパイラは何も言いません。バグは実行時、しかも運が悪ければ本番で初めて表面化します。基本型は「文字列である」ことしか保証せず、「それが何の文字列か」という肝心な情報を持たないのです。
直し方
値オブジェクト(Value Object)を作る。 概念ごとに専用の型を用意し、生成時に検証し、その型でしか受け取らないようにします。
class Email {
private constructor(readonly value: string) {}
static of(raw: string): Email {
if (!raw.includes("@")) throw new Error("不正なメール: " + raw);
return new Email(raw); // 生成できた時点で「正しいメール」が保証される
}
}
class UserId { constructor(readonly value: string) {} }
class OrderId { constructor(readonly value: string) {} }
// UserId を期待する場所に OrderId は渡せない=取り違えを型が弾く
function findUser(id: UserId) { ... }
一度 Email.of() を通れば、その値はもう検証済み。以降どこで受け取っても「正しいメールである」ことが型で保証され、各所での再チェックが要りません。UserId と OrderId は別の型なので、取り違えはコンパイル時に弾かれます。
いきなり全部を値オブジェクトにしなくても構いません。効果が大きいのは、複数の基本型がいつも一緒に現れるところ。(緯度, 経度) を Coordinate に、(金額, 通貨) を Money にまとめると、引数が減って意味が明確になり、「金額だけ渡して通貨を忘れる」事故も消えます。取り違えや検証漏れが実際に起きた箇所から導入するのが現実的です。
まとめ
- 原始的執着は、意味を持つ概念を string / int のまま扱い続けること。メールも金額もIDもただの基本型になる。
- 害は、検証が使う側に散らばることと、同じ型どうしの取り違えをコンパイラが防げないこと。
- 直し方は値オブジェクト。専用の型を作り、生成時に検証し、その型でしか受け取らない。意味を型に乗せる。
- 一緒に現れる値の束(座標・金額と通貨)や、事故が起きた箇所から始めるのが現実的。
アンチパターン図鑑の記事ガイド
原始的執着 — 何でも string と int で表すを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
値オブジェクト
比較で見る軸
難易度: intermediate / カテゴリ: アンチパターン図鑑 / タグ数: 5
導入後に効く点
害は2つ。妥当性の検証が使う側のあちこちに散らばること、そして同じ型どうしの取り違え(user_idの位置に order_idを渡す等)をコンパイラが防げないこと。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- intermediate
- カテゴリ
- アンチパターン図鑑
- タグ数
- 5
判断チェックリスト
- 自社の用途が「値オブジェクト / 型」に近いか確認する。
- 強みである「原始的執着(Primitive Obsession)は、意味を持つ概念を string や int などの基本型のまま扱い続けること。メール・金額・郵便番号がすべてただの文字列や数値になる。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。