前言
領域驅動設計是一個開放的設計方法體系,目的是對軟件所涉及到的領域進行建模,以應對系統規模過大時引起的軟件復雜性的問題,本文將介紹領域驅動的相關概念。
一.軟件復雜度的根源
1.業務復雜度(軟件的規模)
軟件的需求決定了系統的規模。當需求呈現線性增長的趨勢時,為了實現這些功能,軟件規模也會以近似的速度增長。由於需求不可能做到完全獨立,導致出現相互影響相互依賴的關系,修改一處就會牽一發而動全身。就好似城市的一條道路因為施工需要臨時關閉,此路不通,通行的車輛只能改道繞行,這又導致了其他原本已經飽和的道路,因為涌入更多車輛,超出道路的負載從而變得更加擁堵,這種擁堵現象又會順勢向這些道路的其他分叉道路蔓延,形成一種輻射效應的擁堵現象。
2.技術復雜度(軟件的結構)
結構之所以變得復雜,在多數情況下還是因為系統的質量屬性決定的。例如,我們需要滿足高性能、高並發的需求,就需要考慮在系統中引入緩存、並行處理、CDN、異步消息以及支持分區的可伸縮結構。倘若我們需要支持對海量數據的高效分析,就得考慮這些海量數據該如何分布存儲,並如何有效地利用各個節點的內存與 CPU 資源執行運算。
從系統結構的視角看,單體架構一定比微服務架構更簡單。
3.人為的因素
不存在一致性、不存在風格、也沒有統一的概念能夠將不同的部分組織在一起。缺少必要的注釋,沒有字段說明和數據字典, 意大利面條式的代碼,缺乏統一編碼風格,導致錯綜復雜和不可維護的程序。
4.需求引起的軟件復雜度
需求分為業務需求與質量屬性需求,因而需求引起的復雜度可以分為兩個方面:技術復雜度與業務復雜度。
技術復雜度來自需求的質量屬性,諸如安全、高性能、高並發、高可用性等需求,為軟件設計帶來了極大的挑戰,讓人痛苦的是這些因素彼此之間可能又互相矛盾、互相影響。例如,系統安全性要求對訪問進行控制,無論是增加防火牆,還是對傳遞的消息進行加密,又或者對訪問請求進行認證和授權等,都需要為整個系統架構添加額外的間接層,這不可避免會對訪問的低延遲產生影響,拖慢了系統的整體性能。又例如,為了滿足系統的高並發訪問,我們需要對應用服務進行物理分解,通過橫向增加更多的機器來分散訪問負載;同時,還可以將一個同步的訪問請求拆分為多級步驟的異步請求,再通過引入消息中間件對這些請求進行整合和分散處理。這種分離一方面增加了系統架構的復雜性,另一方面也因為引入了更多的資源,使得系統的高可用面臨挑戰,並增加了維護數據一致性的難度。
業務復雜度對應了客戶的業務需求,因而這種復雜度往往會隨着需求規模的增大而增加。由於需求不可能做到完全獨立,一旦規模擴大到一定程度,不僅產生了功能數量的增加,還會因為功能互相之間的依賴與影響使得這種復雜度產生疊加,進而影響到整個系統的質量屬性,比如系統的可維護性與可擴展性。在考慮系統的業務需求時,還會因為溝通不暢、客戶需求不清晰等多種局外因素而帶來的需求變更和修改。如果不能很好地控制這種變更,則可能會因為多次修改而導致業務邏輯糾纏不清,系統可能開始慢慢腐爛而變得不可維護,最終形成一種如 Brian Foote 和 Joseph Yoder 所說的“大泥球”系統。
以電商系統的促銷規則為例。針對不同類型的顧客與產品,商家會提供不同的促銷力度;促銷的形式多種多樣,包括贈送積分、紅包、優惠券、禮品;促銷的周期需要支持定制,既可以是特定的日期,如雙十一促銷,也可以是節假日的固定促銷模式。如果我們在設計時沒有充分考慮促銷規則的復雜度,並處理好促銷規則與商品、顧客、賣家與支付乃至於物流、倉儲之間的關系,開發過程則會變得踉踉蹌蹌、舉步維艱。
技術復雜度與業務復雜度並非完全獨立,二者混合在一起產生的化合作用更讓系統的復雜度變得不可預期,難以掌控。
二.控制軟件復雜度的原則
1.保持結構的清晰與一致.
2.分而治之、控制規模
3.擁抱變化(變化對軟件系統帶來的影響可以說是無解)
除了在開發過程中,我們應盡可能做到敏捷與快速迭代,以此來抵消變化帶來的影響;在架構設計層面,我們還可以分析哪些架構質量屬性與變化有關,這些質量屬性包括:
- 可進化性(Evolvability):有句話這么說的好的架構是進化來的,不是設計出來的。
- 可擴展性(Extensibility)
三.傳統開發設計的模式
1.傳統的設計分層結構:
Model層:
包含數據對象,是service操縱的對象,model層中的對象被建模成業務對象,這些對象是對DB中表的映射,一個表對應一個model,表中的字段就對應成model對象的屬性,然后在加上get() / set()方法,但是並沒有包含這個對象的業務上的行為,不知道它會做什么,這樣就是一個很典型的貧血模式。
Dao層(數據訪問層,DTO對象:數據傳輸對象):
Dao層主要是和數據庫打交道,做數據持久化的工作,也包括一些數據過濾,為model層服務的,比如php里面的mysqli和pdo。
Service層:
公開一些接口給外部服務調用的,放置所有的服務類,它會調用Dao層去處理數據(獲取設置數據)。
展現層(UI):
前端的一些業務邏輯展現,使用各種UI框架,如Layzui,smarty,twing等模版js框架去渲染頁面。
Controller層:
層負責具體的業務模塊流程的控制,在這層調用可以調用service層的接口來控制業務流程,也可以訪問model層獲取數據。
現在主流的php框架都是按照這樣的分層去設計和開發,thinkphp,laveral,ci。
2.傳統的設計方式和開發框架及其問題
1.由於設計或者編碼的不當,核心業務邏輯容易散布在各處。
由於業務邏輯混散在各處,帶來的麻煩維護很困難,有可能在model層 service層做一些業務方面的東西,或者在action里面寫一些業務相關的代碼,比如有些業務寫在展現層,那么就要去改展現層里面的代碼,寫在service層就要去改service的邏輯。當想要了解這里業務邏輯的時候得看下上下文,翻閱很多類,需要追代碼各個文件去看才能大概的明白。
2.過度耦合
業務初期,我們的功能大都非常簡單,普通的CRUD就能滿足,此時系統是清晰的。隨着迭代的不斷演化,業務邏輯變得越來越復雜,我們的系統也越來越冗雜。模塊彼此關聯,誰都很難說清模塊的具體功能意圖是啥。修改一個功能時,往往光回溯該功能需要的修改點就需要很長時間,更別提修改帶來的不可預知的影響面。
下圖是一個常見的系統耦合病例。
訂單服務接口中提供了查詢、創建訂單相關的接口,也提供了訂單評價、支付、保險的接口。同時我們的表也是一個訂單大表,包含了非常多字段。在我們維護代碼時,牽一發而動全身,很可能只是想改下評價相關的功能,卻影響到了創單核心路徑。雖然我們可以通過測試保證功能完備性,但當我們在訂單領域有大量需求同時並行開發時,改動重疊、惡性循環、疲於奔命修改各種問題。
上述問題,歸根到底在於系統架構不清晰,划分出來的模塊內聚度低、高耦合。
問題:既然架構不清晰重構是否可以解決這些問題?
可以,但是並不能解決根本問題。一般重構都是通過在單獨的類及方法級別上做一系列小步重構來完成,封裝一些常用的操作,提煉出通用的代碼片段。所以我們可以很容易重構出一個獨立的類來放某些通用的邏輯,但是你會發現你很難給它一個業務上的含義,只能給予一個技術維度描繪的含義。這會帶來什么問題呢?新來的同事並不總是知道對通用邏輯的改動或獲取來自該類。顯然,制定項目規范並不是好的方法,隨着業務的變化在不久的將來重構還會一直繼續下去。
3.貧血模式,基於數據表的設計,數據驅動(Data-Driven),所有的開發都是圍繞數據表來進行的。
四.貧血模型
貧血領域對象(Anemic Domain Object)是指僅用作數據的載體,而沒有行為和動作的領域對象,只有get和set方法,或者包含少量的CRUD方法,所有的業務邏輯都不包含在內。
貧血模型其實是違背了oop模式,對象有什么反應的就是屬性,對象會做什么,反應在類里面對應的就是方法,傳統開發中很明顯看不到能做什么,不會有業務行為,get / set方式只是外部獲取屬性值的載體而已。
數據庫有什么,模型才會反應什么,這樣迫使我們先去設計數據庫,這就是傳統的開發思路,這就是基於數據庫表的設計,導致的問題就是表中會有一些重復的多余的字段,在設計的時候也會依賴於的特定的數據庫(因為是先去設計數據庫),所以好的系統是不應該依賴於特定的數據庫,更不應該依賴於特定數據庫的存儲過程,存儲函數,觸發器等,做到數據庫無關性,當做到數據遷移等時候很方便很平滑。所以應該先去設計業務對象,就是對對象建模然后再去反向設計數據庫。
缺點:
1.溝通困難,開發人員和業務人員交流語言不統一,開發用技術語言和業務溝通,而業務人員不了解技術,就是交流障礙。
2.業務邏輯不能重用,因為業務散在各個層,業務各個方法互相調用,你不知道調用哪個方法,業務后期的查找維護也比較困難,業務邏輯也會和應用邏輯的混合,業務邏輯反應的是需求,應用邏輯是和系統相關,比如業務要查詢什么數據,查詢的過程是業務,而如何展現在ui上是應用。
3.傳統的開發是和特點的技術耦合的,如果想把業務脫離出來,去更換某種技術就很困難。
4.適應未來的變化就很有問題。
優點:
這種貧血模型的傳統開發也是當前我們最常用的方法,開發速度快,開發人員容易掌握。
// 貧血模型下的實現 public class User{ private $id; private $name; ... // 省略get/set方法 } public class UserManagerService{ public function save(User user){ // 持久化操作.... } } // 保存用戶的操作可能是這樣 $userManagerService::getInstance()->save(user);
我的業務邏輯都是寫在userManagerService中的,User只是個數據載體,沒有任何行為。簡單的業務系統采用這種貧血模型和過程化設計是沒有問題的,但在業務邏輯復雜了,業務邏輯、狀態會散落到在大量方法中,原本的代碼意圖會漸漸不明確,我們將這種情況稱為貧血模型或者是由貧血症引起的失憶症。
// 充血模型下的實現 public class User{ private $id; private $name; ... // 省略get/set方法
//用戶信息保存 public function save(User user){ // 持久化操作.... } } // 保存用戶的操作可能是這樣 $User::getInstance()->save(user);
更好的是采用領域模型的開發方式,將數據和行為封裝在一起,並與現實世界中的業務對象相映射。各類具備明確的職責划分,將領域邏輯分散到領域對象中。繼續舉我們上述的例子,用戶保存信息就應當放到User類中。
五.為什么選擇DDD
既然上述傳統開發和貧血模式有這些問題,那么有沒有什么方法來解決這些問題?
解決思路
1.思想理論:基於領域驅動設計(DDD能應對復雜性與快速變化)。
2.技術實現:
1).從技術維度實現分層:遵循分層架構模式,能夠在每層關注自己的事情,比如領域層關注業務邏輯的事情,倉儲關注持久化數據的事情,應用服務層關注用例的事情,接口層關注暴露給前端的事情。通過‘開發主機服務’(REST服務是其中的一種)、消息模式、事件驅動 等架構風格實現.
2).業務維度:通過將大系統划分層多個上下文,關注點放在domain上,將業務領域限定在同一上下文中,可以讓不同團隊和不同人只關注當前上下文的開發。降低上下文之間的依賴,業務核心與特定的技術隔離開來,不依賴任何一個技術框架。
六.理解DDD概念
DDD的全稱為Domain-driven Design,即領域驅動設計。是一種思維方式和概念,可以應用在處理復雜業務的軟件項目中,加快項目的交付速度。下面我從領域、問題域、領域模型、設計、驅動這幾個詞語的含義和聯系的角度去闡述DDD是如何融入到我們平時的軟件開發初期階段的。要理解什么是領域驅動設計,首先要理解什么是領域,什么是設計,還有驅動是什么意思,什么驅動什么。
1.什么是領域
領域代表的是某個范圍,假如現在要做一個系統,這個系統有一些要實現的功能。那么這個系統肯定屬於某個特定的領域,比如論壇是一個領域,只要你想做一個論壇,那這個論壇的核心業務是確定的,比如都有用戶發帖、回帖等核心基本功能。比如電商系統,這種都屬於網上電商領域,只要是這個領域的系統,那都有商品瀏覽、購物車、下單、減庫存、付款交易,物流等核心環節,或者一個支付平台等。所以,同一個領域的系統都具有相同的核心業務,因為他們要解決的問題的本質是類似的。
因此,我們可以推斷出,一個領域本質上可以理解為就是一個問題域,只要是同一個領域,那問題域就相同。所以,只要我們確定了系統所屬的領域,那這個系統的核心業務,即要解決的關鍵問題、問題的范圍邊界就基本確定了。通常我們說,要成為一個領域的專家,必須要在這個領域深入研究很多年才行。因為只有你研究了很多年,你才會遇到非常多的該領域的問題,同時你解決這個領域中的問題的經驗也非常豐富。領域專家的重要性對於設計良好的領域驅動設計是很重要的。在開發前理解領域知識是基礎,也很重要,因為一個系統要做成什么樣,里面包含哪些業務規則,核心業務關注點是什么,就要求對這個領域內的一切業務相關的知識都非常了解,如果開發一個陌生的系統,比如航空管理軟件,讓一個只會開發電商的程序員去寫,是完全不知道從哪開始下手。
在日常開發中,我們通常會將一個大型的軟件系統拆分成若干個子系統。這種划分有可能是基於架構方面的考慮,也有可能是基於基礎設施的。但是在DDD中,我們對系統的划分是基於領域的,也即是基於業務的,領域的划分,一個大的領域可以划分成多個小的領域,也就是子域。
領域及子域的划分是如何進行的,如何去限定的,這個就得需要限界上下文和上下文映射圖,下面待會說。
2.什么是設計
DDD中的設計主要指領域模型的設計。為什么是領域模型的設計而不是架構設計或其他的什么設計呢?因為DDD是一種基於模型驅動開發的軟件開發思想,強調領域模型是整個系統的核心,領域模型也是整個系統的核心價值所在。每一個領域,都有一個對應的領域模型,領域模型能夠很好的幫我們解決復雜的業務問題。
從領域和代碼實現的角度來理解,領域模型綁定了領域和代碼實現,確保了最終的代碼實現就一定是解決了領域中的核心問題的。因為:1)領域驅動領域模型設計;2)領域模型驅動代碼實現。我們只要保證領域模型的設計是正確的,就能確定領域模型可以解決領域中的核心問題;同理,我們只要保證代碼實現是嚴格按照領域模型的意圖來落地的,那就能保證最后出來的代碼能夠解決領域的核心問題的。這個思路,和傳統的分析、設計、編碼這幾個階段被割裂(並且每個階段的產物也不同)的軟件開發方法學形成鮮明的對比。
3.什么是驅動
上面其實已經提到了,就是:1)領域驅動領域模型設計;2)領域模型驅動代碼實現。這個就和我們傳統的數據庫驅動開發的思路形成對比了。DDD中,我們總是以領域為邊界,分析領域中的核心問題(核心關注點),然后設計對應的領域模型,再通過領域模型驅動代碼實現。而像數據庫設計、持久化技術等這些都不是DDD的核心,而是外圍的東西。
領域驅動設計第一步最關鍵就是應該盡量先把領域模型想清楚,然后再開始動手編碼,這樣的系統后期才會很好維護。但是,很多項目(尤其是互聯網項目,為了趕工)都是一開始模型沒想清楚,一上來就開始建表寫代碼,代碼寫的非常冗余,完全是過程是的思考方式,最后導致系統非常難以維護。而且更糟糕的是,前期的領域模型設計的不好,不夠抽象,如果你的系統會長期需要維護和適應業務變化,那后面你一定會遇到各種問題維護上的困難,比如數據結構設計不合理,代碼到處冗余,改BUG到處引入新的BUG,新人對這種代碼上手困難等。而那時如果你再想重構模型,那要付出的代價會比一開始重新開發還要大,因為你還要考慮兼容歷史的數據,數據遷移,如何平滑發布等各種頭疼的問題。
所以通過建立領域模型來解決領域中的核心問題,這就是模型驅動的思想。
七.DDD核心組件
1.通用語言
形成統一的領域術語,尤其是基於模型的語言概念,是溝通能夠達成一致的前提。尤其是開發人員與領域專家之間,他們掌握的知識存在巨大的差異,尤其是專業性很強的業務,比如金融系統,醫療系統,它們的術語都很專業。而善於技術的開發人員關注於數據庫、通信機制、集成方式與架構體系,而精通業務的領域專家對這些卻一竅不通,但他們在講解業務知識時,非常自然,這些對於開發人員來說,卻成了天書,這種交流就好似使用兩種不同語言的外國人在交談。
使用統一語言可以幫助我們將參與討論的客戶、領域專家與開發團隊拉到同一個維度空間進行討論,若沒有達成這種一致性,那就是雞同鴨講,毫無溝通效率,相反還可能造成誤解。因此,在溝通需求時,團隊中的每個人都應使用統一語言進行交流。
一旦確定了統一語言,無論是與領域專家的討論,還是最終的實現代碼,都可以通過使用相同的術語,清晰准確地定義領域知識。重要的是,當我們建立了符合整個團隊皆認同的一套統一語言后,就可以在此基礎上尋找正確的領域概念,為建立領域模型提供重要參考。
2.界限上下文(Bounded Contexts):
界限上下文是DDD中的一個核心模式,這種模式是幫助我們剝離開復雜的應用程序,將他們隔離開,形成不同的上下文邊界。不同的模塊有着不同的上下文,且能獨立調用,而各自的模塊可以有自己的持久化的,互不干擾。
在大型的應用程序中,不同的人對不同的的東西可能取相同的名字,這跟我們程序的類一樣,為何我們要在外面放一個namespace在外面,其實也是形成一個邊界。程序內部也是如此。例如,售樓部內的員工把商品房認為是產品(Product);但是在工程部,可能他們把刷灰和修理管道的服務叫做產品,為了消除這些歧義,可以定義一個邊界,分離開兩種情況,以免在系統內產生混淆。每個界限上下文根據特點,具體實現方式又不同,比如有些界限上下文基本沒有業務邏輯,就是增刪改查,則可以使用CRUD最簡單的模式;有些界限上線文有一定的業務邏輯,但對高並發、高性能沒要求,則可以使用經典DDD模式。有些界限上下文有一定的業務邏輯,而且有高性能要求,則可以使CQRS模式(命令查詢職責分離(Command Query Responsibility Segregation,簡稱CQRS))。
上面這張圖展示了界限上下文
舉個例子,比如電商系統中訂單模塊的上下文有商品,物流模塊上下文有貨物,庫存模塊上下文有存貨等等,這時候你會發現其實他們都是指的同一個東西,只不過在不同的上下文中被人為的賦予了不同的概念。這樣是不是就更好理解界限上下文了。
3.實體:
有業務生命周期,采用業務標識符進行跟蹤。比如一個訂單就是實體,訂單有生命周期的,而且有一個訂單號唯一的標識它自己,如果兩個訂單所有屬性值全部相同,但訂單號不同,也是不同的實體。
實體之間的關系:
1)關系越多,耦合越大。
2)找出整個業務期間的都依賴的關系,某些關系只是在對象創建的時候有意義,比如創建訂單的時候會查詢一下商品價格信息。
3)盡可能的簡化關系,避免雙向依賴關系。
4.值對象:
無業務生命周期,無業務標識符,通常用於描述實體。比如訂單的收貨地址、訂單支付的金額等就是值對象。
根據上下文的不同,一個值對象在一個界限上下文中上值對象,到了另一個界限上下文環境中會是實體,就是在不同的領域里屬性是不一樣的,具體還得根據上下文來看。
值對象是不可變(只讀),這樣線程安全,可以到處傳遞。
值對象最大的好處在於增加了代碼復用。
5.領域服務:
無狀態,有行為,服務本身也是對象,但它卻沒有屬性(只有行為),因此說是無狀態,通常負責協調多個領域對象的操作來完成一些功能。比如嘗試如何將信息轉化為領域模型,但並非所有的點我們都能用Model來涵蓋。對象應當有屬性,狀態和行為,但有時領域中有一些行為是無法映射到具體的對象中的,我們也不能強行將其放入在某一個模型對象中,而將其單獨作為一個方法又沒有地方,此時就需要服務。協調聚合之間的業務邏輯,並且完成用例,表示某種能力。
6.聚合:
聚合是一組相關的對象,它通過定義對象之間清晰的所屬關系和邊界來實現領域模型的內聚,並避免了錯綜復雜的難以維護的對象關系網的形成,我們把聚合看作是一個修改數據的單元,目的將這些對象作為一個單元(是業務的一個最小單元,持久化最小單元),每個聚合都有一個邊界和一個根,邊界定義了聚合里應該包含什么,根是聚合中唯一可以被外部飲用的元素,比如說不能直接繞過訂單實體去訪問訂單項,但在聚合邊界內部,可以互相引用。聚合根具有全局唯一標識,聚合根由倉儲負責持久化其生命周期,而實體只有在聚合內部有唯一局部標識,由聚合根負責其生命周期持久化。
通常將多個實體和值對象組合到一個聚合中來表達一個完整的概念,比如訂單實體、訂單明細實體、訂單金額值對象就代表一個完整的訂單概念,而且生命周期是相同的,並且需要統一持久化到數據庫中。
7.聚合根:
將聚合中表達總概念的實體做成聚合根,比如訂單實體就是聚合根,對聚合中所有實體的狀態變更必須經過聚合根,因為聚合根協調了整個聚合的邏輯,保證一致性。當然其他實體可以被外部直接臨時查詢調用。
8.倉儲:
用於對聚合進行持久化,通常為每個聚合根配備一個倉儲即可。倉儲能夠很好的解耦領域邏輯與數據庫。
9.工廠:
用於創建復雜的領域對象,能夠將領域對象復雜的創建過程保護起來,可以創建實體,值對象。在大型系統中,實體和聚合通常是很復雜的,這就導致了很難去通過構造器來創建對象,工廠就決解了這個問題,其實就是一種封裝,隱藏了復雜的創建細節。
10.上下文映射圖
如果我們將限界上下文理解為是對工作邊界的控制,則上下文之間的協作實則就是團隊之間的協作,高效的團隊協作應遵循“各司其職、權責分明”的原則。從組織層面看,需要預防一個團隊的“權力膨脹”,導致團隊的“勢力范圍”擴大到整個組織。從團隊層面,又需要避免自己的權力遭遇壓縮,導致自己的話語權越來越小,這中間就存在一個平衡問題。映射到領域驅動設計的術語,就是要在滿足合理分配職責的前提下,謹慎地確保每個限界上下文的粒度。職責的合理分配,可以更好地滿足團隊的自組織或者說自治,但不可能做到“萬事不求人”,全靠自己來做。如果什么事情都由這一個團隊完成,這個團隊也就成為無所不能的“上帝”團隊了。上下文映射展現了一種組織動態能力(Organizational Dynamic),它可以幫助我們識別出有礙項目進展的一些管理問題。”這也是我為何要在識別上下文的過程中引入項目經理這個角色的原因所在,因為在團隊協作層面,限界上下文與項目管理息息相關。
領域驅動設計根據團隊協作的方式與緊密程度,定義了五種團隊協作模式:
1)合作關系(Partnership):兩個上下文緊密合作的關系,互相聯系緊密。
2)共享內核(Shared Kernel):兩個上下文依賴部分共享的模型。
3)客戶方-供應方開發(Customer-Supplier Development):正常情況下,這是團隊合作中最為常見的合作模式,體現的是上游(供應方)與下游(客戶方)的合作關系。這種合作需要兩個團隊共同協商。
4)遵奉者(Conformist):下游限界上下文對上游限界上下文模型的追隨,做出遵奉模型決策的前提是需要明確這兩個上下文的統一語言是否存在一致性,因為限界上下文的邊界本身就是為了維護這種一致性而存在的。
5)分離方式(Separate Ways):在典型的電商網站中,支付上下文與商品上下文之間就沒有任何關系,二者是“分離方式”的體現。
八.DDD系統的分層架構
分層就是將具有不同職責的組件分離開來,組成一套層內部高聚合,層與層之間低耦合的軟件系統,領域驅動設計的討論同樣也是建立在層模式的基礎上的,但與傳統的分層架構相比,它更注重領域架構和技術架構的分離。
領域驅動設計將軟件系統分為四層:基礎結構層、領域層、應用層和表現層。與上述的三層相比,數據訪問層已經不在了,它被移到基礎設施層了,這些是屬於外圍的,不是核心。
從上圖還可以看到,表現層與應用層之間是通過數據傳輸對象(DTO)進行交互的,數據傳輸對象是沒有行為的POCO對象,它的目的只是為了對領域對象進行數據封裝,實現層與層之間的數據傳遞。為何不能直接將領域對象用於數據傳遞?因為領域對象更注重領域,而DTO更注重數據。不僅如此,由於“富領域模型”的特點,這樣做會直接將領域對象的行為暴露給表現層。
領域層是業務的核心,所有的業務都是在領域層。應用層是在領域層之上,為ui服務,它是響應ui的請求去領域層調用相應的服務,把結果返回給ui,這里面包括一些事務,分頁等和業務無關的,所以這些和業務無關都會放在應用層。基礎設施層是服務領域層的。
架構風格
針對DDD的架構設計,《實現領域驅動設計》書中提到了幾種架構風格:六邊形架構、REST架構、CQRS、事件驅動等。在實際使用中,落地的架構並非是純粹其中的一種,而很有可能戶將上述幾種架構風格結合起來實現。
所謂的六邊形架構,其實是分層架構的擴展,原來的分層架構通常是上下分層的,比如常見的MVC模式,上層是對外的服務接口,下層是對接存儲層或者是集成第三方服務,中層是業務邏輯層。我們跳出分層的概念,會發現上面層和下面層其實都是端口+適配器的實現,上面層開放http/tcp端口,采用rest/soap/mq協議等對外提供服務,同時提供對應協議的適配器;下層也是端口+適配器,只不過應用程序這時候變成了調用者,第三方服務或者存儲層提供端口和服務,應用程序本身實現適配功能。
領域驅動設計(Domain Driven Design)有一個官方的sample工程,名為DDDSample,官網:http://dddsample.sourceforge.net/,該工程給出了一種實踐領域驅動設計的參考架構。下圖就是它的代碼結構。
各個目錄含義:Infrastructure(基礎實施層),Domain(領域層),Application(應用層),Interfaces(表示層,也叫用戶界面層或是接口層),config(各種配置)
領域驅動設計過程中使用的模式
九.領域驅動總結
DDD與數據庫設計不同:
1.領域驅動設計是一種面向對象的設計,先建模再去設計數據庫。
2.領域驅動設計主要是基於現實業務中的模型,更加貼近真實業務,不僅僅是一種技術的實現。
3.領域驅動設計出來的產品---領域對象(Domain Object),是一個充血模型,不但包含業務對象的屬性,也包含業務對象的方法和行為,更加符合oo原則。
4.領域驅動設計並不包含數據庫具體設計,而是和領域專家一起,采用統一的語言分析領域對象的屬性,業務方法,以及領域之間的關系,並為之建模。
5.領域驅動設計能減少溝通的成本。
DDD的特點:
1.統一語言,業務 產品 技術交流都不會設計到具體的技術方面,主要是對核心業務的建模,不會先考慮數據表的設計,先考慮建模。
2.專有的領域層,領域層除了業務之外不設計軟件架構,等底層技術。
3.領域層代碼就是業務文檔,看到領域層代碼就能看到業務 的核心,就是從對象中不僅僅看到屬性還能可以看到業務。
DDD的一些問題
1.為什么DDD可以應對復雜性?
答:就是分而自治思想,比如說一個系統幾百張表,不可能一下子弄清楚,但是可以按業務,模塊去划分,DDD里面叫做界限上下文,和模塊(但是提出了更多概念,比如聚合,一個模塊有可能還是很大。)類似,划分成一個個領域,而領域模型有清晰的邊界,同時DDD重構了設計模式 架構模式 它里面也引入了ioc,工廠模式 策略模式 只是在更高層次上的應用。
2.為什么可以快速應對變化?
當問題空間出現變化的時候,我們可以快速的找到領域模型。領域層是可以很容易將業務模塊拿出來重用的。
何時考慮使用領域驅動設計?
1.如果系統只是簡單的curd,沒有很復雜的業務邏輯,不需要領域驅動設計,反而回增加復雜性。
2.如果你的應用多於用例場景,你的系統可能會逐漸成為一個大泥球(混雜在一起的上下文關系,邊界不清晰,代碼混亂)。如果你確定你的系統將會更復雜,你應該使用領域驅動設計來處理這個復雜性。
使用領域驅動的難點
應用領域驅動設計並沒那么簡單容易,這需要花費時間和精力去了解業務領域、術語、調查、和領域專家一起合作去划分如何去划分領域,划分好邊界並建模,去業務進行抽象,這也是DDD的最重要的地方。
十.一些相關的擴展閱讀
CQRS架構:
核心思想是將應用程序的查詢部分和命令部分完全分離,這兩部分可以用完全不同的模型和技術去實現。比如命令部分可以通過領域驅動設計來實現;查詢部分可以直接用最快的非面向對象的方式去實現,比如用SQL。這樣的思想有很多好處:
1) 實現命令部分的領域模型不用經常為了領域對象可能會被如何查詢而做一些折中處理;
2) 由於命令和查詢是完全分離的,所以這兩部分可以用不同的技術架構實現,包括數據庫設計都可以分開設計,每一部分可以充分發揮其長處;
3) 高性能,命令端因為沒有返回值,可以像消息隊列一樣接受命令,放在隊列中,慢慢處理;處理完后,可以通過異步的方式通知查詢端,這樣查詢端可以做數據同步的處理。
事件溯源(Event Sourcing):
基於DDD的設計,對於聚合,不保存聚合的當前狀態,而是保存對象上所發生的每個事件。當要重建一個聚合對象時,可以通過回溯這些事件(即讓這些事件重新發生)來讓對象恢復到某個特定的狀態;因為有時一個聚合可能會發生很多事件,所以如果每次要在重建對象時都從頭回溯事件,會導致性能低下,所以我們會在一定時候為聚合創建一個快照。這樣,我們就可以基於某個快照開始創建聚合對象了。
DCI架構:
DCI架構強調,軟件應該真實的模擬現實生活中對象的交互方式,代碼應該准確朴實的反映用戶的心智模型。在DCI中有:數據模型、角色模型、以及上下文這三個概念。數據模型表示程序的結構,目前我們所理解的DDD中的領域模型可以很好的表示數據模型;角色模型表示數據如何交互,一個角色定義了某個“身份”所具有的交互行為;上下文對應業務場景,用於實現業務用例,注意是業務用例而不是系統用例,業務用例只與業務相關;軟件運行時,根據用戶的操作,系統創建相應的場景,並把相關的數據對象作為場景參與者傳遞給場景,然后場景知道該為每個對象賦予什么角色,當對象被賦予某個角色后就真正成為有交互能力的對象,然后與其他對象進行交互;這個過程與現實生活中我們所理解的對象是一致的;
DCI的這種思想與DDD中的領域服務所做的事情是一樣的,但實現的角度有些不同。DDD中的領域服務被創建的出發點是當一些職責不太適合放在任何一個領域對象上時,這個職責往往對應領域中的某個活動或轉換過程,此時我們應該考慮將其放在一個服務中。比如資金轉帳的例子,我們應該提供一個資金轉帳的服務,用來對應領域中的資金轉帳這個領域概念。但是領域服務內部做的事情是協調多個領域對象完成一件事情。因此,在DDD中的領域服務在協調領域對象做事情時,領域對象往往是處於一個被動的地位,領域服務通知每個對象要求其做自己能做的事情,這樣就行了。這個過程中我們似乎看不到對象之間交互的意思,因為整個過程都是由領域服務以面向過程的思維去實現了。而DCI則通用引入角色,賦予角色以交互能力,然后讓角色之間進行交互,從而可以讓我們看到對象與對象之間交互的過程。但前提是,對象之間確實是在交互。因為現實生活中並不是所有的對象在做交互,比如有A、B、C三個對象,A通知B做事情,A通知C做事情,此時可以認為A和B,A和C之間是在交互,但是B和C之間沒有交互。所以我們需要分清這種情況。資金轉帳的例子,A相當於轉帳服務,B相當於帳號1,C相當於帳號2。因此,資金轉帳這個業務場景,用領域服務比較自然。有人認為DCI可以替換DDD中的領域服務,我持懷疑態度。
四色原型分析模式:
1) 時刻-時間段原型(Moment-Interval Archetype)
表示在某個時刻或某一段時間內發生的某個活動。使用粉紅色表示,簡寫為MI。
2) 參與方-地點-物品原型(Part-Place-Thing Archetype)
表示參與某個活動的人或物,地點則是活動的發生地。使用綠色表示。簡寫為PPT。
3) 描述原型(Description Archetype)
表示對PPT的本質描述。它不是PPT的分類!Description是從PPT抽象出來的不變的共性的屬性的集合。使用藍色表示,簡寫為DESC。
舉個例子,有一個人叫張三,如果某個外星人問你張三是什么?你會怎么說?可能會說,張三是個人,但是外星人不知道“人”是什么。然后你會怎么辦?你就會說:張三是個由一個頭、兩只手、兩只腳,以及一個身體組成的客觀存在。雖然這時外星人仍然不知道人是什么,但我已經可以借用這個例子向大家說明什么是“Description”了。在這個例子中,張三就是一個PPT,而“由一個頭、兩只手、兩只腳,以及一個身體組成的客觀存在”就是對張三的Description,頭、手、腳、身體則是人的本質的不變的共性的屬性的集合。但我們人類比較聰明,很會抽象總結和命名,已經把這個Description用一個字來代替了,那就是“人”。所以就有所謂的張三是人的說法。
4) 角色原型(Role Archetype)
角色就是我們平時所理解的“身份”。使用黃色表示,簡寫為Role。為什么會有角色這個概念?因為有些活動,只允許具有特定角色(身份)的PPT(參與者)才能參與該活動。比如一個人只有具有教師的角色才能上課(一種活動);一個人只有是一個合法公民才能參與選舉和被選舉;但是有些活動也是不需要角色的,比如一個人不需要具備任何角色就可以睡覺(一種活動)。當然,其實說人不需要角色就能睡覺也是錯誤的,錯在哪里?因為我們可以這樣理解:一個客觀存在只要具有“人”的角色就能睡覺,其實這時候,我們已經把DESC當作角色來看待了。所以,其實角色這個概念是非常廣的,不能用我們平時所理解的狹義的“身份”來理解,因為“教師”、“合法公民”、“人”都可以被作為角色來看待。因此,應該這樣說:任何一個活動,都需要具有一定角色的參與者才能參與。
用一句話來概括四色原型就是:一個什么什么樣的人或組織或物品以某種角色在某個時刻或某段時間內參與某個活動。 其中“什么什么樣的”就是DESC,“人或組織或物品”就是PPT,“角色”就是Role,而”某個時刻或某段時間內的某個活動"就是MI。
以上這些東西如果在學習了DDD之后再去學習會對DDD有更深入的了解,但我覺得DDD相對比較基礎,如果我們在已經了解了DDD的基礎之上再去學習這些東西會更加有效和容易掌握。
十一 .其他的軟件開發模式
TDD:測試驅動開發(Test-Driven Development)
BDD:行為驅動開發(Behavior Driven Development)
ATDD:驗收測試驅動開發(Acceptance Test Driven Development)