值对象虽然常常被掩盖在实体的阴影之下,但它倒是很是重要的 DDD 概念。java
值对象不具备身份,它纯粹用于描述实体的特性。处理不具备身份的值对象是很容易的,尤为是不变性与可组合性是支持易用性的两个特征。数据库
值对象用于度量和描述事物,咱们能够很是容易的对值对象进行建立、测试、使用、优化和维护。json
一个值对象,或者更简单的说,值,是对一个不变的概念总体创建的模型。在这个模型中,值就真的只有一个值。和实体不同,他没有惟一标识,而是经过封装属性的对比来决定相等性。一个值对象不是事物,而是用来描述、量化或测量实体的。bash
当你关系某个对象的属性时,该对象即是一个值对象。为其添加有意义的属性,并赋予相应的行为。咱们须要将值对象当作不变对象,不要给他任何身份标识,还应该尽可能避免像实体对象同样的复杂性。app
即便一个领域概念必须建模成实体,在设计时也应该更偏向于将其做为值对象的容器。框架
当决定一个领域概念是否应该建模成值对象时,须要考虑是否拥有一些特性:dom
在使用这个特性分析模型时,你会发现不少领域概念都应该建模成值对象,而非实体。ide
值对象的特征汇总以下:函数
值对象是实体的状态,它描述与实体相关的概念。测试
当一个概念缺少明显的身份时,基本能够判定它大几率是一个值对象。
比较典型的例子即是 Money,大多数状况下,咱们只关心它所表明的实际金额,为其分配标识是一个没有意义的操做。
@Data
@Setter(AccessLevel.PRIVATE)
@Embeddable
public class Money implements ValueObject {
public static final String DEFAULT_FEE_TYPE = "CNY";
@Column(name = "total_fee")
private Long totalFee;
@Column(name = "fee_type")
private String feeType;
...
}
复制代码
领域驱动设计的一切都是为了明确传递业务规则和领域逻辑。像整数和字符串这样的技术单元并不适合这种状况。
好比邮箱可使用字符串进行描述,但会丢失不少邮箱的特性,此时,须要将其建模成值对象。
@Embeddable
@Data
@Setter(AccessLevel.PRIVATE)
public class Email implements ValueObject {
@Column(name = "email_name")
private String name;
@Column(name = "email_domain")
private String domain;
private Email() {
}
private Email(String name, String domain) {
Preconditions.checkArgument(StringUtils.isNotEmpty(name), "name can not be null");
Preconditions.checkArgument(StringUtils.isNotEmpty(domain), "domain can not be null");
this.setName(name);
this.setDomain(domain);
}
public static Email apply(String email) {
Preconditions.checkArgument(StringUtils.isNotEmpty(email), "email can not be null");
String[] ss = email.split("@");
Preconditions.checkArgument(ss.length == 2, "not Email");
return new Email(ss[0], ss[1]);
}
@Override
public String toString() {
return this.getName() + "@" + this.getDomain();
}
}
复制代码
此时,邮箱是一个明确的领域概念,相比字符串方案,其拥有验证逻辑,同时享受编译器类型校验。
值对象是不可变的、无反作用而且易于测试的。
缺失身份是值对象和实体最大的区别。
因为值对象没有身份,且描述了领域中重要的概念,一般,咱们会先定义实体,而后找出与实体相关的值对象。通常状况下,值对象须要实体提供上下文相关性。
若是实体具备相同的类型和标识,则会认为是相等的。相反,值对象要具备相同的值才会认为是相等的。
若是两个 Money 对象表示相等的金额,他们就被认为是相等的。而无论他们是指向同一个实例仍是不一样的实例。
在 Money 类中使用 lombok 插件自动生成 hashCode 和 equals 方法,查看 Money.class 能够看到。
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
public class Mobile implements ValueObject {
public boolean equals(final Object o) {
if (o == this) {
return true;
} else if (!(o instanceof Mobile)) {
return false;
} else {
Mobile other = (Mobile)o;
if (!other.canEqual(this)) {
return false;
} else {
Object this$dcc = this.getDcc();
Object other$dcc = other.getDcc();
if (this$dcc == null) {
if (other$dcc != null) {
return false;
}
} else if (!this$dcc.equals(other$dcc)) {
return false;
}
Object this$mobile = this.getMobile();
Object other$mobile = other.getMobile();
if (this$mobile == null) {
if (other$mobile != null) {
return false;
}
} else if (!this$mobile.equals(other$mobile)) {
return false;
}
return true;
}
}
}
protected boolean canEqual(final Object other) {
return other instanceof Mobile;
}
public int hashCode() {
int PRIME = true;
int result = 1;
Object $dcc = this.getDcc();
int result = result * 59 + ($dcc == null ? 43 : $dcc.hashCode());
Object $mobile = this.getMobile();
result = result * 59 + ($mobile == null ? 43 : $mobile.hashCode());
return result;
}
public String toString() {
return "Mobile(dcc=" + this.getDcc() + ", mobile=" + this.getMobile() + ")";
}
}
复制代码
值对象应该尽量多的暴露面向领域概念的行为。
在 Money 值对象中,能够看到暴露的方法:
方法 | 含义 |
---|---|
apply | 建立 Money |
add | Money 相加 |
subtract | Money 相减 |
multiply | Money 相乘 |
split | Money 切分,将没法查分的偏差汇总到最后的 Money 中 |
@Data
@Setter(AccessLevel.PRIVATE)
@Embeddable
public class Money implements ValueObject {
public static final String DEFAULT_FEE_TYPE = "CNY";
@Column(name = "total_fee")
private Long totalFee;
@Column(name = "fee_type")
private String feeType;
private static final BigDecimal NUM_100 = new BigDecimal(100);
private Money() {
}
private Money(Long totalFee, String feeType) {
Preconditions.checkArgument(totalFee != null);
Preconditions.checkArgument(StringUtils.isNotEmpty(feeType));
Preconditions.checkArgument(totalFee.longValue() > 0);
this.totalFee = totalFee;
this.feeType = feeType;
}
public static Money apply(Long totalFee){
return apply(totalFee, DEFAULT_FEE_TYPE);
}
public static Money apply(Long totalFee, String feeType){
return new Money(totalFee, feeType);
}
public Money add(Money money){
checkInput(money);
return Money.apply(this.getTotalFee() + money.getTotalFee(), getFeeType());
}
private void checkInput(Money money) {
if (money == null){
throw new IllegalArgumentException("input money can not be null");
}
if (!this.getFeeType().equals(money.getFeeType())){
throw new IllegalArgumentException("must be same fee type");
}
}
public Money subtract(Money money){
checkInput(money);
if (getTotalFee() < money.getTotalFee()){
throw new IllegalArgumentException("money can not be minus");
}
return Money.apply(this.getTotalFee() - money.getTotalFee(), this.getFeeType());
}
public Money multiply(int var){
return Money.apply(this.getTotalFee() * var, getFeeType());
}
public List<Money> split(int count){
if (getTotalFee() < count){
throw new IllegalArgumentException("total fee can not lt count");
}
List<Money> result = Lists.newArrayList();
Long pre = getTotalFee() / count;
for (int i=0; i< count; i++){
if (i == count-1){
Long fee = getTotalFee() - (pre * (count - 1));
result.add(Money.apply(fee, getFeeType()));
}else {
result.add(Money.apply(pre, getFeeType()));
}
}
return result;
}
}
复制代码
一般状况下,值对象会内聚封装度量值和度量单位。在 Money 中能够看到这一点。
固然,并不局限于此,对于拥有概念总体性的对象,都具备很强的内聚性。好比,英文名称,由 firstName,lastName 组成。
@Data
@Setter(AccessLevel.PRIVATE)
public class EnglishName{
private String firstName;
private String lastName;
private EnglishName(String firstName, String lastName){
Preconditions.checkArgument(StringUtils.isNotEmpty(firstName));
Preconditions.checkArgument(StringUtils.isNotEmpty(lastName));
setFirstName(firstName);
setLastName(lastName);
}
public static EnglishName apply(String firstName, String lastName){
return new EnglishName(firstName, lastName);
}
}
复制代码
一旦建立完成后,值对象就永远不能改变。
若是须要改变值对象,应该建立新的值对象,并由新的值对象替换旧值对象。 好比,Money 的 subtract 方法。
public Money subtract(Money money){
checkInput(money);
if (getTotalFee() < money.getTotalFee()){
throw new IllegalArgumentException("money can not be minus");
}
return Money.apply(this.getTotalFee() - money.getTotalFee(), this.getFeeType());
}
复制代码
只会建立新的 Money 对象,不会对原有对象进行修改。
在技术实现上,对于一个不可变对象,须要将全部字段设置为 final,并经过构造函数为其赋值。但,有时为了迎合一些框架需求,需求进行部分妥协,及将 setter 方法设置为 private,从而对外隐藏修改方法。
对于用于度量的值对象,一般会有数值,此时,能够将其组合起来以建立新的值。
好比 Money 的 add 方法,Money 加上 Money 会获得一个新的 Money。
public Money add(Money money){
checkInput(money);
return Money.apply(this.getTotalFee() + money.getTotalFee(), getFeeType());
}
复制代码
值对象做为一个概念总体,决不该该变成无效状态,它自身就应该负责对其进行验证。
一般状况下,在建立一个值对象实例时,若是参数与业务规则不一致,则构造函数应该抛出异常。
仍是看咱们的 Money 类,须要进行以下检验:
private Money(Long totalFee, String feeType) {
Preconditions.checkArgument(totalFee != null);
Preconditions.checkArgument(StringUtils.isNotEmpty(feeType));
Preconditions.checkArgument(totalFee.longValue() > 0);
this.totalFee = totalFee;
this.feeType = feeType;
}
复制代码
固然,若是值对象的构建过程过于复杂,可使用 Factory 模式进行构建。此时,应该在 Factory 中对值对象的有效性进行验证。
不变性、内聚性和可组合性使值对象变的可测试。
仍是看咱们的 Money 对象的测试类。
public class MoneyTest {
@Test
public void add() {
Money m1 = Money.apply(100L);
Money m2 = Money.apply(200L);
Money money = m1.add(m2);
Assert.assertEquals(300L, money.getTotalFee().longValue());
Assert.assertEquals(m1.getFeeType(), money.getFeeType());
Assert.assertEquals(m2.getFeeType(), money.getFeeType());
}
@Test
public void subtract() {
Money m1 = Money.apply(300L);
Money m2 = Money.apply(200L);
Money money = m1.subtract(m2);
Assert.assertEquals(100L, money.getTotalFee().longValue());
Assert.assertEquals(m1.getFeeType(), money.getFeeType());
Assert.assertEquals(m2.getFeeType(), money.getFeeType());
}
@Test
public void multiply() {
Money m1 = Money.apply(100L);
Money money = m1.multiply(3);
Assert.assertEquals(300L, money.getTotalFee().longValue());
Assert.assertEquals(m1.getFeeType(), money.getFeeType());
}
@Test
public void split() {
Money m1 = Money.apply(100L);
List<Money> monies = m1.split(33);
Assert.assertEquals(33, monies.size());
monies.forEach(m -> Assert.assertEquals(m1.getFeeType(), m.getFeeType()));
long total = monies.stream()
.mapToLong(m->m.getTotalFee())
.sum();
Assert.assertEquals(100L, total);
}
}
复制代码
经过一些经常使用的值对象建模模式,能够提升值对象的处理体验。
静态工厂方法是更简单、更具备表达性的一种技巧。
好比 java 中的 Instant 的静态工厂方法。
public static Instant now() {
...
}
public static Instant ofEpochSecond(long epochSecond) {
...
}
public static Instant ofEpochMilli(long epochMilli){
...
}
复制代码
经过方法签名就能很清楚的了解其含义。
经过使用更具体的领域模型类型封装技术类型,使其更具表达能力。
典型的就是 Mobile 封装,其本质是一个 String。经过 Mobile 封装,使其具备字符串没法表达的含义。
@Setter(AccessLevel.PRIVATE)
@Data
@Embeddable
public class Mobile implements ValueObject {
public static final String DEFAULT_DCC = "0086";
@Column(name = "dcc")
private String dcc;
@Column(name = "mobile")
private String mobile;
private Mobile() {
}
private Mobile(String dcc, String mobile){
Preconditions.checkArgument(StringUtils.isNotEmpty(dcc));
Preconditions.checkArgument(StringUtils.isNotEmpty(mobile));
setDcc(dcc);
setMobile(mobile);
}
public static Mobile apply(String mobile){
return apply(DEFAULT_DCC, mobile);
}
public static Mobile apply(String dcc, String mobile){
return new Mobile(dcc, mobile);
}
}
复制代码
一般状况下,须要尽可能避免使用值对象集合。这种表达方式没法正确的表达领域概念。
使用值对象集合一般意味着须要使用某种形式来取出特定项,这就至关于为值对象添加了身份。 好比 List 第一个表明是主邮箱,第二个表示是副邮箱,最佳的表达方式是直接用属性进行表式,如:
@Data
@Setter(AccessLevel.PRIVATE)
public class Person{
private Email primary;
private Email second;
public void updateEmail(Email primary, Email second){
Preconditions.checkArgument(primary != null);
Preconditions.checkArgument(second != null);
setPrimary(primary);
setSecond(second);
}
}
复制代码
处理值对象最难的点就在他们的持久化。通常状况下,不会直接对其进行持久化,值对象会做为实体的属性,一并进行持久化处理。
持久化过程即将对象序列化成文本格式或二进制格式,而后保存到计算机磁盘中。
在面向文档数据存储时,问题会少不少。咱们能够在同一个文档中存储实体和值对象;然而,使用 SQL 数据库就麻烦的多,这将致使不少变化。
许多 NoSQL 数据库都使用了数据反规范化,为咱们提供了很大便利。
在 NoSQL 中,整个实体均可以做为一个文档来建模。在 SQL 中的表链接、规范化数据和 ORM 延迟加载等相关问题都不存在了。在值对象上下文中,这就意味着他们会与实体一块儿存储。
@Data
@Setter(AccessLevel.PRIVATE)
@Document
public class PersonAsMongo {
private Email primary;
private Email second;
public void updateEmail(Email primary, Email second){
Preconditions.checkArgument(primary != null);
Preconditions.checkArgument(second != null);
setPrimary(primary);
setSecond(second);
}
}
复制代码
面向文档的 NoSQL 数据库会将文档持久化为 JSON,上例中 Person 的 primary 和 second 会做为 JSON 文档的属性进行存储。
在 SQL 数据库中存储值对象,能够遵循标准的 SQL 约定,也可使用范模式。
多数状况下,持久化值对象时,咱们都是经过一种非范式的方式完成,即全部的属性和实体都保存在相同的数据库表中。有时,值对象须要以实体的身份进行持久化。好比聚合中维护一个值对象集合时。
基本思路就是将值对象与其所在的实体对象保存在同一张表中,值对象的每一个属性保存为一列。
这种方式,是最多见的值对象序列化方式,也是冲突最小的方式,能够在查询中使用链接语句进行查询。
Jpa 提供 @Embeddable 和 @Embedded 两个注解,以支持这种方式。
首先,在值对象上添加 @Embeddable 注解,以标注其为可嵌入对象。
@Embeddable
@Data
@Setter(AccessLevel.PRIVATE)
public class Email implements ValueObject {
@Column(name = "email_name")
private String name;
@Column(name = "email_domain")
private String domain;
private Email() {
}
private Email(String name, String domain) {
Preconditions.checkArgument(StringUtils.isNotEmpty(name), "name can not be null");
Preconditions.checkArgument(StringUtils.isNotEmpty(domain), "domain can not be null");
this.setName(name);
this.setDomain(domain);
}
public static Email apply(String email) {
Preconditions.checkArgument(StringUtils.isNotEmpty(email), "email can not be null");
String[] ss = email.split("@");
Preconditions.checkArgument(ss.length == 2, "not Email");
return new Email(ss[0], ss[1]);
}
@Override
public String toString() {
return this.getName() + "@" + this.getDomain();
}
}
复制代码
而后,在实体对于属性上添加 @Embedded 注解,标注该属性将展开存储。
@Data
@Entity
public class Person1 {
@Embedded
private Email primary;
}
复制代码
值对象的全部属性保存为一列。当不但愿在查询中使用额外语句来链接他们时,这是一个很好的选择。
通常状况下,会涉及如下几个操做:
如,对于 Email 值对象,咱们采用 JSON 做为持久化格式:
public class EmailSerializer {
public static Email toEmail(String json){
if (StringUtils.isEmpty(json)){
return null;
}
return JSON.parseObject(json, Email.class);
}
public static String toJson(Email email){
if (email == null){
return null;
}
return JSON.toJSONString(email);
}
}
复制代码
JPA 中提供了 Converter 扩展,以完成值对象到数据、数据到值对象的转化:
public class EmailConverter implements AttributeConverter<Email, String> {
@Override
public String convertToDatabaseColumn(Email attribute) {
return EmailSerializer.toJson(attribute);
}
@Override
public Email convertToEntityAttribute(String dbData) {
return EmailSerializer.toEmail(dbData);
}
}
复制代码
Converter 完成后,须要将其配置在对应的属性上:
@Data
@Setter(AccessLevel.PRIVATE)
public class PersonAsJpa {
@Convert(converter = EmailConverter.class)
private Email primary;
@Convert(converter = EmailConverter.class)
private Email second;
public void updateEmail(Email primary, Email second){
Preconditions.checkArgument(primary != null);
Preconditions.checkArgument(second != null);
setPrimary(primary);
setSecond(second);
}
}
复制代码
此时,就完成了单个值对象的持久化。
这种应用是前种方案的扩展。将整个集合序列化成某种形式的文本,而后将该文本保存到单个数据库列中。
须要考虑的问题:
如,对于 List 选择 JSON 做为持久化格式:
public class EmailListSerializer {
public static List<Email> toEmailList(String json){
if (StringUtils.isEmpty(json)){
return null;
}
return JSON.parseArray(json, Email.class);
}
public static String toJson(List<Email> email){
if (email == null){
return null;
}
return JSON.toJSONString(email);
}
}
复制代码
扩展 JPA 的 Converter:
public class EmailListConverter implements AttributeConverter<List<Email>, String> {
@Override
public String convertToDatabaseColumn(List<Email> attribute) {
return EmailListSerializer.toJson(attribute);
}
@Override
public List<Email> convertToEntityAttribute(String dbData) {
return EmailListSerializer.toEmailList(dbData);
}
}
复制代码
属性配置:
@Data
@Setter(AccessLevel.PRIVATE)
public class PersonEmailListAsJpa {
@Convert(converter = EmailListConverter.class)
private List<Email> emails;
}
复制代码
咱们应该首先考虑将领域概念建模成值对象,而不是实体。
咱们可使用委派主键的方式,使用两层的层超类型。在上层隐藏委派主键。 这样咱们能够自由的将其映射成数据库实体,同时在领域模型中将其建模成值对象。
首先,定义 IdentitiedObject 用以隐藏数据库 ID。
@MappedSuperclass
public class IdentitiedObject {
@Setter(AccessLevel.PRIVATE)
@Getter(AccessLevel.PRIVATE)
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
}
复制代码
而后,从 IdentitiedObject 派生出 IdentitiedEmail 类,用以完成值对象建模。
@Data
@Setter(AccessLevel.PRIVATE)
@Entity
public class IdentitiedEmail extends IdentitiedObject
implements ValueObject {
@Column(name = "email_name")
private String name;
@Column(name = "email_domain")
private String domain;
private IdentitiedEmail() {
}
private IdentitiedEmail(String name, String domain) {
Preconditions.checkArgument(StringUtils.isNotEmpty(name), "name can not be null");
Preconditions.checkArgument(StringUtils.isNotEmpty(domain), "domain can not be null");
this.setName(name);
this.setDomain(domain);
}
public static IdentitiedEmail apply(String email) {
Preconditions.checkArgument(StringUtils.isNotEmpty(email), "email can not be null");
String[] ss = email.split("@");
Preconditions.checkArgument(ss.length == 2, "not Email");
return new IdentitiedEmail(ss[0], ss[1]);
}
@Override
public String toString() {
return this.getName() + "@" + this.getDomain();
}
}
复制代码
此时,就可使用 JPA 的 @OneToMany 特性存储多个值:
@Data
@Entity
public class PersonOneToMany {
@OneToMany
private List<IdentitiedEmail> emails = Lists.newArrayList();
}
复制代码
大多持久化框架都提供了对枚举类型的支持。要么使用枚举值得 String,要么使用枚举值得 Index,其实都不是最佳方案,对之后得重构不太友好,建议使用自定义 code 进行持久化处理。
定义枚举:
public enum PersonStatus implements CodeBasedEnum<PersonStatus> {
ENABLE(1),
DISABLE(0);
private final int code;
PersonStatus(int code) {
this.code = code;
}
@Override
public int getCode() {
return this.code;
}
public static PersonStatus parseByCode(Integer code){
for (PersonStatus status : values()){
if (code.intValue() == status.getCode()){
return status;
}
}
return null;
}
}
复制代码
扩展枚举 Converter:
public class PersonStatusConverter implements AttributeConverter<PersonStatus, Integer> {
@Override
public Integer convertToDatabaseColumn(PersonStatus attribute) {
return attribute != null ? attribute.getCode() : null;
}
@Override
public PersonStatus convertToEntityAttribute(Integer dbData) {
return dbData == null ? null : PersonStatus.parseByCode(dbData);
}
}
复制代码
配置属性:
@Data
@Setter(AccessLevel.PRIVATE)
public class Person{
@Embedded
private Email primary;
@Embedded
private Email second;
@Convert(converter = PersonStatusConverter.class)
private PersonStatus status;
public void updateEmail(Email primary, Email second){
Preconditions.checkArgument(primary != null);
Preconditions.checkArgument(second != null);
setPrimary(primary);
setSecond(second);
}
}
复制代码
此时,经过枚举对象中的 code 进行持久化。
在使用 DB 进行值对象持久化时,常常遇到阻抗。
当面临阻抗时,咱们应该从领域模型角度,而不是持久化角度去思考问题。
标准类型是用于表示事物类型的描述性对象。
Java 的枚举时实现标准类型的一种简单方法。枚举提供了一组有限数量的值对象,它是很是轻量的,而且无反作用。
一个共享的不变值对象,能够从持久化存储中获取,此时可使用标准类型的领域服务和工厂来获取值对象。咱们应该为每组标准类型建立一个领域服务或工厂。 若是打算使用常规值对象来表示标准类型,可使用领域服务或工厂来静态的建立值对象实例。
当模型概念从上游上下文流入下游上下文中,尽可能使用值对象来表示这些概念。在有可能的状况下,使用值对象完成上下文之间的集成。