手把手教你落地 DDD
钟敬
Thoughtworks 首席咨询师、数字化转型与运营团队 DDD 负责人
19697 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 45 讲
AIGC特别策划 (2讲)
结束语&结课测试 (2讲)
手把手教你落地 DDD
15
15
1.0x
00:00/00:00
登录|注册

18|值对象(上):到底什么是值对象?

你好,我是钟敬。
前面几节课我们学习了聚合,这节课我们继续学习 DDD 中另一个有用的概念——值对象。
DDD 把领域对象分成了两种:一种是实体,另一种是值对象。前面我们讨论的组织、员工等等都是实体。而值对象则往往是用来描述实体的属性“值”的。值对象在一些方面和实体有明显的区别,但在 DDD 提出以前,人们建模的时候,一般都只重视实体,对值对象的研究不够。DDD 强调实体和值对象的区别,可以让领域建模更加准确和深入。
但是,值对象的概念有些不太好理解,不过没关系,你可以暂时忘掉这个词本身,咱们用例子来一步一步地说明。

例一:员工状态

第一个例子是员工状态。在第 16 课,我们实现了关于员工状态(EmpStatus)的两个业务规则:
还记得吗?在那节课末尾,我们问了一个问题:在目前的程序里,改变员工状态的业务规则是在员工对象中实现的,你觉得放在哪里会更合适?
可能你已经想到了,应该放在员工状态(EmpStatus)本身。其实员工状态就是个值对象,至于为什么,我们后面再说。这里我们先看看实现逻辑。
之前的员工状态转换代码是后面这样。
package chapter18.unjuanable.domain.orgmng.emp;
// imports ...
public class Emp extends AggregateRoot {
// 其他属性 ...
protected EmpStatus status;
//其他方法 ...
public Emp becomeRegular() {
onlyProbationCanBecomeRegular();
status = REGULAR;
return this;
}
public Emp terminate() {
shouldNotTerminateAgain();
status = TERMINATED;
return this;
}
private void onlyProbationCanBecomeRegular() {
if (status != PROBATION) {
throw new BusinessException("试用期员工才能转正!");
}
}
private void shouldNotTerminateAgain() {
if (status == TERMINATED) {
throw new BusinessException("已经终止的员工不能再次终止!");
}
}
}
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

领域驱动设计(DDD)中的重要概念——值对象,是本文的重点。作者通过详细讲解值对象的应用,包括员工状态和时间段的建模,并对值对象进行了分类,帮助读者更好地理解值对象的概念。通过具体的代码示例和领域模型图,本文为领域驱动设计提供了实用的指导。同时,文章提出了思考题,引发读者对于值对象的深入思考和探讨。值对象的重要性和应用在领域驱动设计中的价值得到了充分展现。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《手把手教你落地 DDD》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(11)

  • 最新
  • 精选
  • escray
    置顶
    Java 代码写的少,居然不知道 enum 里面也可以有方法。按照类似的思路,是不是类似于状态机的代码都可以放在枚举类里面? 员工状态这样的对象(enum)是没有”生命“的。 时间段类也是第一次见到,原谅我书读得少,其实这段代码解决之前困扰我的一个问题,也是和历史版本相关的。 这里对于代码的重构是通过”设计“来完成的。 实体是靠独立于其他属性的标识 identity 来确定同一性 identity 的。 有单独标识,理论上可以改变的对象,叫做实体 Entity,是一个”东西“; 没有单独标识,并且不可以改变的对象,叫做值对象 Value Object,是一个”值“。 对于思考题, 1. 日期中的年、月、日三个属性,如果单独来看,拆散了”日期“这个对象的原始意义,只有当三个值都存在的时候,”日期“才有意义,时间也类似。 2. 货币如果要写成代码话,需要把币种种类也加进来,然后可能还需要增加一个”转换汇率“的属性值?同种货币的数值与数值相加,然后保留币种。

    作者回复: 笔记记得很好,继续努力! 状态机代码可以放在Enum。 汇率转换,可能放在单独的“转换率”对象里比较好。关于货币的其他思路没问题。

    2023-01-30归属地:北京
    1
  • aoe
    思考题 1. 在日期的定义为“以年月日确定某一天”的前提下,它在概念上不能再拆分,符合“原子值对象”的定义。例如:生日、节假日、工资到账时间这些都能定位到具体的日期,年月日缺一不可。 2. 代码如下 public enum Currency { GOLD_COIN("金币"), GEEK_COIN("极客币"), ; private String name; Currency(String name) { this.name = name; } } import java.math.BigDecimal; public class Money { private final BigDecimal value; private final Currency currency; public Money(BigDecimal value, Currency currency) { this.value = value; this.currency = currency; } public Money add(Money other) { if (other == null) { throw new IllegalArgumentException("货币不能为空"); } if (this.currency != other.currency) { throw new IllegalArgumentException("货币类型不一致,请转换成相同货币类型后进行计算"); } return new Money(this.value.add(other.value), this.currency); } public BigDecimal value() { return value; } } import org.junit.jupiter.api.Test; import java.math.BigDecimal; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; class MoneyTest { @Test void returns_3_when_1_plus_2(){ Money one = new Money(new BigDecimal("1"), Currency.GOLD_COIN); Money two = new Money(new BigDecimal("2"), Currency.GOLD_COIN); BigDecimal result = one.add(two).value(); assertThat(result).isEqualTo(new BigDecimal("3")); } } 读后感 原来 Java 中的 String 是值对象! 快照也是值对象 简单理解:不可变的对象就是值对象 开始日期、结束日期封装在了“时间段对象”以后模型也跟着变了,确实是保持了“模型与代码一致”

    作者回复: 第1题,正如你说的,日期本质上是一个不可分割的时点。年、月、日只是一种表达方式,是表象;比如说,理论上也可以用从公元1年1月1日到现在的总日期数来表达日期。 第2题,代码写得不错,可贵的是还有单元测试。可以在Money对象里直接有一个equals()方法,这样就不用通过value()来判断相等了。

    2023-01-17归属地:浙江
    2
    7
  • Ice
    开始时间和结束时间修改成值对象之后,与之对应的数据库结构是否需要调整呢? 还是说在持久化层再做一次映射转换

    作者回复: 数据库一般不调整,在仓库转换

    2023-02-06归属地:四川
    5
  • 一剑
    老师,我有个问题:EmpStatus的方法是Public的,所以Emp的EmpStatus属性是为了控制状态变更不被外部直接调用所以设成了protected么?但是在实际项目里,状态应该是要用Public对外公开的吧?但是一旦公开,就可能会被人绕过聚合根而直接调用emp.status.becomeRegular()了,这个怎么解决?

    作者回复: EmpStatus是值对象,因此,他的becomeRegular()方法只是返回一个新的值对象,而不会改变任何现有的东西。所以emp.status.becomeRegular()并不会改变emp的任何状态。

    2024-01-10归属地:江西
  • Geek_967502
    钟老师您好,我有一个疑惑,领域模型中没有每个领域实体的具体字段,是如何识别的值对象,会不会识别的不够准确。按照理解应该是每个阶段的输出结果都能对下一个阶段起到支撑,但值对象这一章,感觉没办法通过之前的成果完整的分析值对象

    作者回复: 领域模型是逐步演进的,遇到的时候识别就可以了。

    2023-05-31归属地:北京
  • benhuang
    文中提到JAVA date对象是可变的带了很多问题,具体是什么问题

    作者回复: 比如说线程不安全

    2023-03-17归属地:广西
  • 6点无痛早起学习的和尚
    思考题和一些问题: 问题:1. 所以本篇文章举例的员工状态、时间段 员工状态:原子值对象+依附于实体的值对象 时间段:复合值对象+独立的值对象 所以多种多样的值对象分类之间并不是完全独立不相交 思考题: public class Money { private Long value; private Currency currency; private Money(Long value, Currency currency) { this.value = value; this.currency = currency; } public static Money of(long value, Currency currency) { return new Money(value, currency); } public static Money addTwoMoney(Money money1, Money money2) { // 这里引入货币计算规则,把 2 个货币全部转成人民币,然后进行计算,再 new Money(value,人民币)返回 return new Money(0L, Currency.CNY); } @Getter @AllArgsConstructor public enum Currency { CNY("CNY"); //省略其他货币... private String name; } }

    作者回复: addTwoMoney 可以改成非静态的: public Money add(Money other) { ....... //校验是同一种货币 return new Money(this.value + other.value, this.currency); }

    2023-02-15归属地:北京
  • Geek_ab5b86
    老师,类似员工实体中的身份证号idNum,我认为也是原子值对象,是不是说明也是不能修改的,实体内部不能加set方法,只能通过构造器传入值对象属性重新构造员工实体呢?

    作者回复: 是的

    2023-01-16归属地:上海
  • Geek_1e04e7
    分别表达年月日的值对象也是有的,职责不一样

    作者回复: 日期是时间轴上一个点,本来无所谓再分属性。年月日只是一种表示方法,还可以有其他表示法,不影响日期不可分的本质。

    2023-01-16归属地:广东
  • 阿昕
    1.从含义上来看,年月日组合在一起是日期的一种固有格式,是一个整体,所以是原子值对象; 2.货币相加,需要先校验币种是否统一,是否需要转换;

    作者回复: 没错

    2023-01-15归属地:浙江
收起评论
显示
设置
留言
11
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部