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

歡迎訪問 生活随笔!

生活随笔

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

编程问答

领域驱动设计(DDD)实践之路(三):如何设计聚合

發(fā)布時(shí)間:2023/12/8 编程问答 54 豆豆
生活随笔 收集整理的這篇文章主要介紹了 领域驱动设计(DDD)实践之路(三):如何设计聚合 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

本文首發(fā)于 vivo互聯(lián)網(wǎng)技術(shù) 微信公眾號(hào)?
鏈接:https://mp.weixin.qq.com/s/oAD25H0UKH4zujxFDRXu9Q
作者:wenbo zhang

【領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)實(shí)踐之路】往期精彩文章:

  • 《領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)(DDD)實(shí)踐之路(一)》 主要講述了戰(zhàn)略層面的DDD原則

  • 《領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)(DDD)實(shí)踐之路(二):事件驅(qū)動(dòng)與CQRS》分析了如何應(yīng)用事件來分離軟件核心復(fù)雜度。

這是“領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)實(shí)踐之路”系列的第三篇文章,分析了如何設(shè)計(jì)聚合。聚合這個(gè)概念看似很簡(jiǎn)單,實(shí)際上有很多因素導(dǎo)致我們建立不正確的聚合模型。本文對(duì)這些問題逐一進(jìn)行剖析。

聚合這個(gè)概念看似很簡(jiǎn)單,實(shí)際上有很多因素導(dǎo)致我們建立不正確的聚合模型。一方面,我們可能為了使用上的一時(shí)便利將聚合設(shè)計(jì)得很大。另一方面,因?yàn)檫吔?、職?zé)的模糊性將一些重要的方法放在了其他地方進(jìn)而導(dǎo)致業(yè)務(wù)規(guī)則的泄露,沒有達(dá)到聚合對(duì)業(yè)務(wù)邊界的保護(hù)目的。在開始聚合之前,我們要區(qū)分清楚“實(shí)體Entity”“值對(duì)象Value Obj”的區(qū)別,并且要重視“值對(duì)象Value Obj”的真正價(jià)值。

(圖片來源于網(wǎng)絡(luò))

一、實(shí)體(Entity) OR 值對(duì)象(Value Obj)

領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)里面有兩個(gè)重要的概念,“實(shí)體Entity”“值對(duì)象Value Obj”。很多人講解時(shí)候會(huì)舉類似這樣的例子:用戶在某電商平臺(tái)下單,其收貨地址為“XX市YY街道ZZ園區(qū)”。現(xiàn)實(shí)場(chǎng)景中多個(gè)用戶的收貨地址有可能是同一個(gè),所以會(huì)把地址建模成Value Obj,借此把Value Obj簡(jiǎn)單解釋成“描述性的、不變的東西,比如地址”。這樣的解釋似乎也能說明問題,但是我覺得還沒有深入到本質(zhì)去探究、容易忽略Value Obj的真正要義。

1、實(shí)體Entity

一些對(duì)象不僅僅是由它們的屬性定義組成的,我們更關(guān)心其延續(xù)生命周期內(nèi)經(jīng)歷的不同狀態(tài)階段,這是我們業(yè)務(wù)域的核心。我們出于追蹤的目的,需要給每一個(gè)實(shí)體設(shè)置唯一標(biāo)識(shí)。通常的,我們也會(huì)將其持久化到數(shù)據(jù)庫中,實(shí)體即表里的一行記錄。因此,當(dāng)我們需要考慮一個(gè)對(duì)象的個(gè)性特征,或者需要區(qū)分不同的對(duì)象時(shí),我們引入實(shí)體這個(gè)領(lǐng)域概念。一個(gè)實(shí)體是一個(gè)唯一的東西,并且可以在相當(dāng)長的一段時(shí)間內(nèi)持續(xù)地變化。我們可以對(duì)實(shí)體做多次修改,故一個(gè)實(shí)體對(duì)象可能和它先前的狀態(tài)大不相同。但是,由于它們擁有相同的身份標(biāo)識(shí)(identity),它們依然是同一個(gè)實(shí)體。對(duì)于某電商平臺(tái)而言,一個(gè)個(gè)的用戶就是實(shí)體,我們要對(duì)他們加以區(qū)別并且持續(xù)的關(guān)注他們的行為。

實(shí)體有特殊的建模和設(shè)計(jì)思路。它們具有生命周期,這期間它們的形式和內(nèi)容可能發(fā)生根本改變,但必須保持一種內(nèi)在的連續(xù)性,即全局唯一的id。它們的類定義、職責(zé)、屬性和關(guān)聯(lián)必須由其標(biāo)識(shí)來決定,而不依賴于其所具有的屬性。即使對(duì)于那些不發(fā)生根本變化或者生命周期不太復(fù)雜的實(shí)體,也可以在語義上把它們作為實(shí)體來對(duì)待,這樣可以得到更清晰的模型和更健壯的實(shí)現(xiàn)。當(dāng)然,軟件系統(tǒng)中的大多數(shù)實(shí)體可以是任何事物,只要滿足兩個(gè)條件即可,一是它在整個(gè)生命周期中具有連續(xù)性,二是它的區(qū)別并不是由那些對(duì)用戶非常重要的屬性決定的。根據(jù)業(yè)務(wù)場(chǎng)景的不同,實(shí)體可以是一個(gè)人、一座城市、一輛汽車、一張彩票或一次銀行交易。

跟蹤實(shí)體的標(biāo)識(shí)是非常重要的,但為其他所有對(duì)象也加上標(biāo)識(shí)會(huì)影響系統(tǒng)性能并增加分析工作,而且會(huì)使模型變得混亂,因?yàn)樗袑?duì)象看起來都是相同的。軟件設(shè)計(jì)要時(shí)刻與復(fù)雜性做斗爭(zhēng),我們必須區(qū)別對(duì)待問題,僅在真正需要的地方進(jìn)行特殊處理。比如在上面的例子中,我們把收貨地址“XX市YY街道ZZ園區(qū)”建模成具有唯一標(biāo)識(shí)的實(shí)體,那么三個(gè)用戶就會(huì)創(chuàng)建三個(gè)地址,這對(duì)于系統(tǒng)來說完全沒有必要甚至還會(huì)導(dǎo)致性能或者數(shù)據(jù)一致性問題。

2、值對(duì)象Value Obj

當(dāng)我們只關(guān)心一個(gè)模型元素的屬性時(shí),應(yīng)把它歸類為值對(duì)象。我們應(yīng)該使這個(gè)模型元素能夠表示出其屬性的意義,并為它提供相關(guān)功能。值對(duì)象應(yīng)該是不可變的;不要為它分配任何標(biāo)識(shí),而且不要把它設(shè)計(jì)成像實(shí)體那么復(fù)雜。即描述了領(lǐng)域中的一些屬性,比如用戶的名字、聯(lián)系方式。當(dāng)然也會(huì)存在一些復(fù)雜的描述信息,其本身可能就是一個(gè)對(duì)象,甚至是另一個(gè)實(shí)體概念。

在前述的電商例子中地址是一個(gè)值對(duì)象。但在國家的郵政系統(tǒng)中,國家可能組織為一個(gè)由省、城市、郵政區(qū)、街區(qū)以及最終的個(gè)人地址組成的層次結(jié)構(gòu)。這些地址對(duì)象可以從它們?cè)趯哟谓Y(jié)構(gòu)中的父對(duì)象獲取郵政編碼,而且如果郵政服務(wù)決定重新劃分郵政區(qū),那么所有地址都將隨之改變。在這里地址是一個(gè)實(shí)體。

在電力運(yùn)營公司的軟件中,一個(gè)地址對(duì)應(yīng)于公司線路和服務(wù)的一個(gè)目的地。如果幾個(gè)室友各自打電話申請(qǐng)電力服務(wù),公司需要知道他們其實(shí)是住在同一個(gè)地方,因?yàn)槲覀冋鎸?shí)服務(wù)的是用戶所在地方的電力資源,在這種情況下,我們會(huì)認(rèn)為地址是一個(gè)實(shí)體。但是隨著思考的深入,我們發(fā)現(xiàn)可以換種方式,抽象出一個(gè)電力服務(wù)模型并與地址關(guān)聯(lián)起來。通過這樣的設(shè)計(jì)以后,我們發(fā)現(xiàn)真正的實(shí)體是電力服務(wù),地址不過是一個(gè)具有描述性的值對(duì)象而已。

在房屋設(shè)計(jì)軟件中,可以把每種窗戶樣式視為一個(gè)對(duì)象。我們可以將“窗戶樣式”連同它的高度、寬度以及修改和組合這些屬性的規(guī)則一起放到“窗戶”對(duì)象中。這些窗戶就是由其他值對(duì)象組成的復(fù)雜值對(duì)象,比如圓形天窗、1m規(guī)格平開窗、狹長的哥特式客廳窗戶等等。對(duì)于“墻”對(duì)象而言,所關(guān)聯(lián)的“窗戶”就是一個(gè)值對(duì)象,因?yàn)閮H僅起到描述的作用,“墻”不會(huì)去關(guān)心這個(gè)窗子昨天是什么樣,以至于當(dāng)我們覺得這個(gè)窗戶不合適的時(shí)候直接用另外一個(gè)窗戶替換即可。

歸根結(jié)底,我們使用這個(gè)窗戶對(duì)象來描述墻的窗戶屬性。但是在該房屋設(shè)計(jì)軟件的素材系統(tǒng)中,它的主要職責(zé)就是管理窗戶這一類的附屬組件,那么對(duì)它而言窗戶就是一個(gè)鮮活的實(shí)體。從這個(gè)例子中我們可以看出,所屬業(yè)務(wù)域很重要,這也就是我們之前所講述的上下文,即同一對(duì)象在不同上下文中是不一樣的。

當(dāng)你決定一個(gè)領(lǐng)域概念是否是一個(gè)值對(duì)象時(shí),你需要考慮它是否擁有以下特征:

  • 它度量或者描述了領(lǐng)域中的某個(gè)概念屬性;

    當(dāng)你的模型中的確存在一個(gè)值對(duì)象時(shí),不管你是否意識(shí)到,它都不應(yīng)該成為你領(lǐng)域中的一件東西,而只是用于度量或描述領(lǐng)域中某件東西的一個(gè)概念。一個(gè)人擁有年齡,這里的年齡并不是一個(gè)實(shí)在的東西,而只是作為你出生了多少年的一種度量。一個(gè)人擁有名字,同樣這里的名字也不是一個(gè)實(shí)在的東西,而是描述了如何稱呼這個(gè)人。

  • 它可以作為不變量;

    值對(duì)象可能會(huì)被共享,所以具有不變性,即調(diào)用方不能對(duì)其執(zhí)行set操作。

  • 它將不同的相關(guān)的屬性組合成一個(gè)概念整體;

    一個(gè)值對(duì)象可以只處理單個(gè)屬性,也可以處理一組相關(guān)聯(lián)的屬性。在這組相關(guān)聯(lián)的屬性中,每一個(gè)屬性都是整體屬性所不可或缺的組成部分,這和簡(jiǎn)單地將一組屬性組裝在對(duì)象中是不同的。如果一組屬性聯(lián)合起來并不能表達(dá)一個(gè)整體上的概念,那么這種聯(lián)合并無多大用處。比如貨幣與單位、幣種應(yīng)該是一個(gè)整體概念,否則很難明白12到底代表什么意思?12美分還是12元RMB。

  • 當(dāng)度量和描述改變時(shí),可以用另一個(gè)值對(duì)象予以替換;

    比如隨著時(shí)間推移,用戶年齡從21歲變成22歲,即22替換21。

二、聚合(Aggregate)

每個(gè)對(duì)象都有生命周期,對(duì)象自創(chuàng)建后可能會(huì)經(jīng)歷各種不同的狀態(tài),要么被暫存、要么刪除直至最終消亡。當(dāng)然,很多對(duì)象是簡(jiǎn)單的臨時(shí)對(duì)象,僅通過調(diào)用構(gòu)造函數(shù)來創(chuàng)建,用來做一些計(jì)算,而后由垃圾收集器回收。這類對(duì)象沒必要搞得那么復(fù)雜。但有些對(duì)象具有更長的生命周期,其中一部分時(shí)間不是在活動(dòng)內(nèi)存中度過的。它們與其他對(duì)象具有復(fù)雜的相互依賴性。它們會(huì)經(jīng)歷一些狀態(tài)變化,在變化時(shí)要遵守一些固定規(guī)則。管理這些對(duì)象時(shí)面臨諸多挑戰(zhàn),稍有不慎就會(huì)把自己帶入一個(gè)大泥坑。

減少設(shè)計(jì)中的關(guān)聯(lián)有助于簡(jiǎn)化對(duì)象之間的遍歷,并在某種程度上限制關(guān)系的急劇增多。但大多數(shù)業(yè)務(wù)領(lǐng)域中的對(duì)象都具有十分復(fù)雜的聯(lián)系,以至于最終會(huì)形成很長、很深的對(duì)象引用路徑,我們不得不在這個(gè)路徑上追蹤對(duì)象。在某種程度上,這種混亂狀態(tài)反映了現(xiàn)實(shí)世界,因?yàn)楝F(xiàn)實(shí)世界中就很少有清晰的邊界。但這卻是軟件設(shè)計(jì)中的一個(gè)重要問題,幸而我們可以借助“聚合”來應(yīng)對(duì)。

?

首先,我們需要用一個(gè)抽象來封裝模型中的引用。聚合就是一組相關(guān)對(duì)象的集合,我們把它作為數(shù)據(jù)修改的單元。每個(gè)都有一個(gè)根(root)和一個(gè)邊界(boundary)。邊界定義了聚合內(nèi)部都有什么。根則是聚合所包含的一個(gè)特定實(shí)體。對(duì)聚合而言,外部對(duì)象只可以引用根,而邊界內(nèi)部的對(duì)象之間則可以互相引用。除根以外的其他實(shí)體都有本地標(biāo)識(shí),但這些標(biāo)識(shí)只在聚合內(nèi)部才需要加以區(qū)別,因?yàn)橥獠繉?duì)象除了根之外看不到其他對(duì)象。

三、一些關(guān)于聚合的實(shí)踐

關(guān)于聚合、實(shí)體的概念已經(jīng)描述清楚了,下面我打算借助一個(gè)例子來繼續(xù)深入探討聚合的相關(guān)知識(shí)。

案例:汽車模型設(shè)計(jì)

約束:首先一輛汽車在車輛登記機(jī)構(gòu)歸屬于唯一一個(gè)人或者企業(yè)主體(實(shí)際上企業(yè)也具有法人,所以即使是企業(yè)主體也可以找到對(duì)應(yīng)的歸屬人);其次,正如大家所常見的,我們探討是目前技術(shù)所能實(shí)現(xiàn)的、且普遍流行的車輛結(jié)構(gòu),一輛車具有4個(gè)輪子、一個(gè)引擎;

1、業(yè)務(wù)邊界

Car、Customer很自然的按照實(shí)體進(jìn)行對(duì)待;發(fā)動(dòng)機(jī)作為一個(gè)產(chǎn)品交付時(shí)候有唯一序列號(hào),考慮到其可能的特性我們姑且也視其為實(shí)體;因?yàn)橛?個(gè)輪子,可能需要進(jìn)行區(qū)分所以也被視為實(shí)體。綜上可知,我們先把4個(gè)對(duì)象都當(dāng)做實(shí)體。因?yàn)槭墙F囅嚓P(guān)業(yè)務(wù),所以我們把Car視為根。至此,我們得到了一個(gè)強(qiáng)大的聚合,包含車輪、引擎以及所屬人信息。

public class Car {private Customer customer;/*** WheelPositionEnum枚舉標(biāo)識(shí)輪子狀態(tài)* FR FL BR BL依次標(biāo)識(shí)前右、前左、后右、后左輪* 在聚合內(nèi)部保持獨(dú)立*/private Map<String, Wheel> wheels;private Engine engine;//其他屬性暫略 }

當(dāng)我們分析出聚合以后,事情還沒有結(jié)束。聚合表達(dá)的是業(yè)務(wù),那么業(yè)務(wù)的規(guī)則、約束如何來保證呢?

  • 根ENTITY即Car具有全局標(biāo)識(shí),它最終負(fù)責(zé)檢查固定規(guī)則。

  • 根ENTITY具有全局標(biāo)識(shí)。邊界內(nèi)的ENTITY具有本地標(biāo)識(shí),這些標(biāo)識(shí)只在從聚合內(nèi)部才是唯一的,比如上面的車輪集合。

  • 刪除操作必須一次刪除AGGREGATE邊界之內(nèi)的所有對(duì)象。(利用垃圾收集機(jī)制,這很容易做到。由于除根以外的其他對(duì)象都沒有外部引用,因此刪除了根以后,其他對(duì)象均會(huì)被回收。)我們可以想象,當(dāng)汽車不存在的時(shí)候,我們更不會(huì)去關(guān)心其車輪情況,“皮之不存毛將焉附”。

  • AGGREGATE外部的對(duì)象不能引用除根ENTITY之外的任何內(nèi)部對(duì)象。即我們不可能先獲取到車輪對(duì)象,然后去反向獲取Car對(duì)象,這樣就等于建立了Car、Wheel的雙向關(guān)聯(lián)并且對(duì)調(diào)用方而言會(huì)很困惑。我什么情況下可以直接使用Wheel、何時(shí)可以直接使用Car,這是系統(tǒng)走向腐敗的第一步。

現(xiàn)在我們看下代碼實(shí)現(xiàn),Car具有全局唯一id用以區(qū)分不同對(duì)象;且負(fù)責(zé)約束的檢查,比如是否具有4個(gè)輪子、是否有一個(gè)引擎,否則不能正常使用。也許我們?nèi)粘i_發(fā)中的做法是調(diào)用方獲取到一個(gè)Car實(shí)例以后,去校驗(yàn)這些規(guī)則是否滿足,這樣做的問題就是業(yè)務(wù)規(guī)則的泄露。

public Car getCar(Long id) {Car car = carRepostory.ofId(id);if (car.getEngine() == null ||car.getWheels().keySet().size() != SPECIFIC_WHEEL_SIZE) {throw new CarStatusException(id);}return car; }/** *上述代碼存在的問題,畢竟現(xiàn)實(shí)中有報(bào)廢、廢棄的Car *1.命名getCar實(shí)際上進(jìn)行了狀態(tài)檢查,命名與實(shí)際語義不符; *2.Car的狀態(tài)約束泄露到調(diào)用方; *3.雖然面向流程寫出的是可以工作的代碼,但我們更推薦 * 面向領(lǐng)域的封裝代碼; **/ public Car getWorkableCar(Long id) {Car car = carRepostory.ofId(id);//業(yè)務(wù)約束由Car自己承擔(dān)if (!car.workable()) {throw new CarStatusException(id);}return car; }

2、警惕性能問題

在具有復(fù)雜關(guān)聯(lián)的模型中,要保證對(duì)象更改的一致性是很困難的。不僅互不關(guān)聯(lián)的對(duì)象需要遵守一些固定規(guī)則,而且緊密關(guān)聯(lián)的各組對(duì)象也要遵守一些固定規(guī)則。然而,過于謹(jǐn)慎的鎖定機(jī)制又會(huì)導(dǎo)致多個(gè)用戶之間毫無意義地互相干擾,從而使系統(tǒng)不可用。引用自《領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)》P82。

?

在上面的模型中,Engine被視為Car聚合內(nèi)的一個(gè)實(shí)體,這就意味著要對(duì)Engine做修改必須先擁有Car所有權(quán)?,F(xiàn)在我們遇到一個(gè)需求:發(fā)動(dòng)機(jī)制造商突然發(fā)現(xiàn)其交付的產(chǎn)品存有安全隱患,需要跟蹤運(yùn)行效果以及通過網(wǎng)絡(luò)進(jìn)行補(bǔ)丁安裝。

(1)如何解決爭(zhēng)用問題?

Car對(duì)象自身對(duì)Engine存有一些寫的邏輯,比如更新發(fā)動(dòng)機(jī)的使用情況;發(fā)動(dòng)機(jī)制造商也要對(duì)Engine做一些升級(jí)。這里面可能有一些業(yè)務(wù)限制,比如發(fā)動(dòng)機(jī)升級(jí)期間不提供對(duì)外服務(wù),這里面為了規(guī)避并發(fā)可能要進(jìn)行一些加鎖操作,這就會(huì)導(dǎo)致性能問題。

(2)如何解決效率問題?

制造商不能直接獲取到Engine對(duì)象,因?yàn)閷?duì)外部而言擁有Car實(shí)例才能有渠道去獲得Engine實(shí)例。這就導(dǎo)致了效率問題,因?yàn)橹圃焐滩坏靡阎荒苋ケ闅v所有Car實(shí)體。

因此我們考慮把發(fā)動(dòng)機(jī)作為一個(gè)單獨(dú)的業(yè)務(wù)域,Car聚合里面只需要記錄EngineId。無論是發(fā)動(dòng)機(jī)的運(yùn)行數(shù)據(jù)或者發(fā)動(dòng)機(jī)的監(jiān)控、升級(jí)等操作,都由發(fā)動(dòng)機(jī)自己負(fù)責(zé)。同時(shí)因?yàn)镃ar聚合記錄了EngineId,必要的情況下我們可以方便的從EngineRepository中獲得Engine對(duì)象,這也算是做到了懶加載??梢韵胂?#xff0c;系統(tǒng)中假如存在千萬級(jí)別的Car實(shí)例,按照最初的方案就會(huì)有千萬級(jí)別的Engine對(duì)象,但是我相信并不是每一次對(duì)Car實(shí)例的調(diào)用都需要獲取其Engine信息,這就造成了大量的內(nèi)存消耗。相對(duì)于最初的方案,我們的聚合或更小,也更靈活。

public class Car {private Customer customer;private Map<String, Wheel> wheels;//我們構(gòu)造單獨(dú)的Engine聚合。//此處只記錄EngineId,需要時(shí)候再去獲取實(shí)例。懶加載。//從實(shí)體轉(zhuǎn)為值對(duì)象private String engineId;//...... }

在聚合中,如果你認(rèn)為有些被包含的部分應(yīng)該建模成一個(gè)實(shí)體,此時(shí)你該怎么辦呢?首先,思考一下,這個(gè)部分是否會(huì)隨著時(shí)間而改變,或者該部分是否能被全部替換。如果可以全部替換,那么請(qǐng)將其建模成值對(duì)象,而非實(shí)體。有時(shí),建模成實(shí)體也是有必要的。但是很多情況下,許多建模成實(shí)體的概念都可以重構(gòu)成值對(duì)象。聚合的內(nèi)部建模成值對(duì)象有很多好處的。根據(jù)你所選用的持久化機(jī)制,值對(duì)象可以隨著根實(shí)體而序列化,比如我們可以把EngineId和Car一起存放;而實(shí)體則需要單獨(dú)的存儲(chǔ)區(qū)域予以跟蹤,此外實(shí)體還會(huì)帶來某些不必要的操作,比如我們需要對(duì)多張表進(jìn)行聯(lián)合查詢。但是對(duì)單張表進(jìn)行讀取要快得多,而使用值對(duì)象也更加方便與安全。再者由于值對(duì)象是不變的,測(cè)試起來也相對(duì)簡(jiǎn)單。

在實(shí)際項(xiàng)目中,即使沒有并發(fā)鎖、沒有大事務(wù),我們依然還會(huì)遇到寫操作性能問題。Car被廢棄處理以后,我們可能不僅僅是更新對(duì)應(yīng)數(shù)據(jù)庫記錄信息。我們還需要在車輛登記機(jī)構(gòu)進(jìn)行銷戶操作;對(duì)應(yīng)的車輪、發(fā)動(dòng)機(jī)相關(guān)的數(shù)據(jù)記錄如何處理等等。如果你指望一個(gè)方法體里面處理完這些邏輯,我敢保證你的代碼響應(yīng)時(shí)間會(huì)非常之久,甚至導(dǎo)致“汽車報(bào)廢”業(yè)務(wù)不可用。因此我們要去思考這個(gè)過程,哪些是核心邏輯,哪些允許一定的時(shí)延,對(duì)復(fù)雜的邏輯進(jìn)行異步處理。比如:我們發(fā)布CarAbandonedEvent進(jìn)而由相應(yīng)的handler去處理后續(xù)的業(yè)務(wù)規(guī)則。

3、值對(duì)象-無副作用

值對(duì)象的方法應(yīng)該被設(shè)計(jì)成一個(gè)無副作用函數(shù),即只用于生成輸出而不會(huì)修改對(duì)象的狀態(tài)。對(duì)于不變的值對(duì)象而言,所有的方法都必須是無作用的函數(shù),因?yàn)樗鼈儾荒芷茐闹祵?duì)象的屬性值才能安全的被共享。我們要意識(shí)到值對(duì)象絕不僅僅是一個(gè)屬性容器,其真正的強(qiáng)大特性“無副作用函數(shù)”。比如上面的窗戶對(duì)象,當(dāng)其被實(shí)例化出來以后各個(gè)屬性就不能被肆意修改了,我們通用的做法是在構(gòu)造方法里面進(jìn)行賦值或者基于工廠方法獲得,總之千萬拒絕提供public的set方法,因?yàn)槟悴恢滥膫€(gè)小伙伴在你不知情的情況setBomb。當(dāng)管理窗戶的附屬資源系統(tǒng)進(jìn)行升級(jí),可能導(dǎo)致某低版本的窗戶對(duì)象不可用時(shí)候只需要對(duì)系統(tǒng)發(fā)送一個(gè)WindowsUpgradedEvent,進(jìn)而由各個(gè)業(yè)務(wù)方去檢查是否替換使用新的窗戶對(duì)象。

一個(gè)值對(duì)象允許對(duì)傳入的實(shí)體對(duì)象進(jìn)行修改嗎?如果值對(duì)象中的確有方法會(huì)修改實(shí)體對(duì)象,那么該方法還是無副作用的嗎?該方法容易測(cè)試嗎?因此,如果一個(gè)值對(duì)象方法將一個(gè)實(shí)體對(duì)象作為參數(shù)時(shí),最好的方式是,讓實(shí)體對(duì)象使用該方法的返回結(jié)果來修改其自身的狀態(tài)。

比如某車輛養(yǎng)護(hù)機(jī)構(gòu)提供噴繪功能,用戶基于三原色自由組合自己喜愛的顏料。我們定義了Paint對(duì)象,其顏色由red、yellow、blue構(gòu)成。在這里“顏色”是一個(gè)非常重要的概念。你可以想象某種網(wǎng)紅流行顏色必然會(huì)被大家追捧,在這段期間頻繁地被系統(tǒng)創(chuàng)建出來。通過前面的論述,我們?cè)囍@示定義PigmentColor專門用于三原色的管理。其本身也會(huì)作為一個(gè)值對(duì)象被Paint使用。

public class Paint {private PigmentColor pigmentColor;private Double volume;//一定量的顏料A可以與其他顏料混合配比使用,那么我們可能定義一個(gè)mixedWith方法//還有一個(gè)疑問就是混合后的Paint對(duì)象到底是不是原來的?public void mixedWith(Paint anotherPaint){//1.add volume//2.顏料混合//3.then, but...who am I} }

把PigmentColor分離出來之后,確實(shí)比先前表達(dá)了更多信息,但混合計(jì)算的邏輯該怎么實(shí)現(xiàn)也是一個(gè)頭疼的事情。當(dāng)把顏色數(shù)據(jù)移出來后,與這些數(shù)據(jù)有關(guān)的行為也應(yīng)該一起移出來。但是在做這件事之前,要注意PigmentColor是一個(gè)值對(duì)象,因此應(yīng)該是不可變的。當(dāng)我們混合調(diào)配時(shí),Paint對(duì)象本身被改變了,它是一個(gè)具有生命周期的實(shí)體。相反,表示基個(gè)色調(diào)(棕色、黑色、白色)的PigmentColor則一直表示那種顏色。Paint的結(jié)果是產(chǎn)生一個(gè)新的PigmentColor對(duì)象,用于表示新的顏色。

public class PigmentColor {//mixedwith作為值對(duì)象的無副作用方法,返回一個(gè)新的對(duì)象由調(diào)用方?jīng)Q定是否使用。public PigmentColor mixedwith(PigmentColor otherPigment, Double ratio) {//混合的邏輯return 新的PigmentColor對(duì)象;} }/** * * 如果一個(gè)操作把邏輯或計(jì)算與狀態(tài)改變混合在一起,那么我們 * 就應(yīng)該把這個(gè)操作重構(gòu)為兩個(gè)獨(dú)立的操作。 * 邏輯計(jì)算可以視為命令,我們對(duì)于結(jié)果的獲取視為查詢。這也 * 符合命令查詢分離的原則。 */ public class Paint {public void mixedwith(Paint other) {this.volume += other.getVolume();Double ratio = other.getVolume() / this.volume;//用新返回的顏料對(duì)象替換當(dāng)前的顏料對(duì)象,//通過可以替換的值對(duì)象維護(hù)Paint實(shí)體的完整性。this.pigmentColor =this.pigmentColor.mixedwith(other.getPigmentColor(), ratio);} }

4、聚合的構(gòu)造與保存

當(dāng)創(chuàng)建一個(gè)對(duì)象或創(chuàng)建整個(gè)AGGREGATE時(shí),如果創(chuàng)建工作很復(fù)雜,或者暴露了過多的內(nèi)部結(jié)構(gòu),則可以使用FACTORY進(jìn)行封裝。就好比我們不可能讓調(diào)用方來構(gòu)造我們的Car聚合,因?yàn)檎{(diào)用方并不知道我們WheelPositionEnum與Wheel的映射關(guān)系,不知道如何去構(gòu)造Wheel信息。復(fù)雜的對(duì)象創(chuàng)建是領(lǐng)域?qū)拥穆氊?zé),無論是實(shí)體、值對(duì)象,其創(chuàng)建過程本身就是一個(gè)主要操作,有時(shí)候被創(chuàng)建的對(duì)象自身并不適合承擔(dān)復(fù)雜的裝配操作。將這些職責(zé)混在一起可能產(chǎn)生難以理解的拙劣設(shè)計(jì),好比我們的Car必然不是自己生產(chǎn)出來的,而是產(chǎn)自于某個(gè)“工廠”。

我們應(yīng)該將創(chuàng)建復(fù)雜對(duì)象的實(shí)例和AGGREGATE的職責(zé)轉(zhuǎn)移給單獨(dú)的對(duì)象,提供一個(gè)封裝所有復(fù)雜裝配操作的接口。在創(chuàng)建AGGREGATE時(shí)要把它作為一個(gè)整體,并確保它滿足固定規(guī)則。我們可以視其為“工廠FACTORY”。FACTORY有很多種設(shè)計(jì)方式,包括FACTORY METHOD(工廠方法)、ABSTRACT FACTORY(抽象工廠)和BUILDER(構(gòu)建器)。

這里要強(qiáng)調(diào)的是,BUILDER(構(gòu)建器)也是我們常用的一種工廠方法。我們可以對(duì)Car聚合設(shè)計(jì)一個(gè)工廠方法buildWheels,其接受必須要的參數(shù)進(jìn)而轉(zhuǎn)換為滿足業(yè)務(wù)規(guī)則的映射關(guān)系。這里面更重要的是業(yè)務(wù)約束的檢查,每個(gè)創(chuàng)建方法都是原子的,而且要保證被創(chuàng)建對(duì)象或AGGREGATE的所有固定規(guī)則。在生成ENTITY時(shí),這意味著創(chuàng)建滿足所有固定規(guī)則的整個(gè)AGGREGATE,但在創(chuàng)建完成后可以向聚合添加可選元素。在創(chuàng)建不變的VALUE OBJECT時(shí),這意味著所有屬性必須被初始化為正確的最終狀態(tài)。如果FACTORY通過其接口收到了一個(gè)創(chuàng)建對(duì)象的請(qǐng)求,而它又無法正確地創(chuàng)建出這個(gè)對(duì)象,那么它應(yīng)該拋出一個(gè)異常,或者采用其他機(jī)制,以確保不會(huì)返回錯(cuò)誤的值。

很多場(chǎng)景中,聚合被創(chuàng)建出來以后其生命周期會(huì)持續(xù)一段時(shí)間。我們?cè)谏院蟮拇a里面仍舊需要使用,考慮到復(fù)雜聚合的生成過程比較繁瑣,所以我們有必要找到一個(gè)地方將這些還需要使用的聚合“暫存”起來。否則我們就需要時(shí)刻把這些聚合當(dāng)做參數(shù)進(jìn)行傳遞。為每種需要全局訪問的對(duì)象類型創(chuàng)建一個(gè)“容器”即REPOSITORY,并通過一個(gè)眾所周知的全局接口來提供訪問。提供添加和刪除對(duì)象的方法,用這些方法來封裝在數(shù)據(jù)存儲(chǔ)中實(shí)際插入或刪除數(shù)據(jù)的操作。提供根據(jù)具體條件來挑選對(duì)象的方法,并返回屬性值滿足查詢條件的對(duì)象或?qū)ο蠹?#xff0c;從而將實(shí)際的存儲(chǔ)和查詢技術(shù)封裝起來。只為那些確實(shí)需要直接訪問的AGGREGATE根提供REPOSITORY。讓客戶始終聚焦于模型,而將所有對(duì)象的存儲(chǔ)和訪問操作交給REPOSITORY來完成。

5、展示聚合

首先我們應(yīng)該明確DDD里面有清晰嚴(yán)格的“層”概念,通常情況下展示層需要的信息會(huì)分散在多個(gè)聚合里面,但是每個(gè)聚合里面也有一些本次展現(xiàn)所不需要的信息;而每一個(gè)聚合可能又是有幾個(gè)數(shù)據(jù)庫實(shí)體記錄構(gòu)成的。這就導(dǎo)致了一個(gè)展示對(duì)象涉及了多次數(shù)據(jù)庫查詢且存在多次數(shù)據(jù)對(duì)象的轉(zhuǎn)換。這也許會(huì)成為你的吐槽點(diǎn)。

但可能有些讀者會(huì)選擇直接在數(shù)據(jù)結(jié)構(gòu)中使用業(yè)務(wù)實(shí)體對(duì)象(即在展示層、數(shù)據(jù)庫設(shè)計(jì)時(shí)候也使用領(lǐng)域?qū)泳酆?#xff09;。畢竟,業(yè)務(wù)實(shí)體與請(qǐng)求/響應(yīng)模型之間有很多相同的數(shù)據(jù)。但請(qǐng)一定不要這樣做!這兩個(gè)對(duì)象存在的意義是非常不一樣的。隨著時(shí)間的推移,這兩個(gè)對(duì)象會(huì)以不同的原因、不同的速率發(fā)生變更。所以將它們以任何方式整合在一起都是對(duì)共同閉包原則(CCP)和單一職責(zé)原則(SRP)的違反??傆幸惶?#xff0c;當(dāng)你想要重新設(shè)計(jì)底層存儲(chǔ)時(shí)候會(huì)導(dǎo)致展示層的問題;或者迫于展示層的需求去修改底層的表結(jié)構(gòu)。

針對(duì)一開始的吐槽,我們可以借助懶加載去避免不必要的查詢以及轉(zhuǎn)換;還可以把一些常用的數(shù)據(jù)緩存起來。但如果使用redis一類的內(nèi)存數(shù)據(jù)庫時(shí)候,要考慮對(duì)象的序列化消耗。因?yàn)槿绻岩粋€(gè)層級(jí)較深、比較復(fù)雜的大聚合緩存在redis中,在高頻讀取的情況下序列化也會(huì)令你抓狂。在這樣的情況下,我們可能需要重新設(shè)計(jì)緩存結(jié)構(gòu),盡可能接近于viewObj.setAttribute(redis.getXXX())。很大程度上,對(duì)象之間的轉(zhuǎn)換可能不能完全避免,所以我們要綜合考慮以上幾種因素去權(quán)衡實(shí)踐。

6、不要拋棄領(lǐng)域服務(wù)

很多人認(rèn)為DDD中的聚合就是在與貧血模型做抗?fàn)?#xff0c;所以在領(lǐng)域?qū)邮遣荒艹霈F(xiàn)“service”的,這等于是破壞了聚合的操作性。但有些重要的領(lǐng)域操作無法放到實(shí)體或值對(duì)象中,這當(dāng)中有些操作從本質(zhì)上講是一些活動(dòng)或動(dòng)作,而不是對(duì)象。比如我們的身份認(rèn)證、支付轉(zhuǎn)賬業(yè)務(wù),我們很難去抽象一個(gè)金融對(duì)象去協(xié)調(diào)轉(zhuǎn)賬、收付款等業(yè)務(wù)邏輯;有時(shí)候我們也不太可能讓對(duì)象自己執(zhí)行auth邏輯。因?yàn)檫@些操作從概念上來講不屬于任何業(yè)務(wù)對(duì)象,所以我們考慮將其實(shí)現(xiàn)成一個(gè)service,然后注入到業(yè)務(wù)領(lǐng)域或者說是業(yè)務(wù)域委托這些service去實(shí)現(xiàn)某些功能。

//AuthenticationService注冊(cè)到了DomainRegistry UserDescriptor userDescriptor = DomainRegistry.authenticationService().authenticate(userId, password);

以上方式是簡(jiǎn)單的,也是優(yōu)雅的??蛻舳酥恍?/p>

要獲取到一個(gè)無狀態(tài)的AuthenticationService,然后調(diào)用它的authenticate()方法即可。這種方式將所有的認(rèn)證細(xì)節(jié)放在領(lǐng)域服務(wù)中,而不是應(yīng)用服務(wù)。在需要的情況下,領(lǐng)域服務(wù) 可以使用在何領(lǐng)域?qū)ο髞硗瓿刹僮?#xff0c;包括對(duì)密碼的加密過程??蛻舳瞬恍枰廊魏握J(rèn)證細(xì)節(jié)。此時(shí),通用語言也得到了滿足,因?yàn)槲覀儗⑺械念I(lǐng)域術(shù)語都放在了身份管理這個(gè)領(lǐng)域中,而不是一部分放在領(lǐng)域模型中,另一部分 放在客戶端中。

AuthenticationService和那些與用戶身份相關(guān)的業(yè)務(wù)定義在相同的package中,但對(duì)于該接口的實(shí)現(xiàn)類,我們可以選擇性地將其存放在不同的地方。如果你正使用依賴倒置原則或六邊形架構(gòu),那么你可能會(huì)將這個(gè)多少有些技術(shù)性的實(shí)現(xiàn)類放置在領(lǐng)域模型之外的某個(gè)設(shè)施層。

那么我們來總結(jié)一下,以下幾種情況我們可以使用領(lǐng)域服務(wù)來實(shí)現(xiàn):

  • 執(zhí)行一個(gè)顯著的業(yè)務(wù)操作過程;

  • 對(duì)領(lǐng)域?qū)ο筮M(jìn)行轉(zhuǎn)換;

  • 以多個(gè)領(lǐng)域?qū)ο笞鳛檩斎脒M(jìn)行計(jì)算,結(jié)果產(chǎn)生一個(gè)值對(duì)象;

7、再談命名

類以及函數(shù)的命名一直以來都是令人困惑的話題,根因在于它說起來很簡(jiǎn)單,但要做好確實(shí)太難了。試想一下如果開發(fā)人員為了使用一個(gè)組件而必須要去研究它的實(shí)現(xiàn),那么就失去了封裝的價(jià)值。當(dāng)某個(gè)人開發(fā)的對(duì)象或操作被別人使用時(shí),如果使用這個(gè)組件的新的開發(fā)者不得不根據(jù)其實(shí)現(xiàn)來推測(cè)其用途,那么他推測(cè)出來的可能并不是那個(gè)操作或類的主要用途。如果這不是那個(gè)組件的用途,雖然代碼暫時(shí)可以工作,但設(shè)計(jì)的概念基礎(chǔ)已經(jīng)被誤用了,兩位開發(fā)人員的意圖也是背道而馳。當(dāng)我們把概念顯式地建模為類或方法時(shí),為了真正從中獲取價(jià)值,必須為這些程序元素賦予一個(gè)能夠反映出其概念的名字。類和方法的名稱為開發(fā)人員之間的溝通創(chuàng)造了很好的機(jī)會(huì),也能夠改善系統(tǒng)的抽象。

因此在命名類和操作時(shí)要描述它們的效果和目的,而不要表露它們是通過何種方式達(dá)到目的的。這樣可以使客戶開發(fā)人員不必去理解內(nèi)部細(xì)節(jié)。在創(chuàng)建一個(gè)行為之前先為它編寫一個(gè)測(cè)試,這樣可以促使你站在客戶開發(fā)人員的角度上來思考它。測(cè)試驅(qū)動(dòng)的另一個(gè)價(jià)值就是要求我們寫出易于(測(cè)試)使用的代碼。試想一下,我們自己編寫測(cè)試都很困難的時(shí)候,別人又如何明白呢?

通常的所有復(fù)雜的機(jī)制都應(yīng)該封裝到抽象接口的后面, 接口只表明意圖,而不表明方式。在領(lǐng)域的公共接口中,可以把關(guān)系和規(guī)則表述出來,但不要說明規(guī)則是如何實(shí)施的;可以把事件和動(dòng)作描述出來,但不要描述它們是如何執(zhí)行的。

8、領(lǐng)域核心能力

當(dāng)我們對(duì)現(xiàn)實(shí)領(lǐng)域進(jìn)行思考時(shí)候,很容易被“表象”所迷惑。比如我們的Car聚合內(nèi)部會(huì)有一個(gè)導(dǎo)航服務(wù),一般情況我們可能需要按照最短路徑導(dǎo)航、躲避擁堵、高速優(yōu)先等情況。通過前面的學(xué)習(xí),我們抽象一個(gè)“導(dǎo)航”服務(wù)并將其注入或者注冊(cè)到Car聚合。

隨著導(dǎo)航要求的多樣化,不可避免的該類會(huì)變得臃腫繼而難以維護(hù)。因此我們借助策略模式,抽象一個(gè)導(dǎo)航策略,一切問題都變得更加清晰。

如上圖所示設(shè)計(jì),我們得到了清晰明確的導(dǎo)航模型以及一個(gè)被明確提煉出來的導(dǎo)航策略。無論我們導(dǎo)航需求如何變化,我們只需要去增加實(shí)現(xiàn)類即可,這就是我們架構(gòu)原則所提倡的對(duì)擴(kuò)展開放。這雖然是一個(gè)很小的例子,但是其背后的意義重大,讓我們學(xué)會(huì)區(qū)分什么是行為、什么是策略。因?yàn)樾袨槭枪潭ǖ?#xff0c;策略是變化的。當(dāng)我們將二者區(qū)分以后,就能更加聚焦于領(lǐng)域的核心行為能力。

四、聚合與六邊形架構(gòu)

在之前的系列文章中,我多次提到了六邊形架構(gòu)。但更多的是理念上的解釋,現(xiàn)在講解了聚合以后我們就來看看六邊形架構(gòu)的代碼風(fēng)格是什么樣的,其端口到底為何物。還是參照之前的做法,在一個(gè)DDD沒有完全普及的項(xiàng)目中,我們依然提供一個(gè)CarFacade供外部調(diào)用,以免花費(fèi)很長時(shí)間去和他們爭(zhēng)論到底該不該建模一個(gè)充血的Car對(duì)象。

//通過RPC調(diào)用得到Car聚合信息,進(jìn)而轉(zhuǎn)換成前端展示所需要的ViewObject CarData carData = carFacade.OfId(carId); CarVO carVO = CarVOFactory.build(carData.getValue());

通常應(yīng)用服務(wù)被設(shè)計(jì)成了具有輸入和輸出的API,而傳入數(shù)據(jù)轉(zhuǎn)換器的目的即在于為客戶端生成特定的輸出類型。在六邊形架構(gòu)中我們可能會(huì)使得服務(wù)返回void類型,數(shù)據(jù)隱式的在端口流轉(zhuǎn)。通過這一點(diǎn),我們可以看出六邊形架構(gòu)更強(qiáng)調(diào)數(shù)據(jù)流轉(zhuǎn)而不像傳統(tǒng)開發(fā)方式那樣注重?cái)?shù)據(jù)的返回或加工。

public class CarFacadeImpl {public void OfId(Long carId){//領(lǐng)域?qū)舆壿婥ar car = this.carRepository.OfId(carId);//應(yīng)用層邏輯//這里的輸出端口是一個(gè)位于位于應(yīng)用程序的邊緣特殊的端口;//在使用Spring時(shí),該端口類可以被注入到應(yīng)用服務(wù)中;//在本例中其職責(zé)是把Car聚合轉(zhuǎn)換成前端展示所需要的ViewObject;//如果我們使用SpringMVC一類的框架,該端口還負(fù)責(zé)把數(shù)據(jù)返回給HttpResponse;this.carHttpOutputPort().write(car);} }

當(dāng)然我們可能會(huì)有多個(gè)輸出端口,而各個(gè)端口的隔離實(shí)現(xiàn)又避免了邏輯的污染,為將來任意擴(kuò)展端口場(chǎng)景提供了可能性。在write()方法執(zhí)行后,每一個(gè)注冊(cè)的讀取器都會(huì)將端口的輸出作為自己的輸入。這里最大的問題就是,不了解六邊形架構(gòu)的人會(huì)抱怨“你的getXXX方法竟然沒有返回值”。所以我們?cè)诜椒麜r(shí)候盡可能避免使用get字樣,通常我會(huì)取而代之find/load,因?yàn)椴檎?裝載并不隱含需要返回結(jié)果的意思。無論如何我們都要明白,任何一種架構(gòu)都同時(shí)存在正面的和負(fù)面的影響。

五、演進(jìn)的聚合

提到“重構(gòu)”,我們頭腦中就會(huì)出現(xiàn)這樣一幅場(chǎng)景:幾位開發(fā)人員坐在鍵盤前面,發(fā)現(xiàn)一些代碼可以改進(jìn),然后立即動(dòng)手修改代碼(當(dāng)然還要用單元測(cè)試來驗(yàn)證結(jié)果)。當(dāng)然這個(gè)過程應(yīng)該一直進(jìn)行下去,但它并不是重構(gòu)過程的全部。與傳統(tǒng)重構(gòu)觀點(diǎn)不同的是,即使在代碼看上去很整潔的時(shí)候也可能需要重構(gòu),原因是模型是否與真實(shí)的業(yè)務(wù)一致,或者現(xiàn)有模型導(dǎo)致新需求不能被自然的實(shí)現(xiàn)完成。重構(gòu)的原因也可能來自學(xué)習(xí):當(dāng)開發(fā)人員通過學(xué)習(xí)獲得了更深刻的理解,從而發(fā)現(xiàn)了一個(gè)得到更清晰或更有用的模型。綜合起來以下幾點(diǎn)的出現(xiàn)就說明你應(yīng)該重新審視你的聚合了,當(dāng)然我們重構(gòu)也好、演進(jìn)也罷,也還是要基于實(shí)際項(xiàng)目的情況。

  • 設(shè)計(jì)沒有表達(dá)出團(tuán)隊(duì)對(duì)領(lǐng)域的最新理解;

  • 重要的概念被隱藏在設(shè)計(jì)中了(而且你已經(jīng)發(fā)現(xiàn)了把它們呈現(xiàn)出來的方法);

  • 發(fā)現(xiàn)了一個(gè)能令某個(gè)重要的設(shè)計(jì)部分變得更靈活的機(jī)會(huì);

最后還是延續(xù)前面文章的一貫風(fēng)格,本文講述了很多有關(guān)聚合的細(xì)節(jié),即使在非DDD的項(xiàng)目中,這些有效實(shí)踐依然大有裨益。我們希望設(shè)計(jì)的聚合具有柔性特征,但這往往很難。能夠清楚地表明它的意圖;使人們很容易看出代碼的運(yùn)行效果,因此也很容易預(yù)計(jì)修改代碼的結(jié)果。柔性設(shè)計(jì)主要通過減少依賴性和副作用來減輕人們的思考負(fù)擔(dān)。這樣的設(shè)計(jì)是以深層次的領(lǐng)域模型為基礎(chǔ)的,在模型中,只有那些對(duì)用戶最重要的部分才具有較細(xì)的粒度。在這樣的模型中,那些經(jīng)常需要修改的地方能夠保持很高的靈活性,而其他地方則相對(duì)比較簡(jiǎn)單。這也就是我一再強(qiáng)調(diào)的“行為”“策略”的區(qū)別。當(dāng)我們這樣去思考問題以后,編碼以及設(shè)計(jì)思路會(huì)有很大變化,從原來那樣的流程代碼中脫離出來,進(jìn)而站在一個(gè)更高的抽象層次上去實(shí)現(xiàn)系統(tǒng)。

(圖片來源于網(wǎng)絡(luò))

參考文獻(xiàn):

  • 《領(lǐng)域驅(qū)動(dòng)設(shè)計(jì):軟件核心復(fù)雜性應(yīng)對(duì)之道》

  • 《實(shí)現(xiàn)領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)》

  • 更多內(nèi)容敬請(qǐng)關(guān)注?vivo 互聯(lián)網(wǎng)技術(shù)?微信公眾號(hào)

    注:轉(zhuǎn)載文章請(qǐng)先與微信號(hào):Labs2020?聯(lián)系

    總結(jié)

    以上是生活随笔為你收集整理的领域驱动设计(DDD)实践之路(三):如何设计聚合的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

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