mcpField
D
domain-primitives-and-always-valid

domain-primitives-and-always-valid

github.com/j5ik2o开发8036 次安装27 周前更新
代码仓库

关于 domain-primitives-and-always-valid

domain-primitives-and-always-valid 是一项开发技能,指导用经过验证的不可变类型表达领域概念,替代不受约束的基本类型。它提供设计模式和审查要点,帮助通过类型保证不变量。

主要功能

  1. 01经过验证的领域专用类型
  2. 02不可变值设计
  3. 03智能构造器与范围约束
  4. 04封装复合值
  5. 05按语言提供实现模式
  6. 06领域不变量审查要点

适用场景

  1. 01为 ID 和联系信息设计类型
  2. 02在构造时阻止无效值
  3. 03区分含义不同的参数
  4. 04审查过度使用基本类型的领域模型

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/j5ik2o/okite-ai --skill domain-primitives-and-always-valid

安装后在 Claude Code 中输入/domain-primitives-and-always-valid确认已加载。

详细介绍

领域基元与始终有效的领域模型

不要信任原始类型,而是通过领域专属类型强制实施不变条件。

核心原则

领域基元(安全设计)

不要直接使用原始类型,而要用领域专属的最小单元类型进行封装。

特性说明
构造时验证无法使用无效值创建实例
不可变(Immutable)创建后不可更改
自包含不持有对其他实体的引用
聚合领域操作封装与该类型相关的操作
防止参数混淆即使原始类型相同,也能区分不同的领域类型
始终有效的领域模型

通过类型系统保证领域模型始终处于有效状态。

オブジェクトが存在する = そのオブジェクトは有効である

原始类型的危险性

直接使用原始类型会导致直到生产环境才会暴露的 Bug。

1. 无效值会污染系统
// ❌ プリミティブ型:無効な値が素通りする
fn transfer(from: &str, to: &str, amount: i64) {
    // 負の金額で送金 → 受取人の残高が減り、送金者の残高が増える!
    db.execute("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, from);
    db.execute("UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, to);
}

transfer("alice", "bob", -10000);  // コンパイルOK、テストも通る、本番で大損害
2. 参数混淆会逃过测试
// ❌ 同じ型の引数が並ぶと、取り違えてもコンパイラが検出できない
fn create_user(first_name: &str, last_name: &str, email: &str);

// 姓名を逆に渡している。単体テストでは「動く」ので見逃される
create_user("Smith", "John", "[email protected]");
// → DB: first_name="Smith", last_name="John" 😱
3. 会造成安全漏洞
// ❌ 検証なしのStringはSQLインジェクションの温床
fn find_user(email: String) -> User {
    db.query(&format!("SELECT * FROM users WHERE email = '{}'", email))
}

find_user("'; DROP TABLE users; --".to_string());  // 💀
4. 混淆不同单位
// ❌ 両方ともf64。単位の違いをコンパイラが検出できない
fn calculate_distance(meters: f64, feet: f64) -> f64;

// 火星探査機が墜落した原因(実話:Mars Climate Orbiter, 1999年)
let result = calculate_distance(altitude_in_feet, thrust_in_meters);
为什么测试无法发现这些问题
问题测试的局限性
负金额正常路径测试只使用正值
参数顺序两者都是字符串,因此不会触发类型错误
边界值不可能测试所有组合
单位混淆两者都是数值,因此计算会“正确”运行

如果通过类型加以约束,这些问题都能在编译时发现。

检测反模式

发现以下模式时,请考虑转换为领域基元:

❌ fn send_email(to: String, subject: String)  // StringはEmailではない
❌ fn create_user(age: i32)                    // i32は年齢の制約を持たない
❌ fn process_order(amount: f64, currency: String)  // 別々に渡すと不整合の可能性
❌ struct User { email: String }               // 検証なしで無効な値を保持できる
❌ if !is_valid_email(s) { return Err(...) }   // 検証後も同じString型
❌ fn schedule(room: String, start: String, end: String)  // 引数の取り違えが検出できない
参数混淆的风险

当多个相同原始类型的参数并列时,编译器无法检测 Bug:

// ❌ 引数を取り違えてもコンパイルが通る
fn schedule(room: &str, start_time: &str, end_time: &str);
schedule("10:00", "11:00", "Room A");  // バグだがコンパイルOK

// ✅ 型で区別すればコンパイルエラーで検出
fn schedule(room: MeetingRoom, start: Time, end: Time);
schedule(Time::parse("10:00")?, Time::parse("11:00")?, MeetingRoom::new("Room A")?);
// コンパイルエラー: expected `MeetingRoom`, found `Time`

设计模式

1. 智能构造器(Smart Constructor)
// ❌ 外部から直接構築可能
pub struct Email(pub String);

// ✅ Smart Constructorで検証を強制
mod email {
    pub struct Email(String);  // フィールドはprivate

    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()))
        }

        // カプセル化を破る場合は命名で明示
        pub fn breach_encapsulation_of_value(&self) -> &str {
            &self.0
        }
    }
}
2. 复合值封装
// ❌ 関連する値を別々に渡す
fn calculate_price(amount: f64, currency: &str) -> f64;

// ✅ 複合値を単一の型でカプセル化
pub struct Money {
    amount: Decimal,
    currency: Currency,
}

impl Money {
    pub fn new(amount: Decimal, currency: Currency) -> Result<Self, MoneyError> {
        if amount < Decimal::ZERO {
            return Err(MoneyError::NegativeAmount);
        }
        Ok(Self { amount, currency })
    }

    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)
    }
}
3. 范围约束类型
// ❌ 任意のi32を受け入れる
fn set_age(age: i32);

// ✅ 有効な範囲のみを表現する型
pub struct Age(u8);

impl Age {
    pub fn new(value: u8) -> Result<Self, AgeError> {
        if value > 150 {
            return Err(AgeError::TooOld);
        }
        Ok(Self(value))
    }
}
4. 非空类型(NonEmpty)
// ❌ 空の可能性があるVec
fn process_items(items: Vec<Item>);

// ✅ 空でないことを型で保証
pub struct NonEmpty<T> {
    head: T,
    tail: Vec<T>,
}

impl<T> NonEmpty<T> {
    pub fn new(items: Vec<T>) -> Option<Self> {
        let mut iter = items.into_iter();
        iter.next().map(|head| NonEmpty {
            head,
            tail: iter.collect(),
        })
    }

    pub fn head(&self) -> &T {
        &self.head  // 常に安全にアクセス可能
    }
}

判断流程

プリミティブ型を使おうとしている
    ↓
この値にドメイン固有の制約があるか?
    ├─ Yes → Domain Primitiveを作成
    │    ├─ フォーマット制約 → Smart Constructor + 正規表現/パーサー
    │    ├─ 範囲制約 → 境界チェック付きコンストラクタ
    │    ├─ 複合値 → 関連する値をまとめた型
    │    └─ 非空制約 → NonEmpty<T>
    └─ No → プリミティブ型のままでOK(稀)

各语言的实现模式

Rust
// newtype + From/TryFrom
pub struct UserId(Uuid);

impl TryFrom<&str> for UserId {
    type Error = UserIdError;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        Uuid::parse_str(value)
            .map(UserId)
            .map_err(|_| UserIdError::InvalidFormat)
    }
}
TypeScript
// Branded Types
declare const __brand: unique symbol;
type Brand<T, B> = T & { [__brand]: B };

type Email = Brand<string, 'Email'>;

function parseEmail(s: string): Email | null {
  if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s)) {
    return s as Email;
  }
  return null;
}
Java
public final class Email {
    private final String value;

    private Email(String value) {
        this.value = value;
    }

    public static Email parse(String s) {
        if (s == null || !s.contains("@") || s.length() <= 3) {
            throw new IllegalArgumentException("Invalid email: " + s);
        }
        return new Email(s);
    }

    public String breachEncapsulationOfValue() {
        return value;
    }
}

审查要点

代码审查时的检查要点:

  1. 暴露原始类型:String、int、f64等是否在表达领域概念
  2. 构造时验证:是否在构造函数/工厂中验证不变条件
  3. 不可变性:字段是否为private且为final/readonly/不可变
  4. 封装:是否限制对内部状态的访问(breachEncapsulationOf命名)
  5. 领域操作:相关操作是否集中在类型中

应用指南

建议使用
  • ID 类型(UserId、OrderId、ProductId 等)
  • 联系信息(Email、PhoneNumber、Address 等)
  • 金融信息(Money、Currency、Percentage 等)
  • 测量值(Temperature、Distance、Weight 等)
  • 非空集合(NonEmpty)
避免过度应用
  • 临时局部变量
  • 私有的内部实现细节
  • 外部库的边界(但可在转换层中应用类型)
允许转换为原始类型的场景

跨越领域边界时,转换为原始类型既必要也合理。

ドメイン層(ドメイン固有型を使用)
    │
    │  ← ここで変換(breachEncapsulationOf)
    ↓
境界層(JSON, DB, 外部API)← プリミティブ型が必要
场景原因示例
JSON/XML 序列化标准格式只支持原始类型{"quantity": 5}
数据库持久化关系型数据库的列类型是原始类型INSERT INTO orders (quantity) VALUES (5)
外部 API 集成外部系统不了解领域类型REST API 请求/响应
日志输出需要人类可读的格式log::info!("注文数: {}", qty.value())
// ✅ 永続化層での正当な使用例
impl OrderRepository {
    fn save(&self, order: &Order) {
        // ドメイン型 → プリミティブ型への変換は境界層で許容
        let quantity_value = order.quantity().breach_encapsulation_of_value();
        db.execute("INSERT INTO orders (quantity) VALUES (?)", quantity_value);
    }

    fn find(&self, id: OrderId) -> Option<Order> {
        let row = db.query_one("SELECT quantity FROM orders WHERE id = ?", id.value())?;
        // プリミティブ型 → ドメイン型への変換(検証付き)
        let quantity = OrderQuantity::new(row.get("quantity")).ok()?;
        Some(Order::new(id, quantity))
    }
}

原则:在领域层内始终使用领域专属类型,只在跨越边界时进行转换。

与相关技能的使用区分

技能关注点使用时机
本技能类型设计与构造时验证设计新的领域类型时
parse-dont-validate将验证结果转换为类型审查 validate→parse 转换时
domain-building-blocksDDD 战术模式全貌设计实体/聚合/服务时

参考文献

  • Dan Bergh Johnsson 等,《Secure by Design》——领域基元的原始出处
  • Alexis King,《Parse, don't validate》——始终有效原则的理论背景
  • Scott Wlaschin,《Domain Modeling Made Functional》——类型驱动设计的实践
  • Einar Landre,《97 Things Every Programmer Should Know》中的“Prefer Domain-Specific Types to Primitive Types”——让编译器帮忙找出 Bug

相关技能(建议一并阅读)

使用本技能时,请同时参阅以下技能:

  • parse-dont-validate:在类型层面保证不变式的设计理念
  • when-to-wrap-primitives:判断是否应该封装原始类型的标准
  • domain-building-blocks:由领域基元构成的值对象设计

评论

支持 Markdown