mcpField
D
ddd-value-object

ddd-value-object

github.com/yonatankarp开发144 次安装23 周前更新
代码仓库

关于 ddd-value-object

用于建模领域驱动设计值对象的技能:这类概念由属性而非身份定义,且不可变。它指导验证、领域操作,以及何时适合封装基本类型。

主要功能

  1. 01建模不可变且按值相等的概念
  2. 02在构造时验证取值
  3. 03将领域操作放在值对象中
  4. 04区分底层类型相同的概念

适用场景

  1. 01建模 Money、Address 或 DateRange
  2. 02用领域不变量替代基本类型
  3. 03区分底层类型相同的标识符

安装

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

$npx skills add https://github.com/yonatankarp/software-design-skills --skill ddd-value-object

安装后在 Claude Code 中输入/ddd-value-object确认已加载。

详细介绍

一句话概述

值对象是完全由其属性定义的概念——没有身份、不可变,并按值相等。

何时使用此技能

  • 为 Money、Address、DateRange、EmailAddress、ProductCode、Percentage、PhoneNumber 建模。
  • 替换承载了类型系统无法强制执行的不变量的基本类型(String、Long、BigDecimal)。
  • 区分共享同一种底层基本类型的两个概念(CustomerId 与 OrderId——二者都是 UUID,但绝不能互换)。
  • 封装属于某个概念的领域操作:Money.add(Money)、Address.normalize()、DateRange.contains(Instant)。

何时不使用此技能

  • 该概念具有生命周期或身份 → 使用 ddd-entity。
  • 封装既不增加不变量,也不增加行为——为了封装而封装只是徒增形式。
  • 某个基本类型只在一处使用,且从不跨越边界——封装的成本可能超过收益。

核心内容

值对象是不可变的:构造完成后,其字段永不改变。对它们执行操作会返回新的值对象,而不是修改接收者。它们按值相等:具有相同属性值的两个值对象可以互换使用。

值对象也是领域操作的天然归属地。Money 应有一个 add 方法,并拒绝将 USD 与 EUR 相加。Address 可以有一个返回规范形式的 normalize 方法。DateRange 应能判断某个 Instant 是否在其范围内。将这些操作放在值对象上,而不是放在 MoneyUtils 这类静态辅助工具中,可以让领域拥有更丰富的表达方式。

应在构造时进行验证。EmailAddress("notanemail") 应在构造时失败,而不是等到之后有人尝试使用它时才失败。这就是“让无效状态无法表示”的含义。

决策启发

  • 当基本类型 (a) 具有语言无法表达的不变量,(b) 具有特定于领域的操作,或 (c) 被两个不应互换的领域概念共用时,应将其封装起来。
  • 即使在允许使用可变对象的语言中,也优先选择不可变值对象。
  • 如果你正准备编写 MoneyUtils.add(money1, money2),请停下来——这个操作应属于 Money。

反模式

  • 可变值对象——setter 会破坏相等语义,并引发共享状态错误。
  • 带有身份字段的值对象——这其实是伪装成值对象的实体。
  • 认为每个基本类型都需要封装——这是过度设计。只有在基本类型承载领域含义时才封装,不要条件反射般地一概封装。
  • 验证失败时抛出没有消息的通用 IllegalArgumentException——应定义清晰的领域异常,或使用结果类型。

另请参阅

  • ddd-entity — 相反的情况;具有身份的概念。
  • ddd-anti-patterns — 基本类型痴迷、可变值对象。
  • ddd-kotlin-idioms — 如何在 Kotlin 中惯用地表达值对象(data class、value class)。

参考资料

  • Evans,《领域驱动设计》(2003),第 5 章(以软件表达模型)。

评论

支持 Markdown