mcpField
D
domain-modeling

domain-modeling

github.com/jwilger开发5153 次安装17 周前更新
代码仓库

关于 domain-modeling

domain-modeling 是面向开发者和 Agent 的软件设计技能,重点是用类型表达业务概念。它强调编译期正确性、明确的领域状态,以及在系统边界进行校验。

主要功能

  1. 01用语义类型替代领域原始类型
  2. 02将输入解析为已校验的领域类型
  3. 03让无效状态无法构造
  4. 04为标识符使用独立新类型
  5. 05穷尽匹配所有领域状态
  6. 06依据领域原则审查设计

适用场景

  1. 01为业务概念设计类型
  2. 02检查代码中的原始类型滥用
  3. 03将校验移至输入边界
  4. 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 }

应标出所有回答“当前处于什么状态?”而非“这个条件是否成立?”的布尔字段。解决方法是使用枚举,并以变体名称表示领域状态。这项检查不同于“让无效状态不可表示”——那条规则用于捕获不可能的组合;本规则则用于发现隐藏在布尔值中的领域概念。

先解析,而非验证

在边界处验证。内部使用强类型。对于类型已保证的数据,绝不重复验证。

  1. 在系统边界(用户输入、API 响应)接收原始输入。
  2. 将其解析为在构造时确保有效性的领域类型。
  3. 在系统中传递领域类型。无需进一步验证。
# 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 },
}

审查代码时,思考:“这个类型能否表示领域中没有意义的状态?”如果可以,就重新设计。

优先使用语义类型,而非结构类型

类型名称应体现它们在领域中代表什么,而非它们由什么构成。

错误(结构类型)正确(语义类型)
NonEmptyStringUserName
PositiveIntegerOrderQuantity
ValidatedEmailCustomerEmail

判断方法:如果两个字段具有相同的结构类型,编译器就无法发现你把它们互换了。语义类型可以避免这个问题。

// 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 循环中,还是单独进行审查),你有权否决违反这些原则的设计。行使此权力时:

  1. 指明具体违反之处(例如:“基本类型泛滥:email 是 String,应该使用 Email 类型”)。
  2. 提出替代方案,并给出具体的类型定义。
  3. 用一句话说明影响。
  4. 如果对方不同意,就实质性地讨论,最多进行两轮。之后升级给人类处理。

不要为了避免冲突而放弃合理的领域问题。不要默默接受违反这些原则的设计。

执行说明

  • 独立模式:仅提供建议。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

评论

支持 Markdown