关于 domain-modeling
domain-modeling 是面向开发者和 Agent 的软件设计技能,重点是用类型表达业务概念。它强调编译期正确性、明确的领域状态,以及在系统边界进行校验。
主要功能
- 01用语义类型替代领域原始类型
- 02将输入解析为已校验的领域类型
- 03让无效状态无法构造
- 04为标识符使用独立新类型
- 05穷尽匹配所有领域状态
- 06依据领域原则审查设计
适用场景
- 01为业务概念设计类型
- 02检查代码中的原始类型滥用
- 03将校验移至输入边界
- 04避免相互矛盾的领域状态
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ jwilger/ agent-skills --skill domain-modeling安装后在 Claude Code 中输入/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、is_initialized、is_published、has_been_reviewed)的 bool 字段,是用原始类型编码的状态机。今天只有两种状态,明天可能就变成三种,而布尔值无法表示第三种状态。
// 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 }
应标出所有回答“当前处于什么状态?”而非“这个条件是否成立?”的布尔字段。解决方法是使用枚举,并以变体名称表示领域状态。这项检查不同于“让无效状态不可表示”——那条规则用于捕获不可能的组合;本规则则用于发现隐藏在布尔值中的领域概念。
先解析,而非验证
在边界处验证。内部使用强类型。对于类型已保证的数据,绝不重复验证。
- 在系统边界(用户输入、API 响应)接收原始输入。
- 将其解析为在构造时确保有效性的领域类型。
- 在系统中传递领域类型。无需进一步验证。
# 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
如果你在业务逻辑深处发现验证逻辑,它应该放在构造边界处。
让无效状态不可表示
使用类型系统,让非法组合无法构造。
问题——布尔标志会产生无效组合:
struct User { email: Option<String>, email_verified: bool }
# Can have email_verified=true with email=None
解决方案——在类型中编码状态:
enum User {
Unverified { email: Email },
Verified { email: Email, verified_at: Timestamp },
}
审查代码时,思考:“这个类型能否表示领域中没有意义的状态?”如果可以,就重新设计。
优先使用语义类型,而非结构类型
类型名称应体现它们在领域中代表什么,而非它们由什么构成。
| 错误(结构类型) | 正确(语义类型) |
|---|---|
NonEmptyString | UserName |
PositiveInteger | OrderQuantity |
ValidatedEmail | CustomerEmail |
判断方法:如果两个字段具有相同的结构类型,编译器就无法发现你把它们互换了。语义类型可以避免这个问题。
// 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 }
结构类型适合作为语义类型的构建块。语义类型赋予领域身份;结构类型提供可复用的验证。
标识符使用新类型
每个标识符都应有自己的类型。绝不要用原始 String 或 int 表示 ID。
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>
便捷的类型转换
让构造经过验证,并使提取值变得简单。
- 构造(输入): 始终通过验证构造函数完成。不要提供从原始类型自动转换的功能。
- 提取(输出): 提供
Display、AsRef、Into或等效方式,让类型便于使用。获取内部值应当轻而易举。
绝不要提供从原始类型自动转换的功能——这会绕过验证,破坏先解析而非验证的原则。
穷尽匹配
使用枚举和穷尽式 match/switch,确保处理所有情况。对于领域状态,绝不要使用兜底默认分支——它会悄无声息地忽略新增变体。
否决权
在审查代码时(无论是在 TDD 循环中,还是单独进行审查),你有权否决违反这些原则的设计。行使此权力时:
- 指明具体违反之处(例如:“基本类型泛滥:
email是String,应该使用Email类型”)。 - 提出替代方案,并给出具体的类型定义。
- 用一句话说明影响。
- 如果对方不同意,就实质性地讨论,最多进行两轮。之后升级给人类处理。
不要为了避免冲突而放弃合理的领域问题。不要默默接受违反这些原则的设计。
执行说明
- 独立模式:仅提供建议。Agent 按惯例遵循领域建模原则。
- TDD 领域审查(链式):仅提供建议。否决权由自身执行:如果 Agent 的工作违反这些原则,Agent 必须否决自己的工作,严格程度应与由另一个 Agent 审查时相同。
- TDD 领域审查(子 Agent):具有门控作用。领域否决会阻止阶段推进。审查 Agent 可以拒绝交接。
硬性约束:
- 不要默默接受违反这些原则的设计:
[RP]—— 如果人类明确推翻这些原则,请记录该推翻决定及其违反的原则。
约束
- 否决权——“不要退让”:不要为了避免升级带来的摩擦而把合理的问题说成没问题。这并不意味着要拒绝回应反驳。如果反驳确实有说服力——即该原则不适用于此处——就更新你的判断。如果你退让的原因是升级处理不方便,那就是违反原则。
- 布尔值表示状态 vs. 条件:判断标准是:此布尔值是否表示领域关注的状态转换(例如,启用/停用、打开/关闭)?如果是,请使用枚举。如果它确实只回答一个没有领域状态含义的是/否问题(例如,“这个计算结果是否为正数?”),则可以使用布尔值。如有疑问,请使用枚举——小型枚举的成本低于布尔值悄然积累领域含义的成本。
验证
应用领域建模原则后,请验证:
- 未使用基本类型(
String、int、number)表示领域概念 - 没有布尔字段用于编码领域状态(状态机应使用枚举)
- 所有标识符都使用新类型包装,而非原始基本类型
- 无效状态无法表示(不存在相互矛盾的字段组合)
- 验证发生在构造边界,而非深入业务逻辑后才进行
- 类型名称体现领域含义(语义),而非结构
- 两个底层类型相同的字段不会被意外互换
- 枚举匹配是穷尽的,没有针对领域状态的兜底默认分支
如果任何一项不符合要求,请先创建或改进领域类型,再继续。
依赖项
此技能可独立使用。要增强工作流,可与以下内容集成:
- tdd: 领域审查是 TDD 循环中的强制检查点。此技能提供审查所依据的原则。
- code-review: 领域完整性是三阶段审查的第 3 阶段。此技能定义了审查重点。
- architecture-decisions: 架构模式(事件溯源、CQRS、六边形架构)会影响领域边界的划分位置。
缺少依赖项?使用以下命令安装:
npx skills add jwilger/agent-skills --skill tdd