npx skills add https://github.com/jwilger/agent-skills --skill domain-modeling价值: 沟通 —— 领域类型让代码用业务语言说话。它们将隐性知识转化为显性的、编译器验证的契约,人类和 AI 都能据此进行推理。
教授如何构建丰富的领域模型,在编译时而非运行时防止错误。涵盖原始类型痴迷检测、解析而非验证、使无效状态无法表示以及语义类型设计。对于任何代码审查或设计任务都独立有用,并为 TDD 循环中的领域审查检查提供原则。
不要为领域概念使用原始类型(String、int、number)。创建能表达业务含义的类型。
应该这样做:
fn transfer(from: AccountId, to: AccountId, amount: Money) -> Result<Receipt, TransferError>
不应该这样做:
fn transfer(from: String, to: String, amount: i64) -> Result<(), String>
审查代码时,标记出每个参数、字段或返回类型中原始类型表示领域概念的情况。修复方法是使用在构造时进行验证的新类型或值对象。
布尔值作为状态的反模式: 一个名称描述领域状态(already_exists、、、)的 字段,是一个编码为原始类型的状态机。今天的两个状态明天可能变成三个,而布尔值无法表示第三个状态。
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
is_initializedis_publishedhas_been_reviewedbool// 错误:bool 将两状态机编码为原始类型
struct Article { is_published: bool }
// 正确:枚举命名了状态并能安全扩展
enum ArticleState { Draft, Published, Archived }
标记任何回答“这是什么状态?”而非“这个条件是否为真?”的布尔字段。修复方法是使用一个枚举,其变体命名了领域状态。此检查与“使无效状态无法表示”不同 —— 该规则捕获不可能的组合;此规则捕获隐藏在布尔值内的领域概念。
在边界处验证。内部使用强类型。绝不重新验证类型已保证有效的数据。
# 边界:将原始输入解析为领域类型
email = Email(raw_input) # 如果无效则抛出异常
# 内部:信任该类型
def send_welcome(email: Email) -> None:
# 无需验证 —— Email 保证了有效性
如果你在业务逻辑深处发现验证逻辑,它应该属于构造边界。
使用类型系统使非法组合无法构造。
问题 —— 布尔标志创建无效组合:
struct User { email: Option<String>, email_verified: bool }
# 可能出现 email_verified=true 而 email=None 的情况
解决方案 —— 在类型中编码状态:
enum User {
Unverified { email: Email },
Verified { email: Email, verified_at: Timestamp },
}
审查代码时,问:“这个类型能否表示一个在领域中无意义的状态?” 如果是,重新设计它。
根据类型在领域中的“身份”命名,而非根据其构成。
| 错误(结构型) | 正确(语义型) |
|---|---|
NonEmptyString | UserName |
PositiveInteger | OrderQuantity |
ValidatedEmail | CustomerEmail |
测试方法是:如果两个字段具有相同的结构类型,编译器无法捕获你交换它们的情况。语义类型可以防止这种情况。
// 错误:title 和 name 都是 NonEmptyString —— 可互换
{ title: NonEmptyString, name: NonEmptyString }
// 正确:不同的类型在编译时捕获混淆
{ title: UserTitle, name: UserName }
结构类型作为构建块很有用,语义类型将其包装起来。语义类型增加了领域身份;结构类型提供了可重用的验证。
每个标识符都有自己的类型。绝不要为 ID 使用原始 String 或 int。
struct AccountId(Uuid);
struct UserId(Uuid);
// 编译器捕获:transfer(user_id, account_id) 将无法编译
fn transfer(from: AccountId, to: AccountId, user: UserId) -> Result<(), Error>
使构造经过验证且提取容易。
Display、AsRef、Into 或等效功能,使类型易于使用。取出内部值应该是微不足道的。绝不要提供从原始类型的自动转换 —— 这会绕过验证并破坏“解析而非验证”原则。
使用枚举配合穷尽的 match/switch 以确保处理所有情况。绝不要为领域状态使用兜底的默认情况 —— 它会默默地吞掉新的变体。
审查代码时(无论是在 TDD 循环中还是独立审查),你有权拒绝违反这些原则的设计。行使此权力时:
email 是 String,应该是 Email 类型”)。不要为了避免冲突而放弃有效的领域关切。不要默默地接受违反这些原则的设计。
硬性约束:
[RP] —— 如果人类明确覆盖,记录覆盖行为及其违反的原则。应用领域建模原则后,验证:
String、int、number)如果任何标准未满足,在继续之前创建或完善领域类型。
此技能可独立工作。对于增强的工作流,它与以下集成:
缺少依赖项?使用以下命令安装:
npx skills add jwilger/agent-skills --skill tdd
每周安装次数
87
仓库
GitHub 星标数
2
首次出现
2026年2月12日
安全审计
安装于
claude-code76
codex74
cursor74
opencode72
github-copilot72
amp72
Value: Communication -- domain types make code speak the language of the business. They turn implicit knowledge into explicit, compiler-verified contracts that humans and AI can reason about.
Teaches how to build rich domain models that prevent bugs at compile time rather than catching them at runtime. Covers primitive obsession detection, parse-don't-validate, making invalid states unrepresentable, and semantic type design. Independently useful for any code review or design task, and provides the principles that domain review checks for in the TDD cycle.
Do not use raw primitives (String, int, number) for domain concepts. Create types that express business meaning.
Do:
fn transfer(from: AccountId, to: AccountId, amount: Money) -> Result<Receipt, TransferError>
Do not:
fn transfer(from: String, to: String, amount: i64) -> Result<(), String>
When reviewing code, flag every parameter, field, or return type where a primitive represents a domain concept. The fix is a newtype or value object that validates on construction.
Bool-as-state anti-pattern: A bool field whose name describes a domain state (already_exists, is_initialized, is_published, has_been_reviewed) is a state machine encoded as a primitive. Two states today become three tomorrow, and the bool cannot represent the third.
// BAD: bool encodes a two-state machine as a primitive
struct Article { is_published: bool }
// GOOD: enum names the states and extends safely
enum ArticleState { Draft, Published, Archived }
Flag any bool field that answers "what state is this in?" rather than "is this condition true?" The fix is an enum whose variants name the domain states. This check is distinct from "make invalid states unrepresentable" -- that rule catches impossible combinations; this one catches domain concepts hiding inside a boolean.
Validate at the boundary. Use strong types internally. Never re-validate data that a type already guarantees.
# Boundary: parse raw input into domain type
email = Email(raw_input) # raises if invalid
# Interior: trust the type
def send_welcome(email: Email) -> None:
# No need to validate -- Email guarantees validity
If you find validation logic deep inside business logic, it belongs at the construction boundary instead.
Use the type system to make illegal combinations impossible to construct.
Problem -- boolean flags create invalid combinations:
struct User { email: Option<String>, email_verified: bool }
# Can have email_verified=true with email=None
Solution -- encode state in the type:
enum User {
Unverified { email: Email },
Verified { email: Email, verified_at: Timestamp },
}
When reviewing code, ask: "Can this type represent a state that is meaningless in the domain?" If yes, redesign it.
Name types for what they ARE in the domain, not what they are made of.
| Wrong (structural) | Right (semantic) |
|---|---|
NonEmptyString | UserName |
PositiveInteger | OrderQuantity |
ValidatedEmail | CustomerEmail |
The test: if two fields have the same structural type, the compiler cannot catch you swapping them. Semantic types prevent this.
// BAD: title and name are both NonEmptyString -- swappable
{ title: NonEmptyString, name: NonEmptyString }
// GOOD: distinct types catch mix-ups at compile time
{ title: UserTitle, name: UserName }
Structural types are useful as building blocks that semantic types wrap. The semantic type adds domain identity; the structural type provides reusable validation.
Every identifier gets its own type. Never use raw String or int for IDs.
struct AccountId(Uuid);
struct UserId(Uuid);
// Compiler catches: transfer(user_id, account_id) won't compile
fn transfer(from: AccountId, to: AccountId, user: UserId) -> Result<(), Error>
Make construction validated and extraction easy.
Display, AsRef, Into or equivalent so the type is convenient to use. Getting the inner value out should be trivial.Never provide an automatic conversion FROM a primitive -- that bypasses validation and undermines parse-don't-validate.
Use enums with exhaustive match/switch to ensure all cases are handled. Never use a catch-all default for domain states -- it silently swallows new variants.
When reviewing code (whether in a TDD cycle or a standalone review), you have authority to reject designs that violate these principles. When exercising this authority:
email is String, should be Email type").Do not back down from valid domain concerns to avoid conflict. Do not silently accept designs that violate these principles.
Hard constraints:
[RP] -- if human explicitly overrides, record the override and the principle it violates.After applying domain modeling principles, verify:
String, int, number) used for domain conceptsIf any criterion is not met, create or refine the domain type before proceeding.
This skill works standalone. For enhanced workflows, it integrates with:
Missing a dependency? Install with:
npx skills add jwilger/agent-skills --skill tdd
Weekly Installs
87
Repository
GitHub Stars
2
First Seen
Feb 12, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
claude-code76
codex74
cursor74
opencode72
github-copilot72
amp72
冲刺回顾模板:敏捷团队回顾会议指南与模板(开始-停止-继续/愤怒-悲伤-高兴/4Ls)
10,400 周安装