值对象虽然经常被掩盖在实体的阴影之下,但它却是非常重要的 DDD 概念。

值对象不具有身份,它纯粹用于描述实体的特性。处理不具有身份的值对象是很容易的,尤其是不变性与可组合性是支持易用性的两个特征。

1 理解值对象

值对象用于度量和描述事物,我们可以非常容易的对值对象进行创建、测试、使用、优化和维护。

一个值对象,或者更简单的说,值,是对一个不变的概念整体建立的模型。在这个模型中,值就真的只有一个值。和实体不一样,他没有唯一标识,而是通过封装属性的对比来决定相等性。一个值对象不是事物,而是用来描述、量化或测量实体的。

当你关系某个对象的属性时,该对象便是一个值对象。为其添加有意义的属性,并赋予相应的行为。我们需要将值对象看成不变对象,不要给他任何身份标识,还应该尽量避免像实体对象一样的复杂性。

即使一个领域概念必须建模成实体,在设计时也应该更偏向于将其作为值对象的容器。

当决定一个领域概念是否应该建模成值对象时,需要考虑是否拥有一些特性:

  • 度量或描述领域中的一件东西。
  • 可以作为不变对象。
  • 将不同的相关属性组合成一个概念整体。
  • 当度量或描述改变时,可以使用另一个值对象予以替换。
  • 可以与其他值对象进行相等性比较。
  • 不对对协作对象造成负面影响。

在使用这个特性分析模型时,你会发现很多领域概念都应该建模成值对象,而非实体。

值对象的特征汇总如下:

  1. 度量或描述。只是度量或描述领域中某件东西的一个概念。
  2. 不变性。值对象在创建后,就不会发生改变,如果需要改变的话,将创建一个新的值对象并对原有对象进行替换。
  3. 概念整体性。一个值对象可以只有一个属性,也可以拥有一组相关属性。如果一组属性联合起来并不能表达一个整体上的概念,那就没有什么意义。
  4. 有效性。值对象的构造函数应该用于保障概念整体性的有效性。
  5. 可替换性。如果需要改变的话,我们需要将整个值对象替换成一个新的值对象实例。
  6. 属性相等性。通过比较两个对象的类型和属性来决定其相等性。
  7. 方法无副作用。由于不变性,值对象的方法一般为一个无副作用函数,这个函数表示对某个对象的操作,它只用于产生输出,不会修改对象状态。

2 何时使用值对象

值对象是实体的状态,它描述与实体相关的概念。

2.1 表示描述性的、缺失身份的概念

当一个概念缺乏明显的身份时,基本可以断定它大概率是一个值对象。

比较典型的例子便是 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;...
}

2.2 增强确定性

领域驱动设计的一切都是为了明确传递业务规则和领域逻辑。像整数和字符串这样的技术单元并不适合这种情况。

比如邮箱可以使用字符串进行描述,但会丢失很多邮箱的特性,此时,需要将其建模成值对象。


@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]);}@Overridepublic String toString() {return this.getName() + "@" + this.getDomain();}
}

此时,邮箱是一个明确的领域概念,相比字符串方案,其拥有验证逻辑,同时享受编译器类型校验。

3 实现值对象

值对象是不可变的、无副作用并且易于测试的。

3.1 欠缺身份

缺失身份是值对象和实体最大的区别。

由于值对象没有身份,且描述了领域中重要的概念,通常,我们会先定义实体,然后找出与实体相关的值对象。一般情况下,值对象需要实体提供上下文相关性。

3.2 基于属性的相等性

如果实体具有相同的类型和标识,则会认为是相等的。相反,值对象要具有相同的值才会认为是相等的。

如果两个 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() + ")";}
}

3.3 富含行为

值对象应该尽可能多的暴露面向领域概念的行为。

在 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;}
}

3.4 内聚

通常情况下,值对象会内聚封装度量值和度量单位。在 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);}
}

3.5 不变性

一旦创建完成后,值对象就永远不能改变。

如果需要改变值对象,应该创建新的值对象,并由新的值对象替换旧值对象。 比如,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,从而对外隐藏修改方法。

3.6 可组合性

对于用于度量的值对象,通常会有数值,此时,可以将其组合起来以创建新的值。

比如 Money 的 add 方法,Money 加上 Money 会得到一个新的 Money。

public Money add(Money money){checkInput(money);return Money.apply(this.getTotalFee() + money.getTotalFee(), getFeeType());
}

3.7 自验证性

值对象作为一个概念整体,决不应该变成无效状态,它自身就应该负责对其进行验证。

通常情况下,在创建一个值对象实例时,如果参数与业务规则不一致,则构造函数应该抛出异常。

还是看我们的 Money 类,需要进行如下检验:

  1. 单位不能为 null;
  2. 金额不能为 null;
  3. 金额不能为负值。
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 中对值对象的有效性进行验证。

3.8 可测试性

不变性、内聚性和可组合性使值对象变的可测试。

还是看我们的 Money 对象的测试类。

public class MoneyTest {@Testpublic 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());}@Testpublic 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());}@Testpublic void multiply() {Money m1 = Money.apply(100L);Money money = m1.multiply(3);Assert.assertEquals(300L, money.getTotalFee().longValue());Assert.assertEquals(m1.getFeeType(), money.getFeeType());}@Testpublic 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);}
}

4 值对象建模模式

通过一些常用的值对象建模模式,可以提高值对象的处理体验。

4.1 静态工厂方法

静态工厂方法是更简单、更具有表达性的一种技巧。

比如 java 中的 Instant 的静态工厂方法。

public static Instant now() {...
}
public static Instant ofEpochSecond(long epochSecond) {...
}
public static Instant ofEpochMilli(long epochMilli){...
}

通过方法签名就能很清楚的了解其含义。

4.2 微类型

通过使用更具体的领域模型类型封装技术类型,使其更具表达能力。

典型的就是 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);}}

4.3 避免集合

通常情况下,需要尽量避免使用值对象集合。这种表达方式无法正确的表达领域概念。

使用值对象集合通常意味着需要使用某种形式来取出特定项,这就相当于为值对象添加了身份。 比如 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);}
}

5 持久化

处理值对象最难的点就在他们的持久化。一般情况下,不会直接对其进行持久化,值对象会作为实体的属性,一并进行持久化处理。

持久化过程即将对象序列化成文本格式或二进制格式,然后保存到计算机磁盘中。

在面向文档数据存储时,问题会少很多。我们可以在同一个文档中存储实体和值对象;然而,使用 SQL 数据库就麻烦的多,这将导致很多变化。

5.1 NoSQL

许多 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 文档的属性进行存储。

5.2 SQL

在 SQL 数据库中存储值对象,可以遵循标准的 SQL 约定,也可以使用范模式。

多数情况下,持久化值对象时,我们都是通过一种非范式的方式完成,即所有的属性和实体都保存在相同的数据库表中。有时,值对象需要以实体的身份进行持久化。比如聚合中维护一个值对象集合时。

5.2.1 多列存储单个值对象

基本思路就是将值对象与其所在的实体对象保存在同一张表中,值对象的每个属性保存为一列。

这种方式,是最常见的值对象序列化方式,也是冲突最小的方式,可以在查询中使用连接语句进行查询。

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]);}@Overridepublic String toString() {return this.getName() + "@" + this.getDomain();}
}

然后,在实体对于属性上添加 @Embedded 注解,标注该属性将展开存储。

@Data
@Entity
public class Person1 {@Embeddedprivate Email primary;
}
5.2.2 单列存储单个值对象

值对象的所有属性保存为一列。当不希望在查询中使用额外语句来连接他们时,这是一个很好的选择。

一般情况下,会涉及以下几个操作:

  1. 创建持久化格式。
  2. 在保存时进行数据转换。
  3. 在加载时解析值。

如,对于 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> {@Overridepublic String convertToDatabaseColumn(Email attribute) {return EmailSerializer.toJson(attribute);}@Overridepublic 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);}
}

此时,就完成了单个值对象的持久化。

5.2.3 多个值对象序列化到单个列中

这种应用是前种方案的扩展。将整个集合序列化成某种形式的文本,然后将该文本保存到单个数据库列中。

需要考虑的问题:

  1. 列宽。数据库列的长度不好确定。
  2. 不方便查询。由于值对象集合被序列化到扁平化文本中,值对象的属性不能使用 SQL 进行查询。
  3. 需要自定义类型。持久化框架对该类型的映射没有提供支撑,需要对其进行扩展。

如,对于 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> {@Overridepublic String convertToDatabaseColumn(List<Email> attribute) {return EmailListSerializer.toJson(attribute);}@Overridepublic List<Email> convertToEntityAttribute(String dbData) {return EmailListSerializer.toEmailList(dbData);}
}

属性配置:

@Data
@Setter(AccessLevel.PRIVATE)
public class PersonEmailListAsJpa {@Convert(converter = EmailListConverter.class)private List<Email> emails;}
5.2.4 使用数据库实体保存多个值对象

我们应该首先考虑将领域概念建模成值对象,而不是实体。

我们可以使用委派主键的方式,使用两层的层超类型。在上层隐藏委派主键。 这样我们可以自由的将其映射成数据库实体,同时在领域模型中将其建模成值对象。

首先,定义 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 IdentitiedObjectimplements 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]);}@Overridepublic String toString() {return this.getName() + "@" + this.getDomain();}
}

此时,就可以使用 JPA 的 @OneToMany 特性存储多个值:

@Data
@Entity
public class PersonOneToMany {@OneToManyprivate List<IdentitiedEmail> emails = Lists.newArrayList();
}
5.2.5 ORM 与 枚举状态对象

大多持久化框架都提供了对枚举类型的支持。要么使用枚举值得 String,要么使用枚举值得 Index,其实都不是最佳方案,对以后得重构不太友好,建议使用自定义 code 进行持久化处理。

定义枚举:

public enum PersonStatus implements CodeBasedEnum<PersonStatus> {ENABLE(1),DISABLE(0);private final int code;PersonStatus(int code) {this.code = code;}@Overridepublic 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> {@Overridepublic Integer convertToDatabaseColumn(PersonStatus attribute) {return attribute != null ? attribute.getCode() : null;}@Overridepublic PersonStatus convertToEntityAttribute(Integer dbData) {return dbData == null ? null : PersonStatus.parseByCode(dbData);}
}

配置属性:

@Data
@Setter(AccessLevel.PRIVATE)
public class Person{@Embeddedprivate Email primary;@Embeddedprivate 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 进行持久化。

5.2.6 阻抗

在使用 DB 进行值对象持久化时,经常遇到阻抗。

当面临阻抗时,我们应该从领域模型角度,而不是持久化角度去思考问题。

  • 根据领域模型来来设计数据模型,而不是通过数据模型来设计领域模型。
  • 报表和商业智能应该由专门的数据模型进行处理,而不是生产环境的数据模型。

6 值对象其他用途

6.1 用值对象表示标准类型

标准类型是用于表示事物类型的描述性对象。

Java 的枚举时实现标准类型的一种简单方法。枚举提供了一组有限数量的值对象,它是非常轻量的,并且无副作用。

一个共享的不变值对象,可以从持久化存储中获取,此时可以使用标准类型的领域服务和工厂来获取值对象。我们应该为每组标准类型创建一个领域服务或工厂。 如果打算使用常规值对象来表示标准类型,可以使用领域服务或工厂来静态的创建值对象实例。

6.2 最小集成

当模型概念从上游上下文流入下游上下文中,尽量使用值对象来表示这些概念。在有可能的情况下,使用值对象完成上下文之间的集成。

7 小结

  • 值对象是 DDD 建模结构体,它用于表示像度量这样的描述概念。
  • 值对象没有身份,比实体要简单得多。
  • 建议将数字和字符串封装成值对象,以更好的表示领域概念。
  • 值对象是不可变的,他们的值在创建后,就不在发生变化。
  • 值对象是内聚的,将多个特征封装成一个完整的概念。
  • 可以通过组合值对象来创建新的值对象,而不改变原始值。
  • 值对象是自验证的,它不应该处于无效状态。
  • 可以使用静态工厂、微类型等模式提高值对象的易用性。
  • 对于 NoSQL 的存储,直接使用反规范持久化值对象,面向文档数据库是首选。
  • 对于 SQL 存储,相对要麻烦下,存在大量的阻抗。

领域驱动设计战术模式--值对象相关推荐

  1. 领域驱动设计战术模式:领域服务

    领域驱动设计战术部分,是一组面向业务的设计模式,是基于技术的一种思维方式,相对开发人员来说更接地气,是提升个人格局比较好的切入点. 该文章为战术模式的第四篇,重心讲解领域服务模式. 在建模时,有时会遇 ...

  2. 领域驱动设计战术模式:领域事件

    领域驱动设计战术部分,是一组面向业务的设计模式,是基于技术的一种思维方式,相对开发人员来说更接地气,是提升个人格局比较好的切入点. 该文章为战术模式的第五篇,重心讲解领域事件模式. 在建模时,有时会遇 ...

  3. 领域驱动设计战术模式:实体

    领域驱动设计战术部分,是一组面向业务的设计模式,是基于技术的一种思维方式,相对开发人员来说更接地气,是提升个人格局比较好的切入点. 该文章为战术模式的第三篇,重心讲解实体模式. 实体是具有唯一标识的概 ...

  4. 领域驱动设计战术模式--领域事件

    使用领域事件来捕获发生在领域中的一些事情. 领域驱动实践者发现他们可以通过了解更多发生在问题域中的事件,来更好的理解问题域.这些事件,就是领域事件,主要是与领域专家一起进行知识提炼环节中获得. 领域事 ...

  5. JPA实现领域驱动设计(DDD) 中值对象的持久化

    文章目录 什么是DDD值对象? 实现方式 单一值对象 改变表中映射的字段的名称 根据值对象的单一属性查询或多属性的动态查询 多个值对象 根据值对象的单一属性查询或多属性的动态查询 什么是DDD值对象? ...

  6. 如何运用领域驱动设计 - 聚合

    概述 DDD实战与进阶 - 值对象 如何运用DDD - 实体 如何运用领域驱动设计 - 领域服务 在前几篇的博文中,我们已经学习到了如何运用实体和值对象.随着我们所在领域的不断深入,领域模型变得逐渐清 ...

  7. 如何运用领域驱动设计 - 领域服务

    概述 本文将介绍领域驱动设计(DDD)战术模式中另一个非常重要的概念 - 领域服务.在前面两篇博文中,我们已经学习到了什么是值对象和实体,并且能够比较清晰的定位它们自身的行为.但是在某些时候,你会发现 ...

  8. 谈谈微服务架构中的领域驱动设计

    谈谈微服务架构中的领域驱动设计 https://mp.weixin.qq.com/s/43HSud6ijdVzPA_wdLrxzQ 谈谈微服务架构中的领域驱动设计 本文是关于领域驱动设计与微服务架构结 ...

  9. 领域驱动设计,让程序员心中有码(七)

    领域驱动设计- 让程序员心中有码(七) -设计原则和设计模式,互联网开发者们共同的追求 前言 多年来,笔者一直从事传统软件企业的软件开发和项目管理工作.笔者发现在众多的传统软件企业中,评判优秀开发者的 ...

最新文章

  1. 机器学习 欧式距离及代码实现
  2. boost::lambda::is_instance_of_1用法的测试程序
  3. 独家专访Mockplus CEO老布,原型设计领域的弄潮儿
  4. log4j2自动删除_登录样式:log4j 2,上下文,自动清除…全部不附带任何字符串!...
  5. 著名开源项目_著名开源项目案例研究
  6. POJ 1088-滑雪
  7. Linux unzip命令:解压zip文件
  8. JVM 自定义的类加载器的实现和使用
  9. keras layer的weight是向后的还是向前的_「马上学tensorflow 2.0」Keras简介、使用流程,后端引擎原理,...
  10. 汕尾python高手_每天两小时,下一个python 高手就是你!满满都是干货
  11. formdata ie9怎么兼容_2021上半年教师资格证笔试报名如何设置兼容性?
  12. JZOJ5371 组合数问题
  13. Lattice - 规划模块 1.采样轨迹 2.计算轨迹cost 3 循环检测筛选轨迹
  14. 2022深圳杯/东三省数学建模ABCD题 赛题分析解题方案
  15. 2015年河南省省赛部分题题解
  16. html网页肯德基设计代码作业,Illustrator设计一幅肯德基广告单页制作教程
  17. php投影,投影+直播双模式方案搭建
  18. M3D GIS三维数字沙盘可视化交互地理信息系统开发教程第44课
  19. 苹果手机投影_智能手机投屏到投影仪
  20. fdm3d打印机有哪些? Stratasys多品类fdm3d打印机推荐

热门文章

  1. Android布局管理器-使用TableLayout表格布局管理器实现简单的用户登录页面
  2. CS中常用转义符与@符号的作用
  3. EasyUI中进度条的简单使用
  4. Python双版本下创建一个Scrapy(西瓜皮)项目
  5. 软考-信息系统项目管理师-项目整体管理
  6. RE:大家说说开发的时候类名和文件名一般是怎么规范的?
  7. flutter创建可移动的stack小部件
  8. C++特化的应用——类型萃取
  9. linux基于文本的配置工具,Linux基本配置和管理 3 ---- Linux命令行文本处理工具
  10. 人工智能缺陷与误觉:让机器产生幻觉的「怪异事件」 1