軟件系統(tǒng)是通過軟件開發(fā)來解決某一個業(yè)務(wù)領(lǐng)域或問題單元而產(chǎn)生的一個交付物。而通過軟件設(shè)計可以幫助我們開發(fā)出更加健壯的軟件系統(tǒng)。因此,軟件設(shè)計是從業(yè)務(wù)領(lǐng)域到軟件開發(fā)之間的橋梁。而DDD是軟件設(shè)計中的其中一種思想,旨在提供一種大型復(fù)雜軟件的設(shè)計思路和規(guī)范。通過DDD思想可以讓我們的業(yè)務(wù)架構(gòu)、系統(tǒng)架構(gòu)、部署架構(gòu)、數(shù)據(jù)架構(gòu)、工程架構(gòu)等都具備高擴展性、高維護性和高測試性。
但是落地DDD是一件很困難的事情。首先在思想認知層面就比較難以突破。
DDD本身是一種思想,不是某種具體的技術(shù),因此在代碼實現(xiàn)和系統(tǒng)架構(gòu)層面沒有約束。而由于市面上成熟的ORM框架(比如hibernate、mybatis等),使得大部分軟件開發(fā)都是直接面向數(shù)據(jù)庫開發(fā)。在傳統(tǒng)開發(fā)中的應(yīng)用分層架構(gòu)又和DDD思想的分層架構(gòu)很類似。從而導(dǎo)致很多人在初學DDD時有一定的理解偏差,從而導(dǎo)致無法落地DDD思想。
這篇文章記錄我對DDD的學習、感悟與項目工程代碼重構(gòu)實戰(zhàn)心得!
一、Domin Primitive
領(lǐng)域“元數(shù)據(jù)”的意思。主要是講解領(lǐng)域的基本準則。這也是使用DDD思想的基本準則。
1.1 隱性的概念顯性化
exp:電話號碼通常是由區(qū)號編碼+號碼組成。在實際的業(yè)務(wù)中會有很多需要電話號碼的業(yè)務(wù)。比如登錄認證、導(dǎo)購分銷等業(yè)務(wù);我們需要對電話號碼進行基礎(chǔ)性校驗;獲取區(qū)號編碼等;在常規(guī)操作下,會在每一個用到電話號碼的方法入口都會寫大量的這種校驗代碼和判斷代碼,盡管我們可以將它的校驗和獲取區(qū)號編碼抽離成util類(實際上大多數(shù)工程中都是這么做的),但這種方式治標不治本?;贒DD思想可以發(fā)現(xiàn)這里有一個隱性概念:區(qū)號編碼。
我們可以基于DDD思想,將電話號碼創(chuàng)建為一個擁有獨立概念和行為的值對象:PhoneNumber,將基礎(chǔ)性校驗和獲取編碼等無狀態(tài)行為封裝在值對象中。這樣在方法中就不需要再充斥著寫大量的校驗和判斷。
1.2 隱性的上下文顯性化
exp:在銀行轉(zhuǎn)賬場景中,通常我們會說A賬戶給B賬戶轉(zhuǎn)1000元。這里的1000元實際上有兩層含義,數(shù)字1000,貨幣元。但我們通常會忽略貨幣單位元。導(dǎo)致在實現(xiàn)轉(zhuǎn)賬功能時,沒有考慮到單位。一旦有國際轉(zhuǎn)賬時,就又會陷入到大量的if else中。
我們基于DDD思想,將錢創(chuàng)建為一個擁有獨立概念和行為的值對象:Money,這樣我們所說的錢才具備完整的概念。通過這種方式就可以將貨幣這個隱性上下文顯性化,從而避免當前未識別到但是未來可能會爆雷的bug。
1.3 封裝多對象行為
exp:在跨境轉(zhuǎn)賬的場景中,需要轉(zhuǎn)換匯率,我們可以將轉(zhuǎn)換匯率封裝成一個值對象。通過封裝金額計算邏輯和各種校驗邏輯,使得整個方法極其簡單。
1.4 DP和值對象的區(qū)別
DP是阿里大神提出來的概念;值對象是DDD思想中的概念。
學習之后,我個人認為DP是對值對象的進一步補充,使其擁有了更加完整的概念。在值對象【不變性】的基礎(chǔ)上補充了【可校驗性】和【獨立行為】。當然也是要求【無副作用】。所謂的無副作用就是【狀態(tài)不可變】。
1.5 DP和DTO的區(qū)別
DTO | DP | |
---|---|---|
功能 | 數(shù)據(jù)傳輸對象,屬于技術(shù)細節(jié) | 屬于領(lǐng)域中的業(yè)務(wù)概念 |
數(shù)據(jù)關(guān)聯(lián)性 | 不具備數(shù)據(jù)關(guān)聯(lián)性 | 數(shù)據(jù)之間有強關(guān)聯(lián)性 |
行為 | 無行為 | 擁有十分豐富的行為和業(yè)務(wù)邏輯 |
1.6 使用DP VS 不使用DP
不使用DP | 使用DP | |
---|---|---|
API接口清晰度 | 含混不清 | 方法簽名清晰易懂 |
數(shù)據(jù)校驗、錯誤處理 | 校驗邏輯分布多個地方、大量重復(fù)代碼 | 校驗邏輯內(nèi)聚,在方法邊界外完成 |
業(yè)務(wù)代碼的清晰度 | 充斥大量膠水代碼,淹沒業(yè)務(wù)核心邏輯 | 代碼簡潔明了,業(yè)務(wù)邏輯一目了然 |
測試復(fù)雜度 | TC數(shù)量:NMP(N個參數(shù),每個參數(shù)M種校驗,有P個方法在調(diào)用) | TC數(shù)量:N+M+P |
其他好處 | 整體安全性大大提升、不可變性、線程安全 |
二、應(yīng)用架構(gòu)
2.1 DDD思想下的標準應(yīng)用架構(gòu)
傳統(tǒng)的MVC架構(gòu)分為展現(xiàn)層、業(yè)務(wù)邏輯層和數(shù)據(jù)訪問層,更加注重從展現(xiàn)層到數(shù)據(jù)訪問層自上而下的交互,編寫出來的代碼像是腳本式代碼。
而基于DDD原則,工程架構(gòu)被分為應(yīng)用層、領(lǐng)域?qū)雍突A(chǔ)設(shè)施層。將工程中不同的功能和職責劃分到不同的層級中。核心的業(yè)務(wù)邏輯放在領(lǐng)域?qū)又小?/p>
2.1.1 應(yīng)用層
按照DDD的思想,應(yīng)用層負責協(xié)調(diào)用戶界面和領(lǐng)域?qū)又g的交互??梢酝ㄋ椎恼J為是對領(lǐng)域服務(wù)的編排,其本身不包含任何業(yè)務(wù)邏輯。
2.1.2 領(lǐng)域?qū)?/p>
領(lǐng)域?qū)迂撠煂崿F(xiàn)核心業(yè)務(wù)的邏輯和規(guī)則。按照DDD的思想,這一層包含實體模塊、值對象模塊、事件、領(lǐng)域服務(wù)。
2.1.3 基礎(chǔ)設(shè)施層
基礎(chǔ)設(shè)施層不處理任何業(yè)務(wù)邏輯,只包含基礎(chǔ)設(shè)施,通常包含數(shù)據(jù)庫、定時任務(wù)、MQ、南向網(wǎng)關(guān)、北向網(wǎng)關(guān)等。
2.2 我對演進出六邊形架構(gòu)的理解
2.2.1 再談應(yīng)用層
在實際業(yè)務(wù)邏輯當中,除了用戶界面層之外,還有其他外部系統(tǒng)會調(diào)用本服務(wù),比如xxljob、MQ、或者提供給外部系統(tǒng)調(diào)用http或者rpc接口等。因此在實際當中,應(yīng)用層應(yīng)當是協(xié)調(diào)外部系統(tǒng)與領(lǐng)域?qū)又g的交互。
按照標準架構(gòu)層級依賴關(guān)系來看,應(yīng)用層依賴了領(lǐng)域?qū)雍突A(chǔ)設(shè)施層。由于依賴了基礎(chǔ)設(shè)施層,因此破壞了應(yīng)用層本身的可維護性和測試性。因此我們需要基于接口進行依賴倒置。
為了防止領(lǐng)域概念外泄,需要對應(yīng)用層進一步的抽象為外部服務(wù)和內(nèi)部服務(wù),所有外部服務(wù)必須通過內(nèi)部服務(wù)調(diào)用領(lǐng)域?qū)?。這樣就可以防止領(lǐng)域模型的外泄。
2.2.2 再談領(lǐng)域?qū)?/p>
同樣的,按照標準架構(gòu)層級依賴關(guān)系,領(lǐng)域?qū)右蕾嚮A(chǔ)設(shè)施層,但這也破壞了領(lǐng)域?qū)颖旧淼目删S護性和可測試性。因此我們基于DDD中的資源庫思想,抽象repository層,通過接口實現(xiàn)依賴反轉(zhuǎn)。讓領(lǐng)域?qū)硬辉僖蕾嚮A(chǔ)設(shè)施層。從而提高領(lǐng)域?qū)颖旧淼目删S護性和可測試性。
2.2.3 再談基礎(chǔ)設(shè)施層
對于基礎(chǔ)設(shè)施層而言,它主要作用是提供基礎(chǔ)設(shè)施的能力,比如數(shù)據(jù)庫、MQ、遠程服務(wù)調(diào)用等。進一步抽象可以發(fā)現(xiàn)它們就是端口和適配器。通過端口實現(xiàn)與外部系統(tǒng)的交互,通過適配器完成數(shù)據(jù)和概念的轉(zhuǎn)換。
2.2.4 演進出六邊形架構(gòu)
通過依賴反轉(zhuǎn),神奇的事情發(fā)生了?;A(chǔ)設(shè)施層變成了最外層。
我們結(jié)合對應(yīng)用層、領(lǐng)域?qū)雍突A(chǔ)設(shè)施層進一步的理解再加上反轉(zhuǎn)后的應(yīng)用架構(gòu),便可以得到六邊形架構(gòu):
2.3 工具類、配置類的代碼應(yīng)該放在哪里?
在一個實際的工程當中,除了上面所說的三層之外,通常會使用到一些工具類(JSON解析工具類、字符串工具類等)。各層可能都會使用到工具類。
從工具類的定位來看,它應(yīng)當屬于基礎(chǔ)設(shè)施層,但是基礎(chǔ)設(shè)施層屬于最上層,如果放在基礎(chǔ)設(shè)施層,那么就會破壞依賴順序。因此我們在邏輯劃分上可以把工具類歸類為基礎(chǔ)設(shè)施或者通用域,在具體的工程結(jié)構(gòu)中,可以單獨一個模塊放工具類。
在實際工程中還有一種類型的代碼是配置相關(guān)的。從業(yè)務(wù)維度劃分的話可以分為業(yè)務(wù)類配置和基礎(chǔ)設(shè)施類配置。因此我們需要根據(jù)配置的類型將其放在對應(yīng)的位置。比如為了靈活應(yīng)對業(yè)務(wù),我們通常會配置一個動態(tài)開關(guān),來動態(tài)調(diào)整業(yè)務(wù)的邏輯,這種業(yè)務(wù)開關(guān)類的配置就應(yīng)該放在領(lǐng)域?qū)?;再比如?shù)據(jù)庫的配置屬于基礎(chǔ)設(shè)施配置,這類配置就應(yīng)當放在基礎(chǔ)設(shè)施層。
2.4 我對于項目的六邊形架構(gòu)的實踐
我們團隊做的的職責是業(yè)務(wù)底座,包含一系列的基礎(chǔ)能力建設(shè)。其中對于IDaaS系統(tǒng)而言,基于六邊形架構(gòu)實現(xiàn)出以下工程結(jié)構(gòu):
三、repository模式
3.1 什么是repository模式?
在DDD思想中,repository表示資源庫的概念,用于區(qū)分數(shù)據(jù)模型和領(lǐng)域模型。它操作的對象是聚合根,因此它屬于領(lǐng)域?qū)印?/p>
3.2 為什么要使用repository模式?
repository模式有兩個非常重要的作用:1、與底層存儲進行解偶;2、為解決貧血模型提供了一種規(guī)范。
3.3 什么是貧血模型?
由于過去ER模型以及主流ORM框架的發(fā)展,讓很多開發(fā)者對實體的概念還停留在與關(guān)系形數(shù)據(jù)庫映射這個層面。從而導(dǎo)致實體只有空洞的屬性,而實體的業(yè)務(wù)邏輯散落各個service、util、helper、handler等各種角落中。這種現(xiàn)象就被稱為貧血模型現(xiàn)象。
如何判斷自己的工程是否有貧血模型現(xiàn)象?
1、大量的XxxDO或者Xxx:實體對象只包含與數(shù)據(jù)庫表映射的屬性,沒有行為或者及其少量的行為;
2、業(yè)務(wù)邏輯在各種service、controller、util、helper、handler中:實體的業(yè)務(wù)邏輯散落在不同層級、不同類、不同方法中,相似場景有大量的重復(fù)代碼。
3.4 為什么貧血模型不好?
無法保證實體對象的完整性和一致性:貧血模型下,實體屬性的狀態(tài)和值只能由調(diào)用方保證,但是屬性的get和set是公開的,因此所有調(diào)用方都可以調(diào)用。所以無法保證對象的完整性和一致性。
操作實體對象的邊界很難發(fā)現(xiàn):由于對象只有屬性,屬性的邊界值、調(diào)用范圍不受實體自身控制,各個地方都可以調(diào)用,邊界值和范圍也只能由調(diào)用方自行保障。如果實體的邊界值有所變化,那么所有調(diào)用方都需要調(diào)整,這種情況下很容易導(dǎo)致bug的產(chǎn)生。
強依賴底層:貧血模型下的實體和數(shù)據(jù)庫模型映射、協(xié)議等。因此如果底層改變,那么上層邏輯需要全部跟著改變?!败浖弊兂闪恕肮碳?。
總結(jié)一句話:貧血模型下,軟件的可維護性、可擴展性、可測試性極差!
擴展: 軟件的可維護性=底層基礎(chǔ)設(shè)施變化時,需要新增/修改的代碼量是多少(越少可維護性越好) 軟件的可擴展性=新增或變更業(yè)務(wù)邏輯時,需要新增/修改的代碼量是多少(越少可擴展性越好) 軟件的可測試性=每條TC執(zhí)行的時長 * 新增或變更業(yè)務(wù)邏輯時產(chǎn)生的TC(時長越低/TC越少,測試性好)
3.5 實際情況中,為什么貧血模型難以消滅?
1、數(shù)據(jù)庫思維
隨著ER和ORM框架的發(fā)展,讓多數(shù)開發(fā)者在剛?cè)腴T的時候(自學、培訓(xùn)等方式),就認為實體就是數(shù)據(jù)庫表映射;從而簡單的將面向業(yè)務(wù)領(lǐng)域開發(fā)轉(zhuǎn)變成了面向數(shù)據(jù)庫開發(fā),漸漸地就認為軟件開發(fā)就是CRUD。
2、簡單
盡管有些架構(gòu)師或者開發(fā)人員知道貧血模型不好,但是企業(yè)為了占領(lǐng)市場,需要快速推出產(chǎn)品。因此工期被壓縮的很厲害。而貧血模型恰好簡單,在軟件初期階段,可以快速實現(xiàn)業(yè)務(wù)邏輯。從而迫使開發(fā)人員不得不“先實現(xiàn)了再說”。這種現(xiàn)象也是行業(yè)的普遍現(xiàn)象。
3、腳本思維
有些開發(fā)人員具備一定的抽象思維,將一些共性的代碼寫成util、helper、handler等類。但寫代碼依然是腳本思維。比如一個方法中,先來個字段校驗代碼,再來個對象轉(zhuǎn)換代碼,然后調(diào)用遠程服務(wù),對遠程服務(wù)返回的結(jié)果再來個對象轉(zhuǎn)換,……最后調(diào)用Dao類的方法保存對象。這種代碼在很多工程中太常見了。
基于這些因素,導(dǎo)致貧血模型難以消滅。
這些因素的根本原因是什么?
根本原因就是,大部分的開發(fā)人員混淆了數(shù)據(jù)模型和領(lǐng)域模型這兩個概念。
數(shù)據(jù)模型(Data Model):數(shù)據(jù)模型解決的是數(shù)據(jù)如何持久化、如何傳輸?shù)膯栴};
領(lǐng)域模型(Domin Model):領(lǐng)域指的是某一個獨立的業(yè)務(wù)領(lǐng)域或者問題空間,領(lǐng)域模型就是解決這個業(yè)務(wù)領(lǐng)域或者問題空間而設(shè)計的模型;解決的是業(yè)務(wù)領(lǐng)域的問題。
在DDD中,repository就是用于區(qū)分數(shù)據(jù)模型和領(lǐng)域模型提出來的概念。
3.6 使用repository之后,數(shù)據(jù)模型和領(lǐng)域模型如何轉(zhuǎn)換?
使用repository之后,數(shù)據(jù)模型和領(lǐng)域模型都各司其職。通過Assembler和Converter進行模型之間的轉(zhuǎn)換。
在代碼中,動態(tài)轉(zhuǎn)換映射 VS 靜態(tài)轉(zhuǎn)換映射
雖然Assembler/Converter是非常好用的對象,但是當業(yè)務(wù)復(fù)雜時,手寫Assembler/Converter是一件耗時且容易出bug的事情,所以業(yè)界會有多種Bean Mapping的解決方案,從本質(zhì)上分為動態(tài)和靜態(tài)映射。
動態(tài)映射方案包括比較原始的 BeanUtils.copyProperties、能通過xml配置的Dozer等,其核心是在運行時根據(jù)反射動態(tài)賦值。動態(tài)方案的缺陷在于大量的反射調(diào)用,性能比較差,內(nèi)存占用多,不適合特別高并發(fā)的應(yīng)用場景。而BeanUtils等copy類工具隱藏了內(nèi)部copy的過程,很容易引發(fā)bug且不易排查。
MapStruct通過注解,在編譯時靜態(tài)生成映射代碼,其最終編譯出來的代碼和手寫的代碼在性能上完全一致,且有強大的注解等能力。會節(jié)省大量的成本。
3.7 代碼層面模型規(guī)范和比較
DO | Entity | DTO | |
---|---|---|---|
命名規(guī)范 | XxxDO | Xxx | XxxDTO/XxxRequest/XxxVO/XxxCommand等 |
代碼層級 | 基礎(chǔ)設(shè)施層 | 領(lǐng)域?qū)?/td> | 應(yīng)用層 |
字段名稱標準 | 于數(shù)據(jù)庫字段保持一致 | 業(yè)務(wù)語言 | 和調(diào)用方商定 |
字段類型標準 | 和數(shù)據(jù)庫字段保持一致 | 根據(jù)業(yè)務(wù)特征確定事基礎(chǔ)類型還是值對象 | 和調(diào)用方商定 |
是否需要序列化 | 不需要 | 不需要 | 需要 |
轉(zhuǎn)換器 | Assembler | Assembler/Converter | Converter |
3.8 代碼層面repository規(guī)范
1、接口名命名規(guī)范
repository中的接口名不要使用底層存儲的名稱(insert、update、add、delete、query等),而是盡量使用具有業(yè)務(wù)含義的命名。比如save、remove、find等。
2、接口的參數(shù)規(guī)范
repository操作的對象是聚合根。因此只能操作聚合根或者實體。這樣才能屏蔽底層的數(shù)據(jù)模型,避免數(shù)據(jù)模型滲透到領(lǐng)域?qū)印?/p>
四、領(lǐng)域?qū)釉O(shè)計規(guī)范
4.1 實體類
大多數(shù)DDD架構(gòu)的核心都是實體類,實體類包含了一個領(lǐng)域里的狀態(tài)、以及對狀態(tài)的直接操作。Entity最重要的設(shè)計原則是保證實體的不變性(Invariants),也就是說要確保無論外部怎么操作,一個實體內(nèi)部的屬性都不能出現(xiàn)相互沖突,狀態(tài)不一致的情況。
4.1.1 創(chuàng)建即一致
constructor參數(shù)要包含所有必要屬性,或者在constructor里有合理的默認值。
4.1.2 使用Factory模式來降低調(diào)用方復(fù)雜度
由于創(chuàng)建即一致的原則,導(dǎo)致實體的構(gòu)造方法可能會很復(fù)雜,因此可以使用Factory模式來快速的構(gòu)造出一個新的實體。降低調(diào)用方的復(fù)雜度。
4.1.3 盡量避免public setter
一個最容易導(dǎo)致不一致性的原因是實體暴露了public的setter方法,特別是set單一參數(shù)會導(dǎo)致狀態(tài)不一致的情況。如果需要改變狀態(tài),盡量語義化方法名稱。
4.1.4 通過聚合根保證主子實體的一致性
通常主實體會包含子實體,這時候主實體就需要起到聚合根的作用,即:
子實體不能單獨存在,只能通過聚合根的方法獲取到。任何外部的對象都不能直接保留子實體的引用
子實體沒有獨立的Repository,不可以單獨保存和取出,必須要通過聚合根的Repository實例化
子實體可以單獨修改自身狀態(tài),但是多個子實體之間的狀態(tài)一致性需要聚合根來保障
exp:常見的電商域中聚合的案例如主子訂單模型、商品/SKU模型、跨子訂單優(yōu)惠、跨店優(yōu)惠模型等。
4.1.5 不可以強依賴其他聚合根實體或領(lǐng)域服務(wù)
一個實體的原則是高內(nèi)聚、低耦合,即一個實體類不能直接在內(nèi)部直接依賴一個外部的實體或服務(wù)。
對外部對象的依賴性會直接導(dǎo)致實體無法被單測; 以及一個實體無法保證外部實體變更后不會影響本實體的一致性和正確性。
正確依賴外部的方式
只保存外部實體的ID:這里我再次強烈建議使用強類型的ID對象,而不是Long型ID。強類型的ID對象不單單能自我包含驗證代碼,保證ID值的正確性,同時還能確保各種入?yún)⒉粫驗閰?shù)順序變化而出bug。
針對于“無副作用”的外部依賴,通過方法入?yún)⒌姆绞絺魅搿1热缟衔闹械膃quip(Weapon,EquipmentService)方法。
4.1.6 任何實體的行為只能直接影響到本實體(和其子實體)
這個原則更多是一個確保代碼可讀性、可理解的原則,即任何實體的行為不能有“直接”的”副作用“,即直接修改其他的實體類。這么做的好處是代碼讀下來不會產(chǎn)生意外。
另一個遵守的原因是可以降低未知的變更的風險。在一個系統(tǒng)里一個實體對象的所有變更操作應(yīng)該都是預(yù)期內(nèi)的,如果一個實體能隨意被外部直接修改的話,會增加代碼bug的風險。
4.1.7 可以利用enum來代替繼承關(guān)系,后續(xù)也可以利用Type Object設(shè)計模式來做到數(shù)據(jù)驅(qū)動
4.2 領(lǐng)域服務(wù)
當一個業(yè)務(wù)邏輯需要用到多個領(lǐng)域?qū)ο笞鳛檩斎?,輸出結(jié)果是一個值對象時,就說明需要使用到領(lǐng)域服務(wù)。
4.2.1 單對象策略型
這種領(lǐng)域?qū)ο笾饕嫦虻氖菃蝹€實體對象的變更,但涉及到多個領(lǐng)域?qū)ο蠡蛲獠恳蕾嚨囊恍┮?guī)則。
在這種類型下,實體應(yīng)該通過方法入?yún)⒌姆绞絺魅脒@種領(lǐng)域服務(wù),然后通過Double Dispatch來反轉(zhuǎn)調(diào)用領(lǐng)域服務(wù)的方法。
什么是Double Dispatch
exp:對于“玩家”實體而言,有一個“equip()”裝備武器的方法。 按照常規(guī)思路,“玩家”實體需要注入一個EquipmentService,然而實體只能保留自己的狀態(tài), 除此之外的其他對象實體無法保證其完整性,因此我們不通過注入的方式使用EquipmentService; 而是通過方法參數(shù)引入的方式來使用。即“玩家”實體的"equip()"方法定義為: public void equip(Weapon weapon, EquipmentService equipmentService) { if(equipmentService.canEquip(this, weapon)) { this.weaponId = weapon.getId(); } } 這種方式就稱為Double Dispatch方式。 Double Dispatch是一個使用Domain Service經(jīng)常會用到的方法,類似于調(diào)用反轉(zhuǎn)。
4.2.2 跨對象事務(wù)型
當一個行為會直接修改多個實體時,不能再通過單一實體的方法作處理,而必須直接使用領(lǐng)域服務(wù)的方法來做操作。在這里,領(lǐng)域服務(wù)更多的起到了跨對象事務(wù)的作用,確保多個實體的變更之間是有一致性的。
4.2.3 通用組件型
這種類型的領(lǐng)域服務(wù)提供了組件化的行為,但本身又不直接綁死在一種實體類上。他的好處是可以通過組件化服務(wù)降低代碼的重復(fù)性。
接口組件化來實現(xiàn)通用領(lǐng)域服務(wù)
exp:在游戲系統(tǒng)中,原價、NPC、怪物都是可移動的。因此可以設(shè)計一個Movable接口, 讓玩家、NPC、怪物實體實現(xiàn)Movable接口。然后再實現(xiàn)一個MoveService,從而實現(xiàn)一個移動通用服務(wù)。
4.3 策略對象(Domain Policy)
Policy或者Strategy設(shè)計模式是一個通用的設(shè)計模式,但是在DDD架構(gòu)中會經(jīng)常出現(xiàn),其核心就是封裝領(lǐng)域規(guī)則。
一個Policy是一個無狀態(tài)的單例對象,通常需要至少2個方法:canApply 和 一個業(yè)務(wù)方法。
canApply方法用來判斷一個Policy是否適用于當前的上下文,如果適用則調(diào)用方會去觸發(fā)業(yè)務(wù)方法。 通常,為了降低一個Policy的可測試性和復(fù)雜度,Policy不應(yīng)該直接操作對象,而是通過返回計算后的值, 在Domain Service里對對象進行操作。
4.4 副作用的處理方法 - 領(lǐng)域事件
什么是副作用?
“副作用”也是一種領(lǐng)域規(guī)則。一般的副作用發(fā)生在核心領(lǐng)域模型狀態(tài)變更后,同步或者異步對另一個對象的影響或行為。比如:當用于積分達到100時,會員等級升1級。
在DDD中,解決“副作用”的手段是領(lǐng)域事件。通過EventBus事件總線可以實現(xiàn)領(lǐng)域事件的傳播。
目前領(lǐng)域事件的缺陷和展望
由于實體需要保證完整性,因此不能夠直接依賴EventBus,所以EventBus只能保持全局singleton。但是全局singleton對象很難被單測,這就容易導(dǎo)致Entity對象很難被完整單測覆蓋全。
五、寫在最后
通過對于DDD的學習與實踐,越來越能夠體會到它作為一種軟件設(shè)計思想和指導(dǎo),對于大型復(fù)雜軟件的建設(shè)十分有幫助。對于歷史遺留屎山工程的重構(gòu)也提供了一個很好的指導(dǎo)方向。
審核編輯 黃宇
-
框架
+關(guān)注
關(guān)注
0文章
404瀏覽量
17721 -
ddd
+關(guān)注
關(guān)注
0文章
23瀏覽量
3004
發(fā)布評論請先 登錄
相關(guān)推薦
評論