充血與貧血模型
不去具體講概念,感興趣的可以網上自己去找找理解一下。
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類了.這種風格是貧血模型的代表。缺點:根據個人代碼風格,有的controller類很重,有的service類很重。最重要的是如果不具備一定的領域分析的知識,往往建出來的類似Dog的領域類是錯誤的,甚至會根據經驗先建數據庫再建領域類。經驗豐富并且經驗是對的那還好,但如果經驗是錯的呢。。。
2 Domain層實現
Domain層是具體的業務領域層,是發生業務變化最為頻繁的地方,是業務系統最核心的一層,是DDD關注的焦點和難點。這一層包含了如下一些domain object:entity、value object、domain event、domain service、factory、repository等
2.1 實體類Enity
領域實體是domain的核心成員。domain entity具有如下三個特征:
· 唯一業務標識
· 持有自己的業務屬性和業務行為
· 屬性可變,有著自己的生命周期,故實體對象可能和它之前的狀態不一樣,但有同樣的唯一標識,是同一個實體.
生成唯一標識的方法:
1,用戶提供 (但幾乎沒有人這樣用)
2, 應用程序提供
3, 持久化機制提供 比如mysql主鍵自增 。通常不適合水平分庫,各限界上下文會重復。

創建實體:
1)通過構造函數

2) 通過工廠方法創建

實體類的校驗:
Strusts2框架有一個不錯的validator接口,用于校驗規則,它在調用控制器方法之前調用。如果自己實現一個簡單的檢驗框架呢?
1)定義一個抽象的校驗類

2)定義具體的實現。

經驗分享:校驗規則分內凜規則和依賴規則。
內凜規則是指那些自身需要必不可少的條件。如名字屬性的不為空及長度等,年齡不能小于0。
依賴規則:比如請假需要依賴外部日歷實體對象等。
2.2 值對象Value Object
對照起來value object有如下特征:
· 可以有唯一業務標識 【區別于domain entity】
· 持有自己的業務屬性和業務行為 【同domain entity】
· 一旦定義,他是不可變的,它通常是短暫的,這和java中的值對象(基本類型和String類型)類似 【區別于domain entity】
· 當度量改變時,可以用另一個對象替換。
· 不會對協作對象造成副作用。
· 值對象相等性比較。(實體對象比較沒有意義,每個實體對象有唯一值)
1,可替換性
如果一個值對象可以正常描述當前狀態,引用關系可以一值存在。否則可以用一個新的值對象替換.
理解:比如Customer有一個收貨地址Address,這個address可以建模值對象。換地址后這個值對象可替換。
2,值對象相等性
值對象全部屬性值相等時,可以互換。
3,無副作用行為
一個對象方法可以設計為一個無副作用函數。它不修改對象本身的狀態。


widthMiddleInitial方法并沒有修改原來對象的firstname lastName屬性
不建議值對象方法參數里有實體對象,防止修改。
3 聚合
1, 啥是聚合?
理解一下聚合,其實是對象的依賴關系。
2,設計聚合時要考慮一致性.意味著一個客戶請求只在一個聚合實例上執行一個命令方法.
3,聚合設計原則:設計小聚合。大的聚合即便能保證事務的一致性,也依然可能限制系統的性能可伸縮性。

一個龐大的聚合根還可能帶來性能損耗。可能需要加載許多關聯對象。
4.通過唯一標識引用其它聚合。
聚合之間有依賴關系時不要直接寫依賴對象,通過唯一標識來引用。通過標識引用可以將不同限界上下文的分布式領域模型關聯起來。
5,在邊界之外使用最終一致性。當一個聚合執行命令方法時,還需要在其它聚合上執行任務,使用最終一致性。一種實用的方法可以支持最終一致性,即一個聚合的命令方法發布的領域事件及時地發布給異步的訂閱方。
6,不要在聚合中注入資源庫和領域服務。
設計小的聚合要注意是否過度的小。重新設計的聚合根如下圖:

另一種設計 -> Task也作為聚合根

不在同一事務中修改BacklogItem和Task.
實現最終一致性

實現:
1,創建唯一標識的根實體。
2,優先使用值對象。
4 領域事件
1,啥是領域事件?
領域專家所關心的發生在領域中的一些事件。
將領域中所發生的活動建模成一系列的離散事件。每個事件都用領域對象來表示...領域事件是領域模型的組成部分,表示領域中所發生的事情
首先是解決領域的聚合性問題。DDD中的聚合有一個原則是,在單個事務中,只允許對一個聚合對象進行修改,由此產生的其他改變必須在單獨的事務中完成。如果一個業務跨多個聚合對象,領域事件會是一個不錯的工具來解決這個問題。通過領域事件的方式可以達到各個組件之間的數據一致性,通過最終一致性取代事務一致性。
其次領域事件也是一種領域分析的工具,有時從領域專家的話中,我們看不出領域事件的跡象,但是業務需求依然有可能需要領域事件。動態流的事件模型加上結合DDD的聚合實體狀態和BC,可以有效進行領域建模。
2,領域事件的技術實現
領域事件的技術實現實質上觀察者模式的實現。技術的實現都好講,關鍵是理解觀察者模式在領域建模中的應用場景。
3,領域事件需要關注的類容。
一,消息設施最終一致性。
二,事件存儲:
1),將事件存儲作為一個消息隊列使用。
2),檢查由模型命令方法的所產生的所有結果的記錄
3),使用事件存儲中的數據進行業務預測和分析。、
4),通過事件存儲重一個聚合。
5),撤銷對聚合的操作
4,轉發存儲的架構風格。
5 領域服務
1、領域服務表示一個無狀態的操作,強調一個無狀態的操作,狀態應該在實體中維護,領域服務處理是無狀態的邏輯過程;
2、實現某個領域的任務,即做的也是領域內的事情,是通用語言的表達。而不是應用服務,應用服務是領域服務的客戶方,比如api聚合服務,不應該做領域內的事情。也不是基礎設施服務,比如DB或消息基礎組件。特別是不能跟現在常用的mvc+s架構中的s(service)層混淆,這種情形下的s,很多時候是持久層接口組裝,更像是DDD中的資源庫的概念。
3、先考慮聚合或值對像的建模,不適合,然后才使用領域服務。聚合(實體)和值對像才是最重要的DDD建模對象,如果反而首先使用領域服務,容易導致“貧血領域模型”。既然不適合直接在實體或值對像上建模,也基本說明很多時候涉及到多個實體或值對像。
那什么時候該使用領域服務呢?
1.執行一個顯著的業務操作過程
2.對領域對象進行轉換
3.以多個領域對象作為輸入進行計算,結果產生一個值對像基本就是跟上面對領域服務概念的理解是一致的。
領域服務實現是否需要獨立接口?
優點:使用接口表達領域概念,而技術實現可以比較隨意,比如放在基礎實施層,或者在依賴倒置原則中,放在應用層實現也可以;獨立接口有利于解耦,通過依賴注入或工廠可以完全解耦客戶端與具體實現;
缺點:得寫兩個對象代碼,特別對于java,還得分兩個文件,閱讀代碼也增加點難度,而很多時候一個接口也只有一個實現;另外一個命名問題,在DDD中領域對象名稱(對應語言實現的類)和操作名稱(對應函數名)是很重要的,是需要表達通用語言的概念的。但如果定義獨立接口,也就是會XXXservice的名字來定義接口,但服務實現用什么命名呢?如果用XXXserviceImpl,那其實也說明可以不需要定義獨立接口了。測試領域服務其實測試方面,我覺得沒有很多需要關注的,或者說我比較少測試方面的需要。但在測試領域服務一節有句話卻比較有意思“我們希望對領域服務進行測試,并且希望從客戶端的角度對領域服務進行建模,同時我們希望測試能夠反映查領域服務的使用方式”,即通過測試代碼,告訴客戶端怎么使用領域服務。這其實是測試代碼的一個重要的作用,但也經常被我們忽略的。
注意:領域服務不能過多,那變成貧血模型了。
6 應用服務層
應用層通過應用服務接口來暴露系統的全部功能。在應用服務的實現中,它負責編排和轉發,它將要實現的功能委托給一個或多個領域對象來實現,它本身只負責處理業務用例的執行順序以及結果的拼裝。通過這樣一種方式,它隱藏了領域層的復雜性及其內部實現機制。
應用層相對來說是較“薄”的一層,除了定義應用服務之外,在該層我們可以進行安全認證,權限校驗,持久化事務控制,或者向其他系統發生基于事件的消息通知,另外還可以用于創建郵件以發送給客戶等。
7 架構及架構風格
一, 架構部分:
1) 限界上下文、子域:
領域:通常是指整個系統的業務邊界.也可以指子域如核心域等。
子域:業務系統的某個方面。
限界上下文:同樣的是業務系統中某個方面,它比子域的粒度更小。通常一個子域可以只包含一個限界上下文。
光看這個,有點繞。但直白點理解,其實它們就是架構中的關于系統/模塊的劃分。系統可以劃分為多個子系統,子系統當然還可以劃分為更小的子系統。或者業務模塊的劃分。
如果還不夠直白,那么如果有微服務經驗,可以想想、對照服務的劃分。
2)上下文映射圖:

DDD中的圖示。個人理解:其實它就是各子系統/模塊的依賴關系。比如現在典型電商系統子系統劃分 會員系統、商品管理系統、資金系統。。。

商品、資金均依賴于會員系統。基本上資金限界上下文同時也是一個子域。同時它們也各自被劃分為了一個微服務系統。
二,架構風格:
《實現領域驅動設計》關于架構一章,章節名雖然叫架構,但應該理解成架構風格。就象傳統的三層架一樣,是一種架構風格。
1)六邊形架構

它并不是真的有六條邊.它也叫端口+適配器.端口可以理解成http,socket自定義傳輸協議、或者某個RPC調用協議等。六邊形的內部代表了application和domain層。外部代表應用的驅動邏輯、基礎設施或其他應用。內部通過端口和外部系統通信,端口代表了一種協議,以API呈現。
一個例子:

2) Rest
RESTful風格的架構將‘資源’放在第一位,每個‘資源’都有一個URI與之對應,可以將‘資源’看著是ddd中的實體;RESTful采用具有自描述功能的消息實現無狀態通信,提高系統的可用性;至于‘資源’的哪些屬性可以公開出去,針對‘資源’的操作,RESTful使用HTTP協議的已有方法來實現:GET、PUT、POST和DELETE。
3) CQRS
CQRS就是平常大家在講的讀寫分離,通常讀寫分離的目的是為了提高查詢性能,同時達到讀/寫的解耦。
CQRS適用于極少數復雜的業務領域,如果不是很適合反而會增加復雜度;另一個適用場景為獲取高性能的服務
4)事件驅動架構
事件可能有不同類型,這里主要只關心領域事件。

象不象微服務架構中引入消息中間件來解耦和最終事務一致?
5)管道和過濾器風格.
管道過濾器來源于linux命令類似 ps -ef | grep tomcat . | 即管道。ps 處理的結果經過管道符| 作為下一個命令的輸入。
《實現領域驅動設計》中舉了個基于事件的發布、訂閱的例子來實現管道和過濾器架構風格。事件的發布訂閱中心作為管道。事件的發布、訂閱方作為過濾器。
推而廣之,考慮基于消息中間件的管道過濾器。
8 最后
1) 采用三層結構的架構風格就沒有領域相關的類容了嗎?
答案當然是有的,但是不象DDD這樣有明確的區分。往往因為程序員沒有相關概念或多多思考就容易引發問題。舉個栗:
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(系統變量=="solor"){
solorService.search(str)
}else {
dbService.search(str);
}
}
}
功能很強大,支持數據檢索和solor檢索。但是再看,solor檢索和數據檢索明顯不是一個玩意,不應該同時出現在SearchService里。從DDD觀點來看,也明顯屬于不同的領域實現模型。即使在同一個子域里,劃分微服務那它也應該是兩個微服務實現。顯明擴展性不好。
2)DDD也有麻煩問題
比如要考慮根實體、值對象、實體。都不象時考慮使用領域服務類。領域服務類干脆就是貧血模血。
3)最后的最后
作為六邊型架構風格的實現,看看一個開發包的結構圖
controller則是適配器。