关于 domain-primitives-and-always-valid
domain-primitives-and-always-valid 是一项开发技能,指导用经过验证的不可变类型表达领域概念,替代不受约束的基本类型。它提供设计模式和审查要点,帮助通过类型保证不变量。
主要功能
- 01经过验证的领域专用类型
- 02不可变值设计
- 03智能构造器与范围约束
- 04封装复合值
- 05按语言提供实现模式
- 06领域不变量审查要点
适用场景
- 01为 ID 和联系信息设计类型
- 02在构造时阻止无效值
- 03区分含义不同的参数
- 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;
}
}
审查要点
代码审查时的检查要点:
- 暴露原始类型:
String、int、f64等是否在表达领域概念 - 构造时验证:是否在构造函数/工厂中验证不变条件
- 不可变性:字段是否为
private且为final/readonly/不可变 - 封装:是否限制对内部状态的访问(
breachEncapsulationOf命名) - 领域操作:相关操作是否集中在类型中
应用指南
建议使用
- 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-blocks | DDD 战术模式全貌 | 设计实体/聚合/服务时 |
参考文献
- 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