DDD小結(jié)
充血與貧血模型
不去具體講概念,感興趣的可以網(wǎng)上自己去找找理解一下。
1.1 還記得初學Java時那個動物對象的例子嗎?
public class Dog {
private int age;
private String color;
public int getAge() {
return age;
}
//吃
public void eat(Object food) {
System.out.println("吃...");
}
//睡
public void rest() {
System.out.println("睡...");
}
}
對象是行為和屬性的封裝。上面的類定義沒毛病。簡單點理解,所謂充血模型其實就是面象對象編程,這也是DDD所推崇的。沒有新鮮事,老外愛搞概念,愣是整出個充血跟DDD來。
1.2 貧血模型的例子
public class Dog {
private int age;
private String color;
private String status;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public class DogService {
//吃
public void eat(Dog dog, Object food) {
dog.setStatus("吃");
System.out.println(dog.toString() + food.toString());
}
//睡
public void rest(Dog dog) {
dog.setStatus("睡覺");
System.out.println(dog.toString() + " 睡覺覺...");
}
public Dog getDog(){
return new Dog();
}
}
public class DogController {
public void eat() {
Object food = new Object();
DogService ds = new DogService();
Dog dog = ds.getDog();
ds.eat(dog, food);
}
public void rest() {
DogService ds = new DogService();
Dog dog = ds.getDog();
ds.rest(dog);
}
}
看到了嗎? Dog類只有針對屬性的get,set操作,行為被搬到service類了.這種風格是貧血模型的代表。缺點:根據(jù)個人代碼風格,有的controller類很重,有的service類很重。最重要的是如果不具備一定的領域分析的知識,往往建出來的類似Dog的領域類是錯誤的,甚至會根據(jù)經(jīng)驗先建數(shù)據(jù)庫再建領域類。經(jīng)驗豐富并且經(jīng)驗是對的那還好,但如果經(jīng)驗是錯的呢。。。
2 Domain層實現(xiàn)
Domain層是具體的業(yè)務領域?qū)樱前l(fā)生業(yè)務變化最為頻繁的地方,是業(yè)務系統(tǒng)最核心的一層,是DDD關注的焦點和難點。這一層包含了如下一些domain object:entity、value object、domain event、domain service、factory、repository等
2.1 實體類Enity
領域?qū)嶓w是domain的核心成員。domain entity具有如下三個特征:
· 唯一業(yè)務標識
· 持有自己的業(yè)務屬性和業(yè)務行為
· 屬性可變,有著自己的生命周期,故實體對象可能和它之前的狀態(tài)不一樣,但有同樣的唯一標識,是同一個實體.
生成唯一標識的方法:
1,用戶提供 (但幾乎沒有人這樣用)
2, 應用程序提供
3, 持久化機制提供 比如mysql主鍵自增 。通常不適合水平分庫,各限界上下文會重復。
創(chuàng)建實體:
1)通過構(gòu)造函數(shù)
2) 通過工廠方法創(chuàng)建
實體類的校驗:
Strusts2框架有一個不錯的validator接口,用于校驗規(guī)則,它在調(diào)用控制器方法之前調(diào)用。如果自己實現(xiàn)一個簡單的檢驗框架呢?
1)定義一個抽象的校驗類
2)定義具體的實現(xiàn)。
經(jīng)驗分享:校驗規(guī)則分內(nèi)凜規(guī)則和依賴規(guī)則。
內(nèi)凜規(guī)則是指那些自身需要必不可少的條件。如名字屬性的不為空及長度等,年齡不能小于0。
依賴規(guī)則:比如請假需要依賴外部日歷實體對象等。
2.2 值對象Value Object
對照起來value object有如下特征:
· 可以有唯一業(yè)務標識 【區(qū)別于domain entity】
· 持有自己的業(yè)務屬性和業(yè)務行為 【同domain entity】
· 一旦定義,他是不可變的,它通常是短暫的,這和java中的值對象(基本類型和String類型)類似 【區(qū)別于domain entity】
· 當度量改變時,可以用另一個對象替換。
· 不會對協(xié)作對象造成副作用。
· 值對象相等性比較。(實體對象比較沒有意義,每個實體對象有唯一值)
1,可替換性
如果一個值對象可以正常描述當前狀態(tài),引用關系可以一值存在。否則可以用一個新的值對象替換.
理解:比如Customer有一個收貨地址Address,這個address可以建模值對象。換地址后這個值對象可替換。
2,值對象相等性
值對象全部屬性值相等時,可以互換。
3,無副作用行為
一個對象方法可以設計為一個無副作用函數(shù)。它不修改對象本身的狀態(tài)。
widthMiddleInitial方法并沒有修改原來對象的firstname lastName屬性
不建議值對象方法參數(shù)里有實體對象,防止修改。
3 聚合
1, 啥是聚合?
理解一下聚合,其實是對象的依賴關系。
2,設計聚合時要考慮一致性.意味著一個客戶請求只在一個聚合實例上執(zhí)行一個命令方法.
3,聚合設計原則:設計小聚合。大的聚合即便能保證事務的一致性,也依然可能限制系統(tǒng)的性能可伸縮性。
一個龐大的聚合根還可能帶來性能損耗。可能需要加載許多關聯(lián)對象。
4.通過唯一標識引用其它聚合。
聚合之間有依賴關系時不要直接寫依賴對象,通過唯一標識來引用。通過標識引用可以將不同限界上下文的分布式領域模型關聯(lián)起來。
5,在邊界之外使用最終一致性。當一個聚合執(zhí)行命令方法時,還需要在其它聚合上執(zhí)行任務,使用最終一致性。一種實用的方法可以支持最終一致性,即一個聚合的命令方法發(fā)布的領域事件及時地發(fā)布給異步的訂閱方。
6,不要在聚合中注入資源庫和領域服務。
設計小的聚合要注意是否過度的小。重新設計的聚合根如下圖:
另一種設計 -> Task也作為聚合根
不在同一事務中修改BacklogItem和Task.
實現(xiàn)最終一致性
實現(xiàn):
1,創(chuàng)建唯一標識的根實體。
2,優(yōu)先使用值對象。
4 領域事件
1,啥是領域事件?
領域?qū)<宜P心的發(fā)生在領域中的一些事件。
將領域中所發(fā)生的活動建模成一系列的離散事件。每個事件都用領域?qū)ο髞肀硎?/span>...領域事件是領域模型的組成部分,表示領域中所發(fā)生的事情
首先是解決領域的聚合性問題。DDD中的聚合有一個原則是,在單個事務中,只允許對一個聚合對象進行修改,由此產(chǎn)生的其他改變必須在單獨的事務中完成。如果一個業(yè)務跨多個聚合對象,領域事件會是一個不錯的工具來解決這個問題。通過領域事件的方式可以達到各個組件之間的數(shù)據(jù)一致性,通過最終一致性取代事務一致性。
其次領域事件也是一種領域分析的工具,有時從領域?qū)<业脑捴校覀兛床怀鲱I域事件的跡象,但是業(yè)務需求依然有可能需要領域事件。動態(tài)流的事件模型加上結(jié)合DDD的聚合實體狀態(tài)和BC,可以有效進行領域建模。
2,領域事件的技術實現(xiàn)
領域事件的技術實現(xiàn)實質(zhì)上觀察者模式的實現(xiàn)。技術的實現(xiàn)都好講,關鍵是理解觀察者模式在領域建模中的應用場景。
3,領域事件需要關注的類容。
一,消息設施最終一致性。
二,事件存儲:
1),將事件存儲作為一個消息隊列使用。
2),檢查由模型命令方法的所產(chǎn)生的所有結(jié)果的記錄
3),使用事件存儲中的數(shù)據(jù)進行業(yè)務預測和分析。、
4),通過事件存儲重一個聚合。
5),撤銷對聚合的操作
4,轉(zhuǎn)發(fā)存儲的架構(gòu)風格。
5 領域服務
1、領域服務表示一個無狀態(tài)的操作,強調(diào)一個無狀態(tài)的操作,狀態(tài)應該在實體中維護,領域服務處理是無狀態(tài)的邏輯過程;
2、實現(xiàn)某個領域的任務,即做的也是領域內(nèi)的事情,是通用語言的表達。而不是應用服務,應用服務是領域服務的客戶方,比如api聚合服務,不應該做領域內(nèi)的事情。也不是基礎設施服務,比如DB或消息基礎組件。特別是不能跟現(xiàn)在常用的mvc+s架構(gòu)中的s(service)層混淆,這種情形下的s,很多時候是持久層接口組裝,更像是DDD中的資源庫的概念。
3、先考慮聚合或值對像的建模,不適合,然后才使用領域服務。聚合(實體)和值對像才是最重要的DDD建模對象,如果反而首先使用領域服務,容易導致“貧血領域模型”。既然不適合直接在實體或值對像上建模,也基本說明很多時候涉及到多個實體或值對像。
那什么時候該使用領域服務呢?
1.執(zhí)行一個顯著的業(yè)務操作過程
2.對領域?qū)ο筮M行轉(zhuǎn)換
3.以多個領域?qū)ο笞鳛檩斎脒M行計算,結(jié)果產(chǎn)生一個值對像基本就是跟上面對領域服務概念的理解是一致的。
領域服務實現(xiàn)是否需要獨立接口?
優(yōu)點:使用接口表達領域概念,而技術實現(xiàn)可以比較隨意,比如放在基礎實施層,或者在依賴倒置原則中,放在應用層實現(xiàn)也可以;獨立接口有利于解耦,通過依賴注入或工廠可以完全解耦客戶端與具體實現(xiàn);
缺點:得寫兩個對象代碼,特別對于java,還得分兩個文件,閱讀代碼也增加點難度,而很多時候一個接口也只有一個實現(xiàn);另外一個命名問題,在DDD中領域?qū)ο竺Q(對應語言實現(xiàn)的類)和操作名稱(對應函數(shù)名)是很重要的,是需要表達通用語言的概念的。但如果定義獨立接口,也就是會XXXservice的名字來定義接口,但服務實現(xiàn)用什么命名呢?如果用XXXserviceImpl,那其實也說明可以不需要定義獨立接口了。測試領域服務其實測試方面,我覺得沒有很多需要關注的,或者說我比較少測試方面的需要。但在測試領域服務一節(jié)有句話卻比較有意思“我們希望對領域服務進行測試,并且希望從客戶端的角度對領域服務進行建模,同時我們希望測試能夠反映查領域服務的使用方式”,即通過測試代碼,告訴客戶端怎么使用領域服務。這其實是測試代碼的一個重要的作用,但也經(jīng)常被我們忽略的。
注意:領域服務不能過多,那變成貧血模型了。
6 應用服務層
應用層通過應用服務接口來暴露系統(tǒng)的全部功能。在應用服務的實現(xiàn)中,它負責編排和轉(zhuǎn)發(fā),它將要實現(xiàn)的功能委托給一個或多個領域?qū)ο髞韺崿F(xiàn),它本身只負責處理業(yè)務用例的執(zhí)行順序以及結(jié)果的拼裝。通過這樣一種方式,它隱藏了領域?qū)拥膹碗s性及其內(nèi)部實現(xiàn)機制。
應用層相對來說是較“薄”的一層,除了定義應用服務之外,在該層我們可以進行安全認證,權(quán)限校驗,持久化事務控制,或者向其他系統(tǒng)發(fā)生基于事件的消息通知,另外還可以用于創(chuàng)建郵件以發(fā)送給客戶等。
7 架構(gòu)及架構(gòu)風格
一, 架構(gòu)部分:
1) 限界上下文、子域:
領域:通常是指整個系統(tǒng)的業(yè)務邊界.也可以指子域如核心域等。
子域:業(yè)務系統(tǒng)的某個方面。
限界上下文:同樣的是業(yè)務系統(tǒng)中某個方面,它比子域的粒度更小。通常一個子域可以只包含一個限界上下文。
光看這個,有點繞。但直白點理解,其實它們就是架構(gòu)中的關于系統(tǒng)/模塊的劃分。系統(tǒng)可以劃分為多個子系統(tǒng),子系統(tǒng)當然還可以劃分為更小的子系統(tǒng)。或者業(yè)務模塊的劃分。
如果還不夠直白,那么如果有微服務經(jīng)驗,可以想想、對照服務的劃分。
2)上下文映射圖:
DDD中的圖示。個人理解:其實它就是各子系統(tǒng)/模塊的依賴關系。比如現(xiàn)在典型電商系統(tǒng)子系統(tǒng)劃分 會員系統(tǒng)、商品管理系統(tǒng)、資金系統(tǒng)。。。
商品、資金均依賴于會員系統(tǒng)。基本上資金限界上下文同時也是一個子域。同時它們也各自被劃分為了一個微服務系統(tǒng)。
二,架構(gòu)風格:
《實現(xiàn)領域驅(qū)動設計》關于架構(gòu)一章,章節(jié)名雖然叫架構(gòu),但應該理解成架構(gòu)風格。就象傳統(tǒng)的三層架一樣,是一種架構(gòu)風格。
1)六邊形架構(gòu)
它并不是真的有六條邊.它也叫端口+適配器.端口可以理解成http,socket自定義傳輸協(xié)議、或者某個RPC調(diào)用協(xié)議等。六邊形的內(nèi)部代表了application和domain層。外部代表應用的驅(qū)動邏輯、基礎設施或其他應用。內(nèi)部通過端口和外部系統(tǒng)通信,端口代表了一種協(xié)議,以API呈現(xiàn)。
一個例子:
2) Rest
RESTful風格的架構(gòu)將‘資源’放在第一位,每個‘資源’都有一個URI與之對應,可以將‘資源’看著是ddd中的實體;RESTful采用具有自描述功能的消息實現(xiàn)無狀態(tài)通信,提高系統(tǒng)的可用性;至于‘資源’的哪些屬性可以公開出去,針對‘資源’的操作,RESTful使用HTTP協(xié)議的已有方法來實現(xiàn):GET、PUT、POST和DELETE。
3) CQRS
CQRS就是平常大家在講的讀寫分離,通常讀寫分離的目的是為了提高查詢性能,同時達到讀/寫的解耦。
CQRS適用于極少數(shù)復雜的業(yè)務領域,如果不是很適合反而會增加復雜度;另一個適用場景為獲取高性能的服務
4)事件驅(qū)動架構(gòu)
事件可能有不同類型,這里主要只關心領域事件。
象不象微服務架構(gòu)中引入消息中間件來解耦和最終事務一致?
5)管道和過濾器風格.
管道過濾器來源于linux命令類似 ps -ef | grep tomcat . | 即管道。ps 處理的結(jié)果經(jīng)過管道符| 作為下一個命令的輸入。
《實現(xiàn)領域驅(qū)動設計》中舉了個基于事件的發(fā)布、訂閱的例子來實現(xiàn)管道和過濾器架構(gòu)風格。事件的發(fā)布訂閱中心作為管道。事件的發(fā)布、訂閱方作為過濾器。
推而廣之,考慮基于消息中間件的管道過濾器。
8 最后
1) 采用三層結(jié)構(gòu)的架構(gòu)風格就沒有領域相關的類容了嗎?
答案當然是有的,但是不象DDD這樣有明確的區(qū)分。往往因為程序員沒有相關概念或多多思考就容易引發(fā)問題。舉個栗:
public class SearchController {
private SearchService service;
@RequestMapping(value="search")
public List search(String searchStr) {
service.search(searchStr);
}
}
public class SearchService{
public List search(String str){
if(系統(tǒng)變量=="solor"){
solorService.search(str)
}else {
dbService.search(str);
}
}
}
功能很強大,支持數(shù)據(jù)檢索和solor檢索。但是再看,solor檢索和數(shù)據(jù)檢索明顯不是一個玩意,不應該同時出現(xiàn)在SearchService里。從DDD觀點來看,也明顯屬于不同的領域?qū)崿F(xiàn)模型。即使在同一個子域里,劃分微服務那它也應該是兩個微服務實現(xiàn)。顯明擴展性不好。
2)DDD也有麻煩問題
比如要考慮根實體、值對象、實體。都不象時考慮使用領域服務類。領域服務類干脆就是貧血模血。
3)最后的最后
作為六邊型架構(gòu)風格的實現(xiàn),看看一個開發(fā)包的結(jié)構(gòu)圖
controller則是適配器。