mcpField
D
domain-driven-design

domain-driven-design

github.com/bfollington开发5170 次安装1 周前更新
代码仓库

关于 domain-driven-design

面向开发者的领域建模技能,结合数据导向设计与类型驱动设计,让无效状态难以表示,并使代码贴合业务语言。

主要功能

  1. 01数据导向建模
  2. 02类型驱动的领域设计
  3. 03显式工作流与错误路径
  4. 04DDD 构建块与边界
  5. 05领域图示与术语表指导

适用场景

  1. 01用不可变数据建模业务领域
  2. 02通过类型表达有效状态与转换
  3. 03分离领域逻辑和基础设施
  4. 04记录领域术语与关系

安装

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

$npx skills add https://github.com/bfollington/terma --skill domain-driven-design

安装后在 Claude Code 中输入/domain-driven-design确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
ddd_foundations_and_patterns.md
rich_hickey_principles.md
visualization_examples.md
wlaschin_patterns.md

详细介绍

领域驱动设计

概述

此技能基于 Rich Hickey 的数据导向设计原则和 Scott Wlaschin 的类型驱动设计方法,为领域建模提供指导。重点是构建让非法状态无法表示的系统,优先考虑数据和转换,而非对象和方法,并建立连接技术实现与业务领域的通用语言。

核心原则

Rich Hickey 的数据导向设计

简单胜于易用

  • 优先使用可以独立理解的简单构造
  • 避免复杂交织(相互纠缠)无关的关注点
  • 将策略与机制、数据与行为分开

数据至上

  • 使用纯数据结构而非包含行为的对象来建模领域
  • 适当时优先使用通用数据结构(映射、集合、向量),而非自定义类
  • 数据应能自我描述并可供检查
  • 函数转换数据;数据不执行行为

值的价值

  • 使用不可变值表示事实
  • 值支持局部推理和简单相等性判断
  • 值可以自由共享,无需协调
  • 思考:这个领域中有哪些不可变事实?

解耦

  • 识别领域中真正重要的内容与偶然产生的复杂性
  • 将何时发生与发生什么分开
  • 将机制与策略分开
  • 思考:这些关注点实际上是彼此独立的吗,还是我们把它们纠缠在了一起?
Scott Wlaschin 的类型驱动设计

让非法状态无法表示

  • 使用类型系统在编译时消除无效状态
  • 显式建模可选值(Option/Maybe 类型)
  • 对互斥状态使用和类型(判别联合)
  • 避免原始类型偏执;创建领域专用类型

以函数式方式进行领域建模

  • 将工作流建模为数据转换:输入 → 处理 → 输出
  • 将业务规则显式建模为函数
  • 将验证与业务逻辑分开
  • 从以下角度思考:可能发生什么?有效的状态转换有哪些?

铁路导向编程

  • 显式建模成功和失败路径(Result 类型)
  • 使用 bind/flatMap 串联可能失败的操作
  • 让正常路径清晰、线性
  • 在适当的边界处理错误

类型即文档

  • 类型签名应传达意图
  • 使用 newtype 包装器来明确语义(UserId、EmailAddress、Timestamp)
  • 使用类型将输入限制在有效范围内
  • 让类型系统指导 API 设计

DDD 构建块

实体与值对象

实体由其标识而非属性定义:

  • 拥有唯一标识符(ID、帐号等)
  • 可以随时间变化,同时保持身份不变
  • 属性相同但 ID 不同的两个实体彼此不同
  • 在领域专家按名称/ID 指称事物时使用

值对象完全由其属性定义:

  • 没有唯一标识符
  • 不可变
  • 属性相同的两个值对象可以互换
  • 仅在值重要而身份不重要时使用

决策指南:

  • 思考:领域专家会按 ID/名称指称它吗?→ 实体
  • 思考:我能用等价副本替换它吗?→ 如果可以:值对象
聚合与聚合根

聚合:一组实体和值对象,在数据变更时作为一个整体处理。

聚合根:外部访问聚合时必须经过的唯一实体。

目的:

  • 定义事务一致性边界
  • 强制执行跨多个对象的业务不变式
  • 将相关概念组合起来,简化模型

规则:

  • 外部引用只能指向聚合根(使用 ID 引用)
  • 根负责强制执行整个聚合的所有不变式
  • 事务不能跨越聚合边界(使用最终一致性)
  • 保持聚合规模小,以提升性能和可扩展性

以下情况不应创建聚合:

  • 对象可以独立修改
  • 没有需要事务一致性保障的共享不变式
  • 不同对象具有不同的生命周期
限界上下文

定义:领域模型适用的明确边界。

目的:

  • 将大型领域划分为易于管理的部分
  • 允许同一术语在不同上下文中具有不同含义
  • 防止混合不兼容的概念导致模型受损

关键认知:通用语言只在一个上下文中通用。在销售上下文中,“客户”可能与在配送上下文中的“客户”含义不同。

建模时:

  • 确定当前所在的限界上下文
  • 在代码结构中明确上下文边界(使用独立模块/命名空间)
  • 跨上下文集成时使用防腐层
  • 记录上下文之间的关系(上下文映射)
领域事件

定义:领域中发生的重要事情。

特征:

  • 使用过去时命名(OrderPlaced、PaymentProcessed、UserRegistered)
  • 不可变的事实
  • 领域专家关注这些事件
  • 可在上下文内部或跨上下文触发响应

用途:

  • 解耦领域逻辑
  • 实现聚合之间的最终一致性
  • 集成限界上下文
  • 事件溯源(将事件存储为事实来源)
仓储

目的:提供聚合的内存集合这一假象,并抽象持久化过程。

特征:

  • 在聚合边界上运作(加载/保存整个聚合)
  • 支持按 ID 查找
  • 隐藏数据库实现细节
  • 返回领域实体,而不是数据库行

模式:应用层使用仓储获取/保存聚合;领域层保持纯粹。

领域建模工作流

1. 发现通用语言

首先使用领域专家的术语识别领域概念:

行动项:

  • 列出领域中的名词(实体、值对象)和动词(操作、事件)
  • 为领域术语编写精确定义
  • 找出同义词并消除歧义
  • 询问:业务人员如何称呼它?这个概念的边界是什么?

输出格式: 创建一个词汇表章节,记录每个术语:

**Term** (Type: Entity/ValueObject/Event/Command)
- Definition: [Clear, domain-expert-approved definition]
- Examples: [Concrete examples]
- Invariants: [Rules that must always hold]
2. 分析现有领域模型

进行更改之前,先了解当前状态:

探索步骤:

  • 确定当前在何处对领域概念进行建模(类型、架构、表)
  • 梳理领域实体之间的关系
  • 找出业务逻辑所在的位置(服务、函数、存储过程)
  • 记录隐式规则和约束
  • 注意命名或建模方面的不一致

需要回答的问题:

  • 哪些类型/类表示领域概念?
  • 不变式有哪些?在哪里强制执行?
  • 哪些概念纠缠在一起,需要拆分?
  • 是否存在不应存在的虚设类型或状态?
3. 识别不一致和异味

需要发现的常见问题:

命名不一致

  • 同一概念使用不同名称(User、Account、Customer)
  • 不同概念使用相同名称(作为实体的 Order 与作为命令的 Order)
  • 技术名称渗入领域语言(DTO、DAO 后缀)

结构问题

  • 可以表示非法状态(例如,status: "approved" | "rejected",同时有独立的 approved_at 和 rejected_at 字段,且两者都可以设置)
  • 基本类型痴迷(用字符串表示电子邮件,用数字表示金额)
  • 在某些状态下实际必需、却标记为可选的字段
  • 用 null/undefined 表示多个不同状态

纠缠的关注点

  • 领域逻辑与基础设施混杂(在业务逻辑中访问数据库)
  • 一个类型/模块承担多项职责
  • 时间耦合(必须先调用 A 再调用 B,否则系统会出错)

缺失的概念

  • 对话中存在、代码中却没有的领域概念
  • 应该显式表示的隐式状态
  • 通过注释或约定而非类型来强制执行的业务规则
4. 设计领域模型

应用类型驱动和数据驱动原则:

数据建模:

  • 从数据形状入手:事实是什么?
  • 对不会变化的事实使用不可变值
  • 显式建模状态转换
  • 将标识与属性分开
  • 思考:哪些内容会一起变化?哪些会独立变化?

类型设计:

  • 为互斥状态创建和类型:
    type PaymentStatus =
      | Pending
      | Approved { approvedAt: Timestamp, approvedBy: UserId }
      | Rejected { rejectedAt: Timestamp, reason: string }
    
  • 使用积类型确保所有必需数据都存在
  • 为基本类型创建语义包装:
    type EmailAddress = EmailAddress of string  // with validation
    type Money = { amount: Decimal, currency: Currency }
    
  • 让不可能的状态无法表示

工作流建模:

  • 将每个业务工作流建模为清晰的流水线:
    ValidateInput → ExecuteBusinessLogic → HandleResult → Persist → Notify
    
  • 找出决策点并显式建模
  • 将纯业务逻辑与副作用(IO、时间、随机性)分开
  • 使用清晰的函数签名来说明意图
5. 构建并维护通用语言

一致性规则:

  • 在代码、文档、对话和 UI 中使用完全相同的术语
  • 领域语言变更时,更新所有表述
  • 避免在领域代码中使用技术术语(除非属于领域术语,否则不要使用“factory”“manager”“handler”)
  • 不要为了技术上的方便而改名领域概念

代码约定:

  • 领域类型应与领域语言完全一致
  • 函数名应使用领域动词
  • 模块边界应遵循领域边界
  • 注释应解释领域规则,而非实现细节

文档:

  • 及时更新术语表
  • 记录决策的原因(尤其是约束和不变量)
  • 将代码链接到领域文档
  • 明确说明隐式领域规则
6. 可视化领域模型

使用图表传达领域结构和关系:

使用 Mermaid 表示关系:

classDiagram
    Order --> Customer
    Order --> OrderLine
    OrderLine --> Product
    Order --> PaymentStatus

    class Order {
        +OrderId id
        +CustomerId customerId
        +List~OrderLine~ lines
        +PaymentStatus status
    }

    class PaymentStatus {
        <<enumeration>>
        Pending
        Approved
        Rejected
    }

使用 Mermaid 表示工作流:

graph LR
    A[Receive Order] --> B{Valid?}
    B -->|Yes| C[Calculate Total]
    B -->|No| D[Return Validation Error]
    C --> E[Process Payment]
    E --> F{Payment Success?}
    F -->|Yes| G[Fulfill Order]
    F -->|No| H[Cancel Order]

使用 Mermaid 表示状态转换:

stateDiagram-v2
    [*] --> Draft
    Draft --> Submitted: submit()
    Submitted --> Approved: approve()
    Submitted --> Rejected: reject()
    Approved --> Fulfilled: fulfill()
    Fulfilled --> [*]
    Rejected --> [*]

使用 Graphviz/DOT 表示复杂关系:

digraph domain {
    rankdir=LR;
    node [shape=box];

    Customer -> Order [label="places"];
    Order -> OrderLine [label="contains"];
    OrderLine -> Product [label="references"];
    Order -> Payment [label="requires"];
    Payment -> PaymentMethod [label="uses"];
}

使用 ASCII 绘制快速草图:

Customer
  └─> Order (1:N)
       ├─> OrderLine (1:N)
       │    └─> Product
       └─> Payment (1:1)
            └─> PaymentMethod

何时使用:

  • Mermaid classDiagram:实体关系和类型结构
  • Mermaid graph/flowchart:业务工作流和决策树
  • Mermaid stateDiagram:状态转换和生命周期
  • Graphviz/DOT:复杂依赖图、模块边界
  • ASCII:讨论期间的快速草图、简单层级结构

领域建模反模式

贫血领域模型

  • 症状:数据结构只有 getter/setter,所有逻辑都放在单独的服务中
  • 问题:增加了形式上的繁琐,却没有封装带来的好处,违背了以数据为中心的原则
  • 解决方案:让数据保持为数据;将相关转换放在同一模块中,但与数据定义分开

实体服务反模式

  • 症状:出现 UserService、OrderManager、ProductFactory 这类类名
  • 问题:掩盖了实际操作;缺乏通用语言
  • 解决方案:根据领域操作命名函数:approveOrder、cancelSubscription、calculateDiscount

原始类型偏执

  • 症状:用字符串表示邮箱、用数字表示金额、用布尔标志表示状态
  • 问题:没有类型安全;可以表示无效值
  • 解决方案:创建带有验证的语义类型

意外复杂性

  • 症状:使用复杂抽象和设计模式,却没有明确的领域收益
  • 问题:增加了掩盖领域含义的层次
  • 解决方案:简化设计;优先使用组合而非继承;避免过早抽象

隐藏的时间耦合

  • 症状:必须按特定顺序调用方法,否则系统会出错
  • 问题:将工作流与状态管理复杂地耦合在一起
  • 解决方案:明确工作流;使用类型强制执行有效的状态转换

布尔盲视

  • 症状:使用布尔标志表示状态(isApproved、isActive、isDeleted)
  • 问题:多个布尔值可能表示不可能的状态
  • 解决方案:对互斥状态使用和类型

结合现有模型进行考量

向现有领域模型中添加内容或对其进行更改时:

  1. 梳理当前状态:记录现有类型、关系和模式
  2. 识别受影响的概念:此更改会涉及哪些现有概念?
  3. 检查一致性:新设计是否遵循现有模式?如果不遵循,原因是什么?
  4. 评估影响:进行此更改会导致哪些内容出错?
  5. 迁移路径:如何从当前状态演进到目标状态?
  6. 更新通用语言:确保更新所有使用位置
  7. 可视化前后变化:创建展示当前模型和拟议模型的图表

关键问题:

  • 此更改是否符合现有领域边界?
  • 我们是否使用一致的术语?
  • 这会引入新概念,还是复用现有概念?
  • 我们是在修正不一致,还是引入新的不一致?
  • 能否逐步完成此更改?

领域建模检查清单

完成领域建模工作前:

语言与沟通:

  • 所有领域概念都使用通用语言命名
  • 领域术语表已更新,包含新增或变更的术语
  • 所有代码、文档和交流都使用统一的术语
  • 已明确识别并记录限界上下文

类型设计:

  • 类型使非法状态无法表示
  • 不存在基本类型偏执;恰当使用语义类型
  • 实体具有清晰的标识;值对象不可变
  • 对互斥状态使用和类型

领域逻辑:

  • 业务规则明确且可测试
  • 数据与行为得到恰当分离
  • 工作流建模为清晰的数据转换
  • 领域逻辑是纯的(无副作用)
  • 消除时间耦合,或将其明确表示

聚合与边界:

  • 聚合边界明确
  • 聚合能够维护自身的不变量
  • 对聚合的外部引用仅使用 ID
  • 聚合保持小巧且专注
  • 事务边界设置恰当

一致性与集成:

  • 已解决与现有模型之间的不一致,或将其记录在案
  • 已定义跨聚合一致性策略(事务性或最终一致性)
  • 使用领域事件记录重要事件
  • 限界上下文之间通过防腐层集成

文档:

  • 可视化图表清晰地传达设计
  • 已记录关键决策和不变量
  • 上下文映射展示了限界上下文之间的关系

资源

references/

此技能包含供深入学习的参考文档:

  • ddd_foundations_and_patterns.md:Eric Evans 的 DDD 基础概念(实体、值对象、聚合、限界上下文、仓储、领域事件)、Martin Fowler 关于通用语言的指导,以及实用的 Clojure/函数式模式。理解 DDD 构建模块及其应用方式的必读资料。

  • rich_hickey_principles.md:Rich Hickey 演讲中的核心概念,包括 Simple Made Easy、Value of Values 和 The Language of the System。重点介绍面向数据的设计、简洁性、解缠复杂性,以及不可变值的强大之处。

  • wlaschin_patterns.md:Scott Wlaschin 的类型驱动设计模式、领域建模方法、函数式架构指导和铁路导向编程。重点是让非法状态无法表示,并使用类型进行设计。

  • visualization_examples.md:Mermaid、Graphviz 和 ASCII 领域建模图表模式的综合示例。包含实体关系、工作流、状态机、聚合边界和限界上下文映射。

需要深入了解特定原则或模式时,请加载这些参考资料。

评论

支持 Markdown