值物件雖然經常被掩蓋在實體的陰影之下,但它卻是非常重要的 DDD 概念。
值物件不具有身份,它純粹用於描述實體的特性。處理不具有身份的值物件是很容易的,尤其是不變性與可組合性是支援易用性的兩個特徵。
1 理解值物件
值物件用於度量和描述事物,我們可以非常容易的對值物件進行建立、測試、使用、優化和維護。
一個值物件,或者更簡單的說,值,是對一個不變的概念整體建立的模型。在這個模型中,值就真的只有一個值。和實體不一樣,他沒有唯一標識,而是通過封裝屬性的對比來決定相等性。一個值物件不是事物,而是用來描述、量化或測量實體的。
當你關係某個物件的屬性時,該物件便是一個值物件。為其新增有意義的屬性,並賦予相應的行為。我們需要將值物件看成不變物件,不要給他任何身份標識,還應該儘量避免像實體物件一樣的複雜性。
即使一個領域概念必須建模成實體,在設計時也應該更偏向於將其作為值物件的容器。
當決定一個領域概念是否應該建模成值物件時,需要考慮是否擁有一些特性:
- 度量或描述領域中的一件東西。
- 可以作為不變物件。
- 將不同的相關屬性組合成一個概念整體。
- 當度量或描述改變時,可以使用另一個值物件予以替換。
- 可以與其他值物件進行相等性比較。
- 不對對協作物件造成負面影響。
在使用這個特性分析模型時,你會發現很多領域概念都應該建模成值物件,而非實體。
值物件的特徵彙總如下:
- 度量或描述。只是度量或描述領域中某件東西的一個概念。
- 不變性。值物件在建立後,就不會發生改變,如果需要改變的話,將建立一個新的值物件並對原有物件進行替換。
- 概念整體性。一個值物件可以只有一個屬性,也可以擁有一組相關屬性。如果一組屬性聯合起來並不能表達一個整體上的概念,那就沒有什麼意義。
- 有效性。值物件的建構函式應該用於保障概念整體性的有效性。
- 可替換性。如果需要改變的話,我們需要將整個值物件替換成一個新的值物件例項。
- 屬性相等性。通過比較兩個物件的型別和屬性來決定其相等性。
- 方法無副作用。由於不變性,值物件的方法一般為一個無副作用函式,這個函式表示對某個物件的操作,它只用於產生輸出,不會修改物件狀態。
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]);
}
@Override
public 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 類,需要進行如下檢驗:
- 單位不能為 null;
- 金額不能為 null;
- 金額不能為負值。
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 {
@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);
}
}
複製程式碼
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]);
}
@Override
public String toString() {
return this.getName() + "@" + this.getDomain();
}
}
複製程式碼
然後,在實體對於屬性上新增 @Embedded 註解,標註該屬性將展開儲存。
@Data
@Entity
public class Person1 {
@Embedded
private Email primary;
}
複製程式碼
5.2.2 單列儲存單個值物件
值物件的所有屬性儲存為一列。當不希望在查詢中使用額外語句來連線他們時,這是一個很好的選擇。
一般情況下,會涉及以下幾個操作:
- 建立持久化格式。
- 在儲存時進行資料轉換。
- 在載入時解析值。
如,對於 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);
}
}
複製程式碼
此時,就完成了單個值物件的持久化。
5.2.3 多個值物件序列化到單個列中
這種應用是前種方案的擴充套件。將整個集合序列化成某種形式的文字,然後將該文字儲存到單個資料庫列中。
需要考慮的問題:
- 列寬。資料庫列的長度不好確定。
- 不方便查詢。由於值物件集合被序列化到扁平化文字中,值物件的屬性不能使用 SQL 進行查詢。
- 需要自定義型別。持久化框架對該型別的對映沒有提供支撐,需要對其進行擴充套件。
如,對於 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;
}
複製程式碼
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 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();
}
複製程式碼
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;
}
@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 進行持久化。
5.2.6 阻抗
在使用 DB 進行值物件持久化時,經常遇到阻抗。
當面臨阻抗時,我們應該從領域模型角度,而不是持久化角度去思考問題。
- 根據領域模型來來設計資料模型,而不是通過資料模型來設計領域模型。
- 報表和商業智慧應該由專門的資料模型進行處理,而不是生產環境的資料模型。
6 值物件其他用途
6.1 用值物件表示標準型別
標準型別是用於表示事物型別的描述性物件。
Java 的列舉時實現標準型別的一種簡單方法。列舉提供了一組有限數量的值物件,它是非常輕量的,並且無副作用。
一個共享的不變值物件,可以從持久化儲存中獲取,此時可以使用標準型別的領域服務和工廠來獲取值物件。我們應該為每組標準型別建立一個領域服務或工廠。 如果打算使用常規值物件來表示標準型別,可以使用領域服務或工廠來靜態的建立值物件例項。
6.2 最小整合
當模型概念從上游上下文流入下游上下文中,儘量使用值物件來表示這些概念。在有可能的情況下,使用值物件完成上下文之間的整合。
7 小結
- 值物件是 DDD 建模結構體,它用於表示像度量這樣的描述概念。
- 值物件沒有身份,比實體要簡單得多。
- 建議將數字和字串封裝成值物件,以更好的表示領域概念。
- 值物件是不可變的,他們的值在建立後,就不在發生變化。
- 值物件是內聚的,將多個特徵封裝成一個完整的概念。
- 可以通過組合值物件來建立新的值物件,而不改變原始值。
- 值物件是自驗證的,它不應該處於無效狀態。
- 可以使用靜態工廠、微型別等模式提高值物件的易用性。
- 對於 NoSQL 的儲存,直接使用反規範持久化值物件,面向文件資料庫是首選。
- 對於 SQL 儲存,相對要麻煩下,存在大量的阻抗。