日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

领域驱动设计战术模式--值对象

發(fā)布時(shí)間:2025/3/18 编程问答 44 豆豆
生活随笔 收集整理的這篇文章主要介紹了 领域驱动设计战术模式--值对象 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

值對(duì)象雖然經(jīng)常被掩蓋在實(shí)體的陰影之下,但它卻是非常重要的 DDD 概念。

值對(duì)象不具有身份,它純粹用于描述實(shí)體的特性。處理不具有身份的值對(duì)象是很容易的,尤其是不變性與可組合性是支持易用性的兩個(gè)特征。

1 理解值對(duì)象

值對(duì)象用于度量和描述事物,我們可以非常容易的對(duì)值對(duì)象進(jìn)行創(chuàng)建、測(cè)試、使用、優(yōu)化和維護(hù)。

一個(gè)值對(duì)象,或者更簡(jiǎn)單的說(shuō),值,是對(duì)一個(gè)不變的概念整體建立的模型。在這個(gè)模型中,值就真的只有一個(gè)值。和實(shí)體不一樣,他沒(méi)有唯一標(biāo)識(shí),而是通過(guò)封裝屬性的對(duì)比來(lái)決定相等性。一個(gè)值對(duì)象不是事物,而是用來(lái)描述、量化或測(cè)量實(shí)體的。

當(dāng)你關(guān)系某個(gè)對(duì)象的屬性時(shí),該對(duì)象便是一個(gè)值對(duì)象。為其添加有意義的屬性,并賦予相應(yīng)的行為。我們需要將值對(duì)象看成不變對(duì)象,不要給他任何身份標(biāo)識(shí),還應(yīng)該盡量避免像實(shí)體對(duì)象一樣的復(fù)雜性。

即使一個(gè)領(lǐng)域概念必須建模成實(shí)體,在設(shè)計(jì)時(shí)也應(yīng)該更偏向于將其作為值對(duì)象的容器。

當(dāng)決定一個(gè)領(lǐng)域概念是否應(yīng)該建模成值對(duì)象時(shí),需要考慮是否擁有一些特性:

  • 度量或描述領(lǐng)域中的一件東西。
  • 可以作為不變對(duì)象。
  • 將不同的相關(guān)屬性組合成一個(gè)概念整體。
  • 當(dāng)度量或描述改變時(shí),可以使用另一個(gè)值對(duì)象予以替換。
  • 可以與其他值對(duì)象進(jìn)行相等性比較。
  • 不對(duì)對(duì)協(xié)作對(duì)象造成負(fù)面影響。

在使用這個(gè)特性分析模型時(shí),你會(huì)發(fā)現(xiàn)很多領(lǐng)域概念都應(yīng)該建模成值對(duì)象,而非實(shí)體。

值對(duì)象的特征匯總?cè)缦?#xff1a;

  • 度量或描述。只是度量或描述領(lǐng)域中某件東西的一個(gè)概念。
  • 不變性。值對(duì)象在創(chuàng)建后,就不會(huì)發(fā)生改變,如果需要改變的話,將創(chuàng)建一個(gè)新的值對(duì)象并對(duì)原有對(duì)象進(jìn)行替換。
  • 概念整體性。一個(gè)值對(duì)象可以只有一個(gè)屬性,也可以擁有一組相關(guān)屬性。如果一組屬性聯(lián)合起來(lái)并不能表達(dá)一個(gè)整體上的概念,那就沒(méi)有什么意義。
  • 有效性。值對(duì)象的構(gòu)造函數(shù)應(yīng)該用于保障概念整體性的有效性。
  • 可替換性。如果需要改變的話,我們需要將整個(gè)值對(duì)象替換成一個(gè)新的值對(duì)象實(shí)例。
  • 屬性相等性。通過(guò)比較兩個(gè)對(duì)象的類型和屬性來(lái)決定其相等性。
  • 方法無(wú)副作用。由于不變性,值對(duì)象的方法一般為一個(gè)無(wú)副作用函數(shù),這個(gè)函數(shù)表示對(duì)某個(gè)對(duì)象的操作,它只用于產(chǎn)生輸出,不會(huì)修改對(duì)象狀態(tài)。
  • 2 何時(shí)使用值對(duì)象

    值對(duì)象是實(shí)體的狀態(tài),它描述與實(shí)體相關(guān)的概念。

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

    當(dāng)一個(gè)概念缺乏明顯的身份時(shí),基本可以斷定它大概率是一個(gè)值對(duì)象。

    比較典型的例子便是 Money,大多數(shù)情況下,我們只關(guān)心它所代表的實(shí)際金額,為其分配標(biāo)識(shí)是一個(gè)沒(méi)有意義的操作。

    @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;... }復(fù)制代碼

    2.2 增強(qiáng)確定性

    領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)的一切都是為了明確傳遞業(yè)務(wù)規(guī)則和領(lǐng)域邏輯。像整數(shù)和字符串這樣的技術(shù)單元并不適合這種情況。

    比如郵箱可以使用字符串進(jìn)行描述,但會(huì)丟失很多郵箱的特性,此時(shí),需要將其建模成值對(duì)象。

    @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();} }復(fù)制代碼

    此時(shí),郵箱是一個(gè)明確的領(lǐng)域概念,相比字符串方案,其擁有驗(yàn)證邏輯,同時(shí)享受編譯器類型校驗(yàn)。

    3 實(shí)現(xiàn)值對(duì)象

    值對(duì)象是不可變的、無(wú)副作用并且易于測(cè)試的。

    3.1 欠缺身份

    缺失身份是值對(duì)象和實(shí)體最大的區(qū)別。

    由于值對(duì)象沒(méi)有身份,且描述了領(lǐng)域中重要的概念,通常,我們會(huì)先定義實(shí)體,然后找出與實(shí)體相關(guān)的值對(duì)象。一般情況下,值對(duì)象需要實(shí)體提供上下文相關(guān)性。

    3.2 基于屬性的相等性

    如果實(shí)體具有相同的類型和標(biāo)識(shí),則會(huì)認(rèn)為是相等的。相反,值對(duì)象要具有相同的值才會(huì)認(rèn)為是相等的。

    如果兩個(gè) Money 對(duì)象表示相等的金額,他們就被認(rèn)為是相等的。而不管他們是指向同一個(gè)實(shí)例還是不同的實(shí)例。

    在 Money 類中使用 lombok 插件自動(dòng)生成 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() + ")";} }復(fù)制代碼

    3.3 富含行為

    值對(duì)象應(yīng)該盡可能多的暴露面向領(lǐng)域概念的行為。

    在 Money 值對(duì)象中,可以看到暴露的方法:

    方法含義
    apply創(chuàng)建 Money
    addMoney 相加
    subtractMoney 相減
    multiplyMoney 相乘
    splitMoney 切分,將無(wú)法查分的誤差匯總到最后的 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;} } 復(fù)制代碼

    3.4 內(nèi)聚

    通常情況下,值對(duì)象會(huì)內(nèi)聚封裝度量值和度量單位。在 Money 中可以看到這一點(diǎn)。

    當(dāng)然,并不局限于此,對(duì)于擁有概念整體性的對(duì)象,都具有很強(qiáng)的內(nèi)聚性。比如,英文名稱,由 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);} } 復(fù)制代碼

    3.5 不變性

    一旦創(chuàng)建完成后,值對(duì)象就永遠(yuǎn)不能改變。

    如果需要改變值對(duì)象,應(yīng)該創(chuàng)建新的值對(duì)象,并由新的值對(duì)象替換舊值對(duì)象。 比如,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()); } 復(fù)制代碼

    只會(huì)創(chuàng)建新的 Money 對(duì)象,不會(huì)對(duì)原有對(duì)象進(jìn)行修改。

    在技術(shù)實(shí)現(xiàn)上,對(duì)于一個(gè)不可變對(duì)象,需要將所有字段設(shè)置為 final,并通過(guò)構(gòu)造函數(shù)為其賦值。但,有時(shí)為了迎合一些框架需求,需求進(jìn)行部分妥協(xié),及將 setter 方法設(shè)置為 private,從而對(duì)外隱藏修改方法。

    3.6 可組合性

    對(duì)于用于度量的值對(duì)象,通常會(huì)有數(shù)值,此時(shí),可以將其組合起來(lái)以創(chuàng)建新的值。

    比如 Money 的 add 方法,Money 加上 Money 會(huì)得到一個(gè)新的 Money。

    public Money add(Money money){checkInput(money);return Money.apply(this.getTotalFee() + money.getTotalFee(), getFeeType()); } 復(fù)制代碼

    3.7 自驗(yàn)證性

    值對(duì)象作為一個(gè)概念整體,決不應(yīng)該變成無(wú)效狀態(tài),它自身就應(yīng)該負(fù)責(zé)對(duì)其進(jìn)行驗(yàn)證。

    通常情況下,在創(chuàng)建一個(gè)值對(duì)象實(shí)例時(shí),如果參數(shù)與業(yè)務(wù)規(guī)則不一致,則構(gòu)造函數(shù)應(yīng)該拋出異常。

    還是看我們的 Money 類,需要進(jìn)行如下檢驗(yàn):

  • 單位不能為 null;
  • 金額不能為 null;
  • 金額不能為負(fù)值。
  • 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; } 復(fù)制代碼

    當(dāng)然,如果值對(duì)象的構(gòu)建過(guò)程過(guò)于復(fù)雜,可以使用 Factory 模式進(jìn)行構(gòu)建。此時(shí),應(yīng)該在 Factory 中對(duì)值對(duì)象的有效性進(jìn)行驗(yàn)證。

    3.8 可測(cè)試性

    不變性、內(nèi)聚性和可組合性使值對(duì)象變的可測(cè)試。

    還是看我們的 Money 對(duì)象的測(cè)試類。

    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);} } 復(fù)制代碼

    4 值對(duì)象建模模式

    通過(guò)一些常用的值對(duì)象建模模式,可以提高值對(duì)象的處理體驗(yàn)。

    4.1 靜態(tài)工廠方法

    靜態(tài)工廠方法是更簡(jiǎn)單、更具有表達(dá)性的一種技巧。

    比如 java 中的 Instant 的靜態(tài)工廠方法。

    public static Instant now() {... } public static Instant ofEpochSecond(long epochSecond) {... } public static Instant ofEpochMilli(long epochMilli){... } 復(fù)制代碼

    通過(guò)方法簽名就能很清楚的了解其含義。

    4.2 微類型

    通過(guò)使用更具體的領(lǐng)域模型類型封裝技術(shù)類型,使其更具表達(dá)能力。

    典型的就是 Mobile 封裝,其本質(zhì)是一個(gè) String。通過(guò) Mobile 封裝,使其具有字符串無(wú)法表達(dá)的含義。

    @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);}} 復(fù)制代碼

    4.3 避免集合

    通常情況下,需要盡量避免使用值對(duì)象集合。這種表達(dá)方式無(wú)法正確的表達(dá)領(lǐng)域概念。

    使用值對(duì)象集合通常意味著需要使用某種形式來(lái)取出特定項(xiàng),這就相當(dāng)于為值對(duì)象添加了身份。 比如 List 第一個(gè)代表是主郵箱,第二個(gè)表示是副郵箱,最佳的表達(dá)方式是直接用屬性進(jìn)行表式,如:

    @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);} } 復(fù)制代碼

    5 持久化

    處理值對(duì)象最難的點(diǎn)就在他們的持久化。一般情況下,不會(huì)直接對(duì)其進(jìn)行持久化,值對(duì)象會(huì)作為實(shí)體的屬性,一并進(jìn)行持久化處理。

    持久化過(guò)程即將對(duì)象序列化成文本格式或二進(jìn)制格式,然后保存到計(jì)算機(jī)磁盤(pán)中。

    在面向文檔數(shù)據(jù)存儲(chǔ)時(shí),問(wèn)題會(huì)少很多。我們可以在同一個(gè)文檔中存儲(chǔ)實(shí)體和值對(duì)象;然而,使用 SQL 數(shù)據(jù)庫(kù)就麻煩的多,這將導(dǎo)致很多變化。

    5.1 NoSQL

    許多 NoSQL 數(shù)據(jù)庫(kù)都使用了數(shù)據(jù)反規(guī)范化,為我們提供了很大便利。

    在 NoSQL 中,整個(gè)實(shí)體都可以作為一個(gè)文檔來(lái)建模。在 SQL 中的表連接、規(guī)范化數(shù)據(jù)和 ORM 延遲加載等相關(guān)問(wèn)題都不存在了。在值對(duì)象上下文中,這就意味著他們會(huì)與實(shí)體一起存儲(chǔ)。

    @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);} } 復(fù)制代碼

    面向文檔的 NoSQL 數(shù)據(jù)庫(kù)會(huì)將文檔持久化為 JSON,上例中 Person 的 primary 和 second 會(huì)作為 JSON 文檔的屬性進(jìn)行存儲(chǔ)。

    5.2 SQL

    在 SQL 數(shù)據(jù)庫(kù)中存儲(chǔ)值對(duì)象,可以遵循標(biāo)準(zhǔn)的 SQL 約定,也可以使用范模式。

    多數(shù)情況下,持久化值對(duì)象時(shí),我們都是通過(guò)一種非范式的方式完成,即所有的屬性和實(shí)體都保存在相同的數(shù)據(jù)庫(kù)表中。有時(shí),值對(duì)象需要以實(shí)體的身份進(jìn)行持久化。比如聚合中維護(hù)一個(gè)值對(duì)象集合時(shí)。

    5.2.1 多列存儲(chǔ)單個(gè)值對(duì)象

    基本思路就是將值對(duì)象與其所在的實(shí)體對(duì)象保存在同一張表中,值對(duì)象的每個(gè)屬性保存為一列。

    這種方式,是最常見(jiàn)的值對(duì)象序列化方式,也是沖突最小的方式,可以在查詢中使用連接語(yǔ)句進(jìn)行查詢。

    Jpa 提供 @Embeddable 和 @Embedded 兩個(gè)注解,以支持這種方式。

    首先,在值對(duì)象上添加 @Embeddable 注解,以標(biāo)注其為可嵌入對(duì)象。

    @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();} } 復(fù)制代碼

    然后,在實(shí)體對(duì)于屬性上添加 @Embedded 注解,標(biāo)注該屬性將展開(kāi)存儲(chǔ)。

    @Data @Entity public class Person1 {@Embeddedprivate Email primary; } 復(fù)制代碼
    5.2.2 單列存儲(chǔ)單個(gè)值對(duì)象

    值對(duì)象的所有屬性保存為一列。當(dāng)不希望在查詢中使用額外語(yǔ)句來(lái)連接他們時(shí),這是一個(gè)很好的選擇。

    一般情況下,會(huì)涉及以下幾個(gè)操作:

  • 創(chuàng)建持久化格式。
  • 在保存時(shí)進(jìn)行數(shù)據(jù)轉(zhuǎn)換。
  • 在加載時(shí)解析值。
  • 如,對(duì)于 Email 值對(duì)象,我們采用 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);} } 復(fù)制代碼

    JPA 中提供了 Converter 擴(kuò)展,以完成值對(duì)象到數(shù)據(jù)、數(shù)據(jù)到值對(duì)象的轉(zhuǎn)化:

    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);} }復(fù)制代碼

    Converter 完成后,需要將其配置在對(duì)應(yīng)的屬性上:

    @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);} } 復(fù)制代碼

    此時(shí),就完成了單個(gè)值對(duì)象的持久化。

    5.2.3 多個(gè)值對(duì)象序列化到單個(gè)列中

    這種應(yīng)用是前種方案的擴(kuò)展。將整個(gè)集合序列化成某種形式的文本,然后將該文本保存到單個(gè)數(shù)據(jù)庫(kù)列中。

    需要考慮的問(wèn)題:

  • 列寬。數(shù)據(jù)庫(kù)列的長(zhǎng)度不好確定。
  • 不方便查詢。由于值對(duì)象集合被序列化到扁平化文本中,值對(duì)象的屬性不能使用 SQL 進(jìn)行查詢。
  • 需要自定義類型。持久化框架對(duì)該類型的映射沒(méi)有提供支撐,需要對(duì)其進(jìn)行擴(kuò)展。
  • 如,對(duì)于 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);} } 復(fù)制代碼

    擴(kuò)展 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);} }復(fù)制代碼

    屬性配置:

    @Data @Setter(AccessLevel.PRIVATE) public class PersonEmailListAsJpa {@Convert(converter = EmailListConverter.class)private List<Email> emails;} 復(fù)制代碼
    5.2.4 使用數(shù)據(jù)庫(kù)實(shí)體保存多個(gè)值對(duì)象

    我們應(yīng)該首先考慮將領(lǐng)域概念建模成值對(duì)象,而不是實(shí)體。

    我們可以使用委派主鍵的方式,使用兩層的層超類型。在上層隱藏委派主鍵。 這樣我們可以自由的將其映射成數(shù)據(jù)庫(kù)實(shí)體,同時(shí)在領(lǐng)域模型中將其建模成值對(duì)象。

    首先,定義 IdentitiedObject 用以隱藏?cái)?shù)據(jù)庫(kù) ID。

    @MappedSuperclass public class IdentitiedObject {@Setter(AccessLevel.PRIVATE)@Getter(AccessLevel.PRIVATE)@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id; } 復(fù)制代碼

    然后,從 IdentitiedObject 派生出 IdentitiedEmail 類,用以完成值對(duì)象建模。

    @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();} }復(fù)制代碼

    此時(shí),就可以使用 JPA 的 @OneToMany 特性存儲(chǔ)多個(gè)值:

    @Data @Entity public class PersonOneToMany {@OneToManyprivate List<IdentitiedEmail> emails = Lists.newArrayList(); } 復(fù)制代碼
    5.2.5 ORM 與 枚舉狀態(tài)對(duì)象

    大多持久化框架都提供了對(duì)枚舉類型的支持。要么使用枚舉值得 String,要么使用枚舉值得 Index,其實(shí)都不是最佳方案,對(duì)以后得重構(gòu)不太友好,建議使用自定義 code 進(jìn)行持久化處理。

    定義枚舉:

    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;} }復(fù)制代碼

    擴(kuò)展枚舉 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);} }復(fù)制代碼

    配置屬性:

    @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);} } 復(fù)制代碼

    此時(shí),通過(guò)枚舉對(duì)象中的 code 進(jìn)行持久化。

    5.2.6 阻抗

    在使用 DB 進(jìn)行值對(duì)象持久化時(shí),經(jīng)常遇到阻抗。

    當(dāng)面臨阻抗時(shí),我們應(yīng)該從領(lǐng)域模型角度,而不是持久化角度去思考問(wèn)題。

    • 根據(jù)領(lǐng)域模型來(lái)來(lái)設(shè)計(jì)數(shù)據(jù)模型,而不是通過(guò)數(shù)據(jù)模型來(lái)設(shè)計(jì)領(lǐng)域模型。
    • 報(bào)表和商業(yè)智能應(yīng)該由專門的數(shù)據(jù)模型進(jìn)行處理,而不是生產(chǎn)環(huán)境的數(shù)據(jù)模型。

    6 值對(duì)象其他用途

    6.1 用值對(duì)象表示標(biāo)準(zhǔn)類型

    標(biāo)準(zhǔn)類型是用于表示事物類型的描述性對(duì)象。

    Java 的枚舉時(shí)實(shí)現(xiàn)標(biāo)準(zhǔn)類型的一種簡(jiǎn)單方法。枚舉提供了一組有限數(shù)量的值對(duì)象,它是非常輕量的,并且無(wú)副作用。

    一個(gè)共享的不變值對(duì)象,可以從持久化存儲(chǔ)中獲取,此時(shí)可以使用標(biāo)準(zhǔn)類型的領(lǐng)域服務(wù)和工廠來(lái)獲取值對(duì)象。我們應(yīng)該為每組標(biāo)準(zhǔn)類型創(chuàng)建一個(gè)領(lǐng)域服務(wù)或工廠。 如果打算使用常規(guī)值對(duì)象來(lái)表示標(biāo)準(zhǔn)類型,可以使用領(lǐng)域服務(wù)或工廠來(lái)靜態(tài)的創(chuàng)建值對(duì)象實(shí)例。

    6.2 最小集成

    當(dāng)模型概念從上游上下文流入下游上下文中,盡量使用值對(duì)象來(lái)表示這些概念。在有可能的情況下,使用值對(duì)象完成上下文之間的集成。

    7 小結(jié)

    • 值對(duì)象是 DDD 建模結(jié)構(gòu)體,它用于表示像度量這樣的描述概念。
    • 值對(duì)象沒(méi)有身份,比實(shí)體要簡(jiǎn)單得多。
    • 建議將數(shù)字和字符串封裝成值對(duì)象,以更好的表示領(lǐng)域概念。
    • 值對(duì)象是不可變的,他們的值在創(chuàng)建后,就不在發(fā)生變化。
    • 值對(duì)象是內(nèi)聚的,將多個(gè)特征封裝成一個(gè)完整的概念。
    • 可以通過(guò)組合值對(duì)象來(lái)創(chuàng)建新的值對(duì)象,而不改變?cè)贾怠?/li>
    • 值對(duì)象是自驗(yàn)證的,它不應(yīng)該處于無(wú)效狀態(tài)。
    • 可以使用靜態(tài)工廠、微類型等模式提高值對(duì)象的易用性。
    • 對(duì)于 NoSQL 的存儲(chǔ),直接使用反規(guī)范持久化值對(duì)象,面向文檔數(shù)據(jù)庫(kù)是首選。
    • 對(duì)于 SQL 存儲(chǔ),相對(duì)要麻煩下,存在大量的阻抗。
    與50位技術(shù)專家面對(duì)面20年技術(shù)見(jiàn)證,附贈(zèng)技術(shù)全景圖

    總結(jié)

    以上是生活随笔為你收集整理的领域驱动设计战术模式--值对象的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

    如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。