本文へ移動
cccskills
無料GitHub で公開

when-to-wrap-primitives

プリミティブ型をドメイン固有型でラップすべきか否かの判断を支援する。 Primitive Obsession(プリミティブ型の過剰使用)と Value Object Obsession(過剰なラップ)の 両極端を避け、投資対効果に基づく合理的な判断基準を提供する。 Value Objectの定義が文脈(PofEAA/DDD/一般)で異なることによる用語混乱の防止も含む。 コードレビュー、新規実装、設計議論時にプリミティブ型のラップ判断が必要な場合に使用。 対象言語: 言語非依存(Rust, TypeScript, Java, Kotlin, Scala, Go, Python等すべて)。 トリガー:「この値をラップすべきか」「プリミティブ型のままでいいか」 「Value Objectにすべきか」「型を作りすぎでは」「Primitive Obsession」 「ラップしすぎ」「型が多すぎる」「Stringのままでいいか」 といったプリミティブ型ラップ判断関連リクエストで起動。

インストール方法を見る

含まれるファイル(1)

  • SKILL.md14.4 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

プリミティブ型ラップ判断ガイド

プリミティブ型をラップすべきか否かは、コスト対効果で判断する。盲目的にラップするのも、 一切ラップしないのも、どちらも設計の失敗である。

前提: Value Objectの定義は1つではない

「Value Object」という用語は文脈によって意味が異なる。議論やレビューで混乱が生じる主因である。

定義出典スコープ核心
一般的定義Wikipedia等最も広い同等性がIDではなく値に基づくオブジェクト
PofEAA定義Martin Fowler実装パターンIDに基づかず値で等価判定される小型オブジェクト。別名参照問題を避けるため不変が推奨
DDD定義Eric EvansドメインモデリングPofEAA版の特性をすべて備えた上で、ドメインの概念を計測・定量化・説明し、不変条件と副作用のない振る舞いを持つドメインオブジェクト

DDD版はPofEAA版のextends

DDD版VOとPofEAA版VOは独立した概念ではなく、特化(specialization)の関係にある。

特性PofEAA VODDD VO
値による等価判定必須必須(継承)
不変性推奨必須(強化)
ドメイン不変条件ー必須(追加)
ドメイン振る舞いー必須(追加)

つまり:

  • DDD VO IS-A PofEAA VO → すべてのDDD VOはPofEAA VOでもある
  • PofEAA VO IS-A DDD VO → 成立しない(Ruby HashはPofEAA VOだがDDD VOではない)

DDD版は「値で等価判定される」「不変である」というPofEAA版の特性を前提として含んだ上で、 ドメイン固有の要件を追加したものである。2つの定義を並列に見ると、DDD版が PofEAA版の特性も持っていることを見落としやすいので注意。

チーム内では「PofEAAのVO」「DDDのVO」のように文脈を明示して使い分けるべき。

本スキルの立場

本スキルでは、「プリミティブ型をドメイン固有型でラップすべきか」という実践的判断に焦点を当てる。 VOの定義論争には立ち入らず、ラップすることで得られる具体的な利益がコストに見合うかで判断する。

2つのアンチパターン

Primitive Obsession(プリミティブ型への固執)

すべてをString/int/floatで表現し、ドメインの制約がコードに表れない。

fn transfer(from: String, to: String, amount: f64)
// fromとtoを取り違えてもコンパイルが通る
// amountが負でもコンパイルが通る
// 通貨の概念がない

症状:

  • 同じプリミティブ型の引数が2つ以上並ぶ
  • バリデーションロジックが呼び出し側に散在
  • 「この文字列はメールアドレスのはず」という暗黙の前提
  • 単位の取り違え(メートルとフィート、円とドル)

Value Object Obsession(過剰なラップ)

すべてのプリミティブ型を機械的にクラスで包み、複雑性だけが増す。

class CustomerFirstName(value: String)
class CustomerLastName(value: String)
class ShippingFirstName(value: String)  // CustomerFirstNameと何が違う?
class ShippingLastName(value: String)   // 同上

症状:

  • 不変条件やドメインロジックを持たない「ただのラッパー」が大量に存在
  • 文脈ごとに別の型を作るが、中身のバリデーションは同一
  • 型変換のボイラープレートがドメインロジックより多い
  • 新規メンバーが型の森で迷子になる

判断フレームワーク

5つの判断基準

プリミティブ型をラップすべきかを以下の基準で評価する。 1つでも強くYesならラップを検討。複数Yesなら強く推奨。すべてNoならラップ不要。

基準1: ドメイン不変条件があるか

その値に「常に満たすべき制約」があるか?

例不変条件判断
メールアドレスRFC準拠のフォーマットラップする
金額非負、通貨との組み合わせラップする
年齢0〜150の範囲ラップする
ログメッセージ特に制約なしラップ不要
一時的なループカウンタなしラップ不要

基準2: 取り違えリスクがあるか

同じプリミティブ型の引数が複数並び、取り違えてもコンパイラが検出できないか?

// ❌ userIdとorderIdは両方String。取り違えても気づかない
fn find_order(user_id: String, order_id: String)

// ✅ 型が異なるのでコンパイラが検出する
fn find_order(user_id: UserId, order_id: OrderId)

特にID型は取り違えの被害が大きい。異なる概念のIDは型で区別すべき。

基準3: ドメイン操作を集約できるか

その値に関連する操作(加算、比較、変換など)がドメインロジックとして存在するか?

例ドメイン操作判断
Money加算(同一通貨のみ)、通貨変換、比較ラップする
DateRange重複判定、包含判定、期間計算ラップする
単なる名前文字列特になし慎重に検討

基準4: 複数の値が不可分か

2つ以上の値が常にセットで意味を持つか?

// ❌ 金額と通貨が別々に渡される → 不整合の可能性
fn price(amount: f64, currency: String)

// ✅ 不可分な値を1つの型にまとめる
fn price(money: Money)

基準5: 利用箇所が複数あるか

その型が複数のコンテキスト(モジュール、レイヤー、関数)で使われるか?

状況判断
3つ以上のモジュールで使われるラップの価値が高い
2つのモジュールで使われる他の基準と合わせて検討
1つの関数内でしか使わないラップ不要(YAGNI)

判断フロー

プリミティブ型を使おうとしている
    │
    ├─ 不変条件がある? ─── Yes ──→ ラップする
    │                                (domain-primitives-and-always-valid スキル参照)
    │
    ├─ 取り違えリスクがある? ─── Yes ──→ ラップする(特にID型)
    │
    ├─ ドメイン操作がある? ─── Yes ──→ ラップする
    │
    ├─ 複数値が不可分? ─── Yes ──→ 複合型としてラップする
    │
    ├─ 利用箇所が多い? ─── Yes ──→ 他の基準と合わせて検討
    │
    └─ すべて No ──→ プリミティブ型のままでよい

ラップの度合い: 3段階

すべてを「フルスペックの型」にする必要はない。状況に応じて適切な粒度を選ぶ。

レベル1: 型エイリアス(最軽量)

不変条件はないが、取り違え防止や可読性向上が目的の場合。

// TypeScript
type UserId = string & { readonly __brand: unique symbol };
type OrderId = string & { readonly __brand: unique symbol };
// Rust(newtype)
pub struct UserId(String);
pub struct OrderId(String);

適用場面: 不変条件なし、取り違え防止が主目的

レベル2: 構築時検証付き型

不変条件があり、無効な値のインスタンス化を防ぐ。

pub struct Email(String);

impl Email {
    pub fn new(value: &str) -> Result<Self, EmailError> {
        if !value.contains('@') || value.len() <= 3 {
            return Err(EmailError::InvalidFormat);
        }
        Ok(Self(value.to_string()))
    }
}

適用場面: 明確な不変条件がある

レベル3: 振る舞いを持つドメイン型

不変条件 + ドメイン操作を集約する。

pub struct Money { amount: Decimal, currency: Currency }

impl Money {
    pub fn add(&self, other: &Money) -> Result<Money, MoneyError> {
        if self.currency != other.currency {
            return Err(MoneyError::CurrencyMismatch);
        }
        Money::new(self.amount + other.amount, self.currency)
    }
}

適用場面: 不変条件 + そこに属すべきドメイン操作がある

言語特性による選択肢

ラップの手段はクラス化だけではない。言語機能に応じて適切な手段を選ぶ。

言語軽量な手段フルラップ
Rustnewtype(struct Foo(T))newtype + impl
TypeScriptBranded Typesclass with private constructor
Kotlinvalue classdata class
Scalaopaque type / value classcase class
Gotype Foo stringstruct + コンストラクタ関数
Javaー(軽量手段がない)record / final class
PythonNewType(typing)dataclass(frozen=True)

原則: 不変条件がなく取り違え防止だけが目的なら、その言語で最も軽量な手段を使う。

レビューチェックリスト

「ラップすべきなのにしていない」の検出

  • 同じプリミティブ型の引数が2つ以上並んでいないか
  • バリデーションが呼び出し側に散在していないか
  • 「この文字列は〇〇のはず」という暗黙の前提がないか
  • 異なる概念のIDが同じ型(String等)で表現されていないか
  • 不可分な値のペア(金額+通貨等)が別々の引数になっていないか

「ラップしすぎ」の検出

  • 不変条件もドメイン操作もない「ただのラッパー」がないか
  • 型変換のボイラープレートがドメインロジックより多くないか
  • 文脈ごとに型を分けたが、中身のバリデーションが同一ではないか
  • 1箇所でしか使わない型をわざわざ作っていないか
  • 型の数がチームの認知負荷を超えていないか

関連スキルとの使い分け

スキルフォーカス使うタイミング
本スキルラップすべきか否かの判断「この型を作るべきか?」という意思決定時
domain-primitives-and-always-validラップする場合の設計と実装ラップすると決めた後の具体的な設計時
domain-building-blocksVO/Entity/Aggregate等の設計全般ドメインモデル全体の構造を設計するとき
parse-dont-validate検証結果を型で保持する変換validate→parse変換をレビューするとき

議論のための用語整理

チームで議論する際、パターン(設計概念)と実装技法を混同しないことが重要である。

パターン(何であるか)

パターン出典定義関係
Value Object(PofEAA)Fowler値で等価判定されるオブジェクト。不変が推奨基底
Value Object(DDD)EvansPofEAA VOを継承し、ドメイン不変条件と振る舞いを追加PofEAA VOのextends
Domain PrimitiveJohnsson et al.構築時検証・不変性・自己完結性を備えたドメイン固有の最小単位の型Secure by Design由来

実装技法(どう作るか)

技法特性例
Branded Type / Newtype型レベルで区別するが、ランタイム検証なしRust: struct UserId(String)
Smart Constructor構築時に不変条件を検証。無効な値を作れないEmail::new(s) -> Result<Email, Error>
振る舞い付きドメイン型不変条件 + ドメイン操作をカプセル化Money::add(&self, other) -> Result<Money, Error>

パターンと実装技法の対応

パターン典型的な実装技法
PofEAA VOいずれの技法でも実現可能(値の等価性を実装すればよい)
DDD VOSmart Constructor または 振る舞い付きドメイン型
Domain PrimitiveSmart Constructor(最小単位なので振る舞いは少ないことが多い)

推奨: 「Value Objectにすべき」のようにパターン名だけで語らず、 「この値にはドメイン不変条件があるからSmart Constructorで保護すべき」 「取り違えリスクがあるからNewtypeで型を区別すべき」のように、理由と実装技法をセットで述べる。

参考文献

  • Dan Bergh Johnsson et al. "Secure by Design" - Domain Primitivesの原典
  • Martin Fowler "Patterns of Enterprise Application Architecture" - PofEAA版Value Objectの定義
  • Eric Evans "Domain-Driven Design" - DDD版Value Objectの定義
  • J. B. Rainsberger "Demystifying the Dependency Inversion Principle" - Primitive Obsessionへの言及
  • ThoughtWorks "Object Calisthenics" - 「Wrap All Primitives」ルールの出典(練習用であり本番ルールではない)

関連スキル(併読推奨)

このスキルを使用する際は、以下のスキルも併せて参照すること:

  • domain-primitives-and-always-valid: ラップする判断後の具体的な設計パターン
  • parse-dont-validate: ラップ時に適用するparseパターン
  • domain-building-blocks: ラップされたプリミティブが属する値オブジェクトの設計

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

aggregate-design

無料日本語概要

DDDの集約(Aggregate)設計ルールに基づくコードレビュー・設計支援・リファクタリングを行う。 Evans Rules、Vernon's 4 Rules、Design by Contractに基づき、集約の境界定義、不変条件の検証、 不変(Immutable)設計、ID参照、結果整合性、ドメインイベント連携を包括的にガイドする。 以下のいずれかに該当する場合は必ずこのスキルを使用すること: - 集約(Aggregate)の新規設計・実装・リファクタリング(どの言語でも) - 既存の集約やエンティティクラスのDDD観点でのコードレビュー - 集約の境界決定(「AとBは同じ集約にすべきか?」「この集約は大きすぎるか?」) - 集約内の不変条件・整合性境界の設計 - 集約間の連携方式の判断(ドメインイベント、結果整合性、Sagaパターン) - 可変(Mutable)な集約コードを不変(Immutable)設計にリファクタリングする - publicフィールド、直接参照、push/appendなどカプセル化違反の検出・修正 キーワード例:集約、Aggregate、aggregate boundary、集約ルート、AggregateRoot、 エンティティ設計、DDD実装、Vernon Rules、Evans Rules、集約の分割、真の不変条件

j5ik2o/okite-ai802026年4月25日 更新

aggregate-transaction-boundary

無料日本語概要

集約とトランザクション境界の関係を明確化し、複数集約を単一トランザクションに含めるアンチパターンを 検出・是正する。集約は強い整合性境界であり、ユースケースで複数集約を更新する場合は結果整合性を 使うべきという原則を適用する。コードレビュー、ユースケース設計、リファクタリング時に トランザクション境界の問題を検出する場合に使用。 対象言語: 言語非依存(Java, Kotlin, Scala, TypeScript, Go, Rust, Python等すべて)。 トリガー:「複数集約を同じトランザクションで更新している」「ユースケースに@Transactionalがある」 「集約間の整合性をどう取るか」「Sagaパターンを使うべきか」「トランザクション境界の設計」 「1トランザクション1集約」「結果整合性の実装」「集約をまたぐトランザクション」 といったトランザクション境界関連リクエストで起動。

j5ik2o/okite-ai802026年4月25日 更新

backward-compat-governance

無料日本語概要

後方互換性がゴミコードを量産する構造を検出し、互換性を「契約と撤去計画」として管理する ガバナンスを支援するスキル。公開API境界の明確化、非推奨化サイクル(deprecation cycle)の 制度化、互換層の局所化(Adapter/Strangler Fig)、契約テスト(CDC)による互換性検証、 AI生成コードの互換性ゲート設計を含む。コードレビュー、API設計、リファクタリング、 レガシー移行時に互換性起因の技術的負債を防ぐために使用。 対象言語: 言語非依存(Java, TypeScript, Go, Python, Rust等すべて)。 トリガー:「後方互換性を保ちたい」「非推奨APIをどうする」「互換層が増えてきた」 「レガシー移行の戦略」「API設計レビュー」「互換性のためのコードが多い」 「deprecation policyを作りたい」「破壊的変更の管理」といった互換性管理関連リクエストで起動。

j5ik2o/okite-ai802026年4月25日 更新

breach-encapsulation-naming

無料日本語概要

getterの濫用を防ぐための命名規約スキル。ドメインモデルでgetterが必要な場合(永続化、JSON変換など)に `breachEncapsulationOf` プレフィックスを付与することで、カプセル化を破っていることを明示する。 これにより、Tell Don't Ask原則の違反を未然に防ぎ、getterの意図しない使用を抑制する。 コードレビュー、新規実装、リファクタリング時にgetter設計が必要な場合に使用。 対象言語: Java, Kotlin, Scala, TypeScript, Python, Go, Rust。 トリガー:「getterの命名規約」「カプセル化を破るgetter」「永続化用のgetter」 「breachEncapsulation」「getterを作りたいが濫用を防ぎたい」といったgetter命名関連リクエストで起動。

j5ik2o/okite-ai802026年4月25日 更新

clean-architecture

無料日本語概要

クリーンアーキテクチャを採用しているプロジェクト向けの設計・レビュー支援。4層構造(ドメイン層、 ユースケース層、インターフェースアダプタ層、インフラストラクチャ層)に基づく。特にインフラ層は 横断的関心事(ロギング、設定管理)のみ、永続化やRPCはインターフェースアダプタ層に配置すべき という原則を適用する。トリガー:「クリーンアーキテクチャで」「クリーンアーキテクチャに従って」 「クリーンアーキテクチャのレビュー」など、クリーンアーキテクチャを明示的に指定した場合のみ起動。 一般的な「設計レビュー」「アーキテクチャ相談」では起動しない。

j5ik2o/okite-ai802026年4月25日 更新

cqrs-aggregate-modeling

無料日本語概要

CQRS/ESが集約の境界定義とモデリングに与える影響を解説する。CQRSを導入すると集約は コマンド実行に必要な最小限の状態のみ保持すればよくなり、読み取り責務はリードモデルに 委譲できる。大きすぎる集約の軽量化、集約境界の再定義、イベントによる状態管理を支援する。 集約設計、CQRS導入時のモデリング見直し、パフォーマンス問題の解決時に使用。 対象言語: 言語非依存。 トリガー:「CQRSで集約が変わる」「集約が大きすぎる」「集約にメッセージ1000件」 「集約の更新が重い」「CQRS導入で集約を見直す」「集約を軽量化したい」 「集約にクエリ用データが混ざっている」「集約の境界を再定義」 といったCQRS/モデリング関連リクエストで起動。

j5ik2o/okite-ai802026年4月25日 更新

j5ik2o のスキルをすべて見る

このスキルの問題を報告する