值對象雖然常常被掩蓋在實體的陰影之下,但它倒是很是重要的 DDD 概念。java
值對象不具備身份,它純粹用於描述實體的特性。處理不具備身份的值對象是很容易的,尤爲是不變性與可組合性是支持易用性的兩個特徵。數據庫
值對象用於度量和描述事物,咱們能夠很是容易的對值對象進行建立、測試、使用、優化和維護。
一個值對象,或者更簡單的說,值,是對一個不變的概念總體創建的模型。在這個模型中,值就真的只有一個值。和實體不同,他沒有惟一標識,而是經過封裝屬性的對比來決定相等性。一個值對象不是事物,而是用來描述、量化或測量實體的。json
當你關係某個對象的屬性時,該對象即是一個值對象。爲其添加有意義的屬性,並賦予相應的行爲。咱們須要將值對象當作不變對象,不要給他任何身份標識,還應該儘可能避免像實體對象同樣的複雜性。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<Email> 第一個表明是主郵箱,第二個表示是副郵箱,最佳的表達方式是直接用屬性進行表式,如:
@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<Email> 選擇 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 的枚舉時實現標準類型的一種簡單方法。枚舉提供了一組有限數量的值對象,它是很是輕量的,而且無反作用。
一個共享的不變值對象,能夠從持久化存儲中獲取,此時可使用標準類型的領域服務和工廠來獲取值對象。咱們應該爲每組標準類型建立一個領域服務或工廠。
若是打算使用常規值對象來表示標準類型,可使用領域服務或工廠來靜態的建立值對象實例。
當模型概念從上游上下文流入下游上下文中,儘可能使用值對象來表示這些概念。在有可能的狀況下,使用值對象完成上下文之間的集成。