在上一篇文章中,通過Spring Web應(yīng)用的瑕疵引出改善的措施椎眯,我們講解了領(lǐng)域驅(qū)動開發(fā)的相關(guān)概念和設(shè)計策略挠将。本文主要講解領(lǐng)域模型的幾種類型和DDD的簡單實踐案例。
架構(gòu)風(fēng)格
在《實現(xiàn)領(lǐng)域驅(qū)動設(shè)計》一書中提到了幾種架構(gòu)風(fēng)格:六邊形架構(gòu)编整、REST架構(gòu)舔稀、CQRS 和事件驅(qū)動等。在實際使用中掌测,落地的架構(gòu)并非是純粹其中的一種内贮,而很有可能戶將上述幾種架構(gòu)風(fēng)格結(jié)合起來實現(xiàn)。
分層架構(gòu)
分層架構(gòu)的一個重要原則是每層只能與位于其下方的層發(fā)生耦合赏半。分層架構(gòu)可以簡單分為兩種贺归,即嚴格分層架構(gòu)和松散分層架構(gòu)。在嚴格分層架構(gòu)中断箫,某層只能與位于其直接下方的層發(fā)生耦合拂酣,而在松散分層架構(gòu)中,則允許某層與它的任意下方層發(fā)生耦合仲义。DDD分層架構(gòu)中比較經(jīng)典的三種模式:四層架構(gòu)婶熬、五層架構(gòu)和六邊形架構(gòu)。
四層架構(gòu)
Eric Evans在《領(lǐng)域驅(qū)動設(shè)計-軟件核心復(fù)雜性應(yīng)對之道》這本書中提出了傳統(tǒng)的四層架構(gòu)模式:
- User Interface為用戶界面層(或表示層)埃撵,負責(zé)向用戶顯示信息和解釋用戶命令赵颅。這里指的用戶可以是另一個計算機系統(tǒng),不一定是使用用戶界面的人暂刘。
- Application為應(yīng)用層饺谬,定義軟件要完成的任務(wù),并且指揮表達領(lǐng)域概念的對象來解決問題谣拣。這一層所負責(zé)的工作對業(yè)務(wù)來說意義重大募寨,也是與其它系統(tǒng)的應(yīng)用層進行交互的必要渠道。應(yīng)用層要盡量簡單森缠,不包含業(yè)務(wù)規(guī)則或者知識拔鹰,而只為下一層中的領(lǐng)域?qū)ο髤f(xié)調(diào)任務(wù),分配工作贵涵,使它們互相協(xié)作列肢。它沒有反映業(yè)務(wù)情況的狀態(tài)恰画,但是卻可以具有另外一種狀態(tài),為用戶或程序顯示某個任務(wù)的進度瓷马。
- Domain為領(lǐng)域?qū)樱ɑ蚰P蛯樱┧┗梗撠?zé)表達業(yè)務(wù)概念,業(yè)務(wù)狀態(tài)信息以及業(yè)務(wù)規(guī)則欧聘。盡管保存業(yè)務(wù)狀態(tài)的技術(shù)細節(jié)是由基礎(chǔ)設(shè)施層實現(xiàn)的自沧,但是反映業(yè)務(wù)情況的狀態(tài)是由本層控制并且使用的。領(lǐng)域?qū)邮菢I(yè)務(wù)軟件的核心树瞭,領(lǐng)域模型位于這一層。
- Infrastructure層為基礎(chǔ)實施層爱谁,向其他層提供通用的技術(shù)能力:為應(yīng)用層傳遞消息晒喷,為領(lǐng)域?qū)犹峁┏志没瘷C制,為用戶界面層繪制屏幕組件访敌,等等凉敲。基礎(chǔ)設(shè)施層還能夠通過架構(gòu)框架來支持四個層次間的交互模式寺旺。
傳統(tǒng)的四層架構(gòu)都是限定型松散分層架構(gòu)爷抓,即Infrastructure層的任意上層都可以訪問該層(“L”型),而其它層遵守嚴格分層架構(gòu)阻塑。
五層架構(gòu)
五層架構(gòu)是根據(jù)《DCI架構(gòu):面向?qū)ο缶幊痰男聵?gòu)想》中提及的DCI架構(gòu)模式總結(jié)而成蓝撇。DCI架構(gòu)(Data碑幅、Context和Interactive三層架構(gòu)):
- Data層描述系統(tǒng)有哪些領(lǐng)域概念及其之間的關(guān)系丝蹭,該層專注于領(lǐng)域?qū)ο蟮拇_立和這些對象的生命周期管理及關(guān)系,讓程序員站在對象的角度思考系統(tǒng)澈驼,從而讓“系統(tǒng)是什么”更容易被理解走搁。
- Context層:是盡可能薄的一層独柑。Context往往被實現(xiàn)得無狀態(tài),只是找到合適的role私植,讓role交互起來完成業(yè)務(wù)邏輯即可忌栅。但是簡單并不代表不重要,顯示化context層正是為人去理解軟件業(yè)務(wù)流程提供切入點和主線曲稼。
- Interactive層主要體現(xiàn)在對role的建模索绪,role是每個context中復(fù)雜的業(yè)務(wù)邏輯的真正執(zhí)行者,體現(xiàn)“系統(tǒng)做什么”躯肌。role所做的是對行為進行建模者春,它聯(lián)接了context和領(lǐng)域?qū)ο蟆S捎谙到y(tǒng)的行為是復(fù)雜且多變的清女,role使得系統(tǒng)將穩(wěn)定的領(lǐng)域模型層和多變的系統(tǒng)行為層進行了分離钱烟,由role專注于對系統(tǒng)行為進行建模。該層往往關(guān)注于系統(tǒng)的可擴展性,更加貼近于軟件工程實踐拴袭,在面向?qū)ο笾懈嗟氖且灶惖囊暯沁M行思考設(shè)計读第。
DCI目前廣泛被看作是對DDD的一種發(fā)展和補充,用在基于面向?qū)ο蟮念I(lǐng)域建模上拥刻。五層架構(gòu)的具體定義如下:
- User Interface是用戶接口層怜瞒,主要用于處理用戶發(fā)送的Restful請求和解析用戶輸入的配置文件等,并將信息傳遞給Application層的接口般哼。
- Application層是應(yīng)用層吴汪,負責(zé)多進程管理及調(diào)度、多線程管理及調(diào)度蒸眠、多協(xié)程調(diào)度和維護業(yè)務(wù)實例的狀態(tài)模型漾橙。當(dāng)調(diào)度層收到用戶接口層的請求后,委托Context層與本次業(yè)務(wù)相關(guān)的上下文進行處理楞卡。
- Context是環(huán)境層霜运,以上下文為單位,將Domain層的領(lǐng)域?qū)ο骳ast成合適的role蒋腮,讓role交互起來完成業(yè)務(wù)邏輯淘捡。
- Domain層是領(lǐng)域?qū)樱x領(lǐng)域模型池摧,不僅包括領(lǐng)域?qū)ο蠹捌渲g關(guān)系的建模焦除,還包括對象的角色role的顯式建模。
- Infrastructure層是基礎(chǔ)實施層作彤,為其他層提供通用的技術(shù)能力:業(yè)務(wù)平臺踢京,編程框架,持久化機制宦棺,消息機制瓣距,第三方庫的封裝,通用算法代咸,等等蹈丸。
六邊形架構(gòu)
六邊形架構(gòu)(Hexagonal Architecture),又稱為端口和適配器風(fēng)格呐芥,最早由 Alistair Cockburn 提出逻杖。在 DDD 社區(qū)得到了發(fā)展和推廣,之所以是六變形是為了突顯這是個扁平的架構(gòu)思瘟,每個邊界的權(quán)重是相等的荸百。
我們知道,經(jīng)典分層架構(gòu)分為三層(展現(xiàn)層滨攻、應(yīng)用層够话、數(shù)據(jù)訪問層)蓝翰,而對于六邊形架構(gòu),可以分成另外的三層:
- 領(lǐng)域?qū)樱―omain Layer):最里面女嘲,純粹的核心業(yè)務(wù)邏輯畜份,一般不包含任何技術(shù)實現(xiàn)或引用。
- 端口層(Ports Layer):領(lǐng)域?qū)又庑滥幔撠?zé)接收與用例相關(guān)的所有請求爆雹,這些請求負責(zé)在領(lǐng)域?qū)又袇f(xié)調(diào)工作。端口層在端口內(nèi)部作為領(lǐng)域?qū)拥倪吔玢倒模诙丝谕獠縿t扮演了外部實體的角色钙态。
- 適配器層(Adapters Layer):端口層之外,負責(zé)以某種格式接收輸入菇晃、及產(chǎn)生輸出驯绎。比如,對于 HTTP 用戶請求谋旦,適配器會將轉(zhuǎn)換為對領(lǐng)域?qū)拥恼{(diào)用,并將領(lǐng)域?qū)觽骰氐捻憫?yīng)進行封送屈尼,通過 HTTP 傳回調(diào)用客戶端册着。在適配器層不存在領(lǐng)域邏輯,它的唯一職責(zé)就是在外部世界與領(lǐng)域?qū)又g進行技術(shù)性的轉(zhuǎn)換脾歧。適配器能夠與端口的某個協(xié)議相關(guān)聯(lián)并使用該端口甲捏,多個適配器可以使用同一個端口,在切換到某種新的用戶界面時鞭执,可以讓新界面與老界面同時使用相同的端口司顿。
這樣做的好處是將使業(yè)務(wù)邊界更加清晰,從而獲得更好的擴展性兄纺,除此之外大溜,業(yè)務(wù)復(fù)雜度和技術(shù)復(fù)雜度分離,是 DDD 的重要基礎(chǔ)估脆,核心的領(lǐng)域?qū)涌梢詫W⒃跇I(yè)務(wù)邏輯而不用理會技術(shù)依賴钦奋,外部接口在被消費者調(diào)用的時候也不用去關(guān)心業(yè)務(wù)內(nèi)部是如何實現(xiàn)。
REST架構(gòu)
RESTful風(fēng)格的架構(gòu)將 資源
放在第一位疙赠,每個 資源
都有一個 URI 與之對應(yīng)付材,可以將 資源
看著是 DDD 中的實體;RESTful 采用具有自描述功能的消息實現(xiàn)無狀態(tài)通信圃阳,提高系統(tǒng)的可用性厌衔;至于 資源
的哪些屬性可以公開出去,針對 資源
的操作捍岳,RESTful使用HTTP協(xié)議的已有方法來實現(xiàn):GET富寿、PUT睬隶、POST和DELETE。
在 DDD 的實現(xiàn)中作喘,我們可以將對外的服務(wù)設(shè)計為 RESTful 風(fēng)格的服務(wù)理疙,將實體/值對象/領(lǐng)域服務(wù)作為資源
對外提供增刪改查服務(wù)。但是并不建議直接將實體暴露在外泞坦,一來實體的某些隱私屬性并不能對外暴露窖贤,二來某些資源獲取場景并不是一個實體就能滿足。因此我們在實際實踐過程中贰锁,在領(lǐng)域模型上增加了 DTO 這樣一個角色赃梧,DTO 可以組合多個實體/值對象的資源對外暴露。
CQRS
CQRS 就是平常大家在講的讀寫分離豌熄,通常讀寫分離的目的是為了提高查詢性能授嘀,同時達到讀/寫的解耦。讓 DDD 和 CQRS 結(jié)合锣险,我們可以分別對讀和寫建模蹄皱,查詢模型通常是一種非規(guī)范化數(shù)據(jù)模型,它并不反映領(lǐng)域行為芯肤,只是用于數(shù)據(jù)顯示巷折;命令模型執(zhí)行領(lǐng)域行為,且在領(lǐng)域行為執(zhí)行完成后崖咨,想辦法通知到查詢模型锻拘。
那么命令模型如何通知到查詢模型呢? 如果查詢模型和領(lǐng)域模型共享數(shù)據(jù)源击蹲,則可以省卻這一步署拟;如果沒有共用數(shù)據(jù)源,則可以借助于 消息模式
(Messaging Patterns)通知到查詢模型歌豺,從而達到最終一致性(Eventual Consistency)推穷。
Martin 在 blog 中指出:CQRS 適用于極少數(shù)復(fù)雜的業(yè)務(wù)領(lǐng)域,如果不是很適合反而會增加復(fù)雜度类咧;另一個適用場景為獲取高性能的服務(wù)缨恒。
領(lǐng)域模型
在上面小節(jié)講解了領(lǐng)域驅(qū)動設(shè)計的幾種架構(gòu)風(fēng)格,下面我們具體結(jié)合簡單的實例來看其中的領(lǐng)域模型劃分轮听,初步分為4大類:
- 失血模型
- 貧血模型
- 充血模型
- 脹血模型
我們看看這些領(lǐng)域模型的具體內(nèi)容骗露,以及他們的優(yōu)缺點。
失血模型
失血模型簡單來說血巍,就是domain object只有屬性的getter/setter方法的純數(shù)據(jù)類萧锉,所有的業(yè)務(wù)邏輯完全由business object來完成(又稱TransactionScript),這種模型下的domain object被Martin Fowler稱之為“貧血的domain object”述寡。如下:
-
一個實體類叫做Item
public class Item implements Serializable { private Long id = null; private int version; private String name; private User seller; // ... // getter/setter方法省略不寫柿隙,避免篇幅太長
}
```
-
一個DAO接口類叫做ItemDao
public interface ItemDao { public Item getItemById(Long id); public Collection findAll(); public void updateItem(Item item);
}
```
-
一個DAO接口實現(xiàn)類叫做ItemDaoHibernateImpl
public class ItemDaoImpl implements ItemDao extends DaoSupport { public Item getItemById(Long id) { return (Item) getHibernateTemplate().load(Item.class, id); } public Collection findAll() { return (List) getHibernateTemplate().find("from Item"); } public void updateItem(Item item) { getHibernateTemplate().update(item); }
}
```
-
一個業(yè)務(wù)邏輯類叫做ItemManager(或者叫做ItemService)
public class ItemManager {
private ItemDao itemDao;
public void setItemDao(ItemDao itemDao) { this.itemDao = itemDao;}
public Bid loadItemById(Long id) {
itemDao.loadItemById(id);
}
public Collection listAllItems() {
return itemDao.findAll();
}
public Bid placeBid(Item item, User bidder, MonetaryAmount bidAmount,
Bid currentMaxBid, Bid currentMinBid) throws BusinessException {
if (currentMaxBid != null && currentMaxBid.getAmount().compareTo(bidAmount) > 0) {
throw new BusinessException("Bid too low.");
}
// ...
}
```
以上是一個完整的失血模型的示例代碼叶洞。在這個示例中,loadItemById禀崖、findAll 等等業(yè)務(wù)邏輯統(tǒng)統(tǒng)放在 ItemManager 中實現(xiàn)衩辟,而 Item 只有 getter/setter 方法。
貧血模型
簡單來說波附,就是 domain ojbect 包含了不依賴于持久化的領(lǐng)域邏輯艺晴,而那些依賴持久化的領(lǐng)域邏輯被分離到 Service 層。
Service(業(yè)務(wù)邏輯掸屡,事務(wù)封裝) --> DAO ---> domain object
這也就是 Martin Fowler 指的 rich domain object:
- 一個帶有業(yè)務(wù)邏輯的實體類封寞,即domain object是Item
- 一個DAO接口ItemDao
- 一個DAO實現(xiàn)ItemDaoHibernateImpl
- 一個業(yè)務(wù)邏輯對象ItemManager
這種模型的優(yōu)點:
- 各層單向依賴,結(jié)構(gòu)清楚仅财,易于實現(xiàn)和維護
- 設(shè)計簡單易行狈究,底層模型非常穩(wěn)定
缺點為:
- domain object的部分比較緊密依賴的持久化 domain logic 被分離到Service層,顯得不夠 OO
- Service 層過于厚重
具體代碼較為簡單盏求,不再展示抖锥。
充血模型
充血模型和第二種模型差不多,所不同的就是如何劃分業(yè)務(wù)邏輯碎罚,即認為磅废,絕大多業(yè)務(wù)邏輯都應(yīng)該被放在domain object里面(包括持久化邏輯),而Service層應(yīng)該是很薄的一層魂莫,僅僅封裝事務(wù)和少量邏輯,不和DAO層打交道爹耗。
Service(事務(wù)封裝) ---> domain object <---> DAO
[圖片上傳失敗...(image-5fe4c2-1555327562177)]
這種模型就是把第二種模型的 domain object 和 business object 合二為一了耙考。所以 ItemManager 就不需要了,在這種模型下面潭兽,只有三個類倦始,他們分別是:
- Item:包含了實體類信息,也包含了所有的業(yè)務(wù)邏輯
- ItemDao:持久化DAO接口類
- ItemDaoHibernateImpl:DAO接口的實現(xiàn)類
在這種模型中山卦,所有的業(yè)務(wù)邏輯全部都在Item中鞋邑,事務(wù)管理也在Item中實現(xiàn)。
這種模型的優(yōu)點:
- 更加符合OO的原則
- Service層很薄账蓉,只充當(dāng)Facade的角色枚碗,不和DAO打交道。
這種模型的缺點:
- DAO和domain object形成了雙向依賴铸本,復(fù)雜的雙向依賴會導(dǎo)致很多潛在的問題肮雨。
- 如何劃分Service層邏輯和domain層邏輯是非常含混的,在實際項目中箱玷,由于設(shè)計和開發(fā)人員的水平差異怨规,可能導(dǎo)致整個結(jié)構(gòu)的混亂無序陌宿。
- 考慮到Service層的事務(wù)封裝特性,Service層必須對所有的domain object的邏輯提供相應(yīng)的事務(wù)封裝方法波丰,其結(jié)果就是Service完全重定義一遍所有的domain logic壳坪,非常煩瑣,而且 Service 的事務(wù)化封裝其意義就等于把 OO 的domain logic 轉(zhuǎn)換為過程的 Service TransactionScript掰烟。
脹血模型
基于充血模型的第三個缺點爽蝴,有同學(xué)提出,干脆取消Service層媚赖,只剩下domain object和DAO兩層霜瘪,在domain object的domain logic上面封裝事務(wù)。
domain object(事務(wù)封裝惧磺,業(yè)務(wù)邏輯) <---> DAO
似乎ruby on rails就是這種模型颖对,他甚至把 domain object 和 DAO 都合并了。
該模型優(yōu)點:
- 簡化了分層
- 也算符合OO
該模型缺點:
- 很多不是domain logic的 service 邏輯也被強行放入 domain object磨隘,引起了domain ojbect模型的不穩(wěn)定
- domain object 暴露給web層過多的信息缤底,可能引起意想不到的副作用。
小結(jié)
在這四種模型當(dāng)中番捂,失血模型和脹血模型應(yīng)該是不被提倡的个唧。而貧血模型和充血模型從技術(shù)上來說,都已經(jīng)是可行的了设预。貧血模型和充血模型哪個更加好一些徙歼?人們針對這個問題進行了曠日持久的爭論,最后仍然沒有什么結(jié)果鳖枕。雙方爭論的焦點主要在我上面加粗的兩句話上魄梯,就是領(lǐng)域模型是否要依賴持久層,因為依賴持久層就意味著單元測試的展開要更加困難(無法脫離框架進行測試宾符,原文的討論中這里專指Hibernate)酿秸,領(lǐng)域?qū)泳透y獨立,將來也更難從應(yīng)用程序中剝離出來魏烫,當(dāng)然好處是業(yè)務(wù)邏輯不必混放在不同的層中辣苏,使得單一職責(zé)性體現(xiàn)的更好。而支持者(充血模型)認為哄褒,只要將持久層抽象出來稀蟋,即可減少測試的困難性,同時適用充血模型畢竟帶來了不少開發(fā)上的便利性呐赡,除了依賴持久層這一點糊治,擁有更多好處的充血模型仍然值得選擇。最后罚舱,誰也沒能說服誰井辜,關(guān)于貧血模型和充血模型的選擇绎谦,更多的要靠具體的業(yè)務(wù)場景來決定,并不能說哪一種更比哪一種好粥脚。設(shè)計模式這種東西不是向來都沒有什么定論么窃肠。
我個人則傾向使用充血模型,因為充血模型更加像一個設(shè)計完善的系統(tǒng)架構(gòu)刷允,好在計算機世界里有很多的 IOC 和 DI 框架冤留,唯一的缺陷依賴持久層可以通過各種變通的方法繞過,隨著技術(shù)的進步树灶,一些缺陷也會被慢慢解決纤怒。我的思路是這樣的:先將持久層抽象為接口,然后通過服務(wù)層將持久層注入到領(lǐng)域模型中天通,這樣領(lǐng)域模型僅僅會依賴于持久層的接口泊窘。而這個接口,可以利用現(xiàn)有框架的技術(shù)進行抽象像寒。