閱讀此文須要必定的DDD基礎,若是你是第一次接觸DDD讀者,建議先去閱讀一些DDD相關的書籍或者文章以後再來閱讀本文。html
背景
自從我在團隊中推行DDD以來,咱們團隊經歷了一系列的磨難——先是把核心項目重構,接着又在一些衍生項目中嘗試全面落地DDD, 最終探索了一些經驗出來,特此記錄一下。java
本文采用語言無關的角度陳述,不管你是Java或者c#的開發同窗相信均可以無障礙閱讀。git
請注意本文並非介紹如何實現DDD,由於這個話題實在太大了。github
此次的主題是分享一些咱們團隊在實踐DDD過程當中碰到問題和如何克服它們,以及介紹一下咱們所使用的架構體系。docker
先說說爲何標題限定在「單體應用」這個範圍內,數據庫
- 咱們團隊此次實踐的應用全是單體應用
-
若是是分佈式的應用,那麼拆分限界上下文(BoundedContext)的最佳實踐是什麼?固然是微服務!c#
我相信如今討論微服務的文章確定不在少數,微軟也專門出過容器化微服務架構的電子書。傳送門點我。後端
資源如此豐富,固然就不須要我多此一舉了。設計模式
領域模型
領域模型的分析能夠說是DDD當中最爲核心的部分,由於你整個系統的業務邏輯代碼都是基於領域模型而構成的。網絡
而要將業務邏輯轉換成領域模型除了對業務的熟悉外還須要極高的抽象能力,因此通常須要業務專家和建模專家共同完成。
怎樣提煉一個好的領域模型是一個很是大的話題,推薦你閱讀如下書籍:
- 《領域驅動設計:軟件核心複雜性應對之道》Eric Evans
- 《實現領域驅動設計》Vaughn Vernon
- 《領域驅動設計與模式實戰》Jimmy Nilsson
另外微軟架構電子書上還有推薦其餘幾本DDD的書籍,遺憾的是,JD和TB都沒搜到。
在團隊剛開始分析領域模型時,對全部相關者都是一個極大的挑戰,我這裏分享幾點經驗幫助團隊更好地度過這段時期:
- 不要想着可以一次提煉出完美的領域模型(除非團隊中有着經驗豐富的DDD實踐者),一般來講,咱們會在會議上決定一個粗略的模型,而後在開發過程當中你會發現有一些不天然的地方,好比某些上下文頻繁地與其餘上文通訊,或者某個實體的行爲不是很恰當,這個時候再去修正領域模型,這樣演進式的過程能夠大大下降大家在初期的壓力。
- 若是你的團隊總體能力不足以支撐領域模型的推行,或者他們在初期的配合度不高時,你能夠選擇把你的項目中業務邏輯最爲複雜的部分使用弱化的領域模型拆解,好比僅使用充血模型和領域服務,這樣至少你能夠對最爲複雜的部分引入一些DDD戰術模式或設計模式。
- 就算你的團隊能力夠了,但大部分人都沒有DDD的經驗的話,我也建議先只引入部分模式(好比只引入實體,值對象和倉儲這類比較容易理解的模式)來提升團隊的敏感度以後再採用完整的領域模型。
- 領域模型會對查詢帶來必定的複雜性,這種時候你能夠採用CQRS來分離Query和Command,只有在Cammand的時候你才須要發揮領域模型的威力,至於Query,SQL語句顯然是更好選擇。
基礎架構
瞭解DDD的同窗都應該知道,DDD當中最爲重要的部分就是限界上下文(BoundedContext),在領域模型中咱們區分好了上下文以後,下一步就是選擇一種技術手段來確保每一個上下都是低耦合高內聚且自治的。
在分佈式應用中,多數設計者和包括微軟架構的電子書都會推薦使用一個上下文對應一個微服務的方式來實現(確實微服務和上下文的設計需求不謀而合)。
但單體應用該怎麼辦呢?
有同窗說,咱們能夠經過命名空間來隔離它們啊。
不錯,咱們能夠這樣作,可是有如下幾個缺點
- 在使用IDE的智能引用時,你得確認你引用的實體到底是位於當前上下文以內仍是以外。
- 會致使你的項目結構層次過深,不便於查看。(至於過深的標準是多少,看我的了,對於我來講,5層是能夠接受的上限,理想是控制在4層之內)
- 不便於向微服務架構遷移
因此咱們選擇了使用程序集(java是使用jar包)的方式來隔離每一個上下文,這樣作克服了以上的缺點,但卻帶來了新的問題:動態加載這些上下文。
不過這種程度的問題比起帶來的收益幾乎能夠忽視。
咱們團隊使用一個基礎平臺來動態加載這些上下文,
咱們採用了 Abp 框架提供的插件功能來實現,若是你也是.net 的使用者,也能夠採用 Abp 來構建這個應用。
固然本身寫一個動態加載功能也並不困難。
基礎架構以下圖所示:
但是咱們的平臺要承擔不少功能,好比開放RESTful的API與Webservice(爲了兼容老的接口), 同時還要提供受權(使用了基於Oauth2.0協議的三種模式)、數據庫初始化、處理請求上下文等等,我就不一一列出來了。
咱們但願BC(BoundedContext,後文都會簡寫爲BC)裏不須要關注網絡層面的東西而只聚焦於應用,因此不少通用的事情都由平臺來承擔, 並且有時還會有一些交互,好比在驗證權限時你得跟用戶權限上下文通訊。
在這種前提下,咱們抽出了一個用於鏈接平臺和這些BC的交互層,咱們把它稱做——橋接組件(BrigeComponent),它負責聯繫起平臺和這些BC,外加上一些共用的基礎設施,咱們的架構圖變成了這樣:
這樣一來,你能夠把每一個BC都看成微服務來處理,每個BC內的分層結構你能夠按你的喜歡的來,若是你喜歡標準的三層架構(UI + BLL + DAL),你能夠將BC設計那樣。
你甚至能夠每一個BC都採用不一樣的風格,好比一個採用N層架構,而另外一個採用事件驅動架構(EDA)。
這裏咱們的BC都用了相同的DDD推薦分層架構(這裏省去了 表現層, 由於現代應用大多都是先後端分離了的),以下圖所示:
好了,如今總體架構和領域模型都已經肯定下來後,咱們開始編碼了,但很快咱們就遇到了阻礙。
「結算上下文須要訪問用戶權限上下文,它須要知道這個用戶的機構信息,我能夠直接引用嗎?」
「賬戶上下文這裏輸出的數據須要通用上下文提供一些有效性校驗,我能夠直接引用嗎?」
「我這裏也須要訪問通用上下文!」
……
好吧,若是咱們直接提供引用,會有如下問題:
- 因爲咱們採用了程序集分割上下文,因此相互引用是不被容許的。
- 就算克服了相互引用的問題,最終也會致使引用拓撲圖混亂不堪。
- 強耦合,這會直接影響到之後的拓展性。
在微服務中,爲了克服服務間的互相通訊問題,目前我瞭解的有兩類解決方案,
一是相似於ESB(企業服務總線)的中心化通訊模式,好比大名鼎鼎的SprinCloud。
二是如今微服務界炒得沸沸騰騰的ServiceMesh(服務網格),好比 Linkerd 和 Istio。
咱們項目選擇了前者,使用了相似於ESB中心化通訊方式來解決,簡單來講,你須要一個通訊中介者(Mediator)來負責BC之間的交互,結構圖以下:
若是你是 .Net 的開發者,請允許我給你安利一下咱們在項目中使用的,本身開發的組件——ServiceAnt,它目前只支持進程內的通訊,但不久後會開發分佈式的。
詳細狀況你能夠點擊上面的鏈接進去查看,也能夠查看我寫的 另外一篇博客 瞭解ServiceAnt是作什麼的,固然你也能夠選擇 Mediator 來實現這個通訊中間件。
Java的話,因爲經驗較少,沒有發現相似的項目,Mule ESB什麼的就跟 NServiceBus 同樣是重量級的組件,不適用咱們這樣的場景。
以上就是咱們用於實現DDD的基礎架構,基於這樣的架構咱們能夠很輕鬆地將現有應用向微服務拆分。
固然,上面的架構隱藏了不少細節,好比大量的基礎設施(Ioc,Aop, Logger, cache等等),
緣由之一是由於這些東西的設計都很常見,網上你隨便就能夠搜到相關設計的文章,
緣由之二是由於我不想這些細節影響到了讀者的關注點,我但願咱們能夠聚焦於如何實現DDD而不是系統的其餘部分。
其餘的一些話
在推行DDD過程當中,總會有一些成員會問我,DDD給咱們帶來的好處是什麼。
我總會不厭其煩地告訴他們,爲了下降系統的維護成本和更合理地去解決系統業務的複雜性。
但後來我漸漸發現,實現DDD自己就不是一件容易的事情,它會對項目引入新的複雜性,有時候你會發現你團隊花上大量時間去建模以後,在開發過程當中卻依然須要不斷修正模型。
這很容易讓整個團隊士氣變低,而且讓開發人員有挫敗感,這種時候我常常會懷疑DDD對咱們而言是否真的有價值。
不過堅持下去,在你使用DDD完成一到兩個項目以後,你會發現建模是一件很是有意思的事情——提煉業務並將其轉換爲一個無關技術的模型,這就跟搭積木同樣。
最後給全部但願經過DDD來改善項目,而且提高本身的同窗說如下兩點:
1,不要奢望光經過閱讀就能充分地理解DDD,你須要真正去實踐(固然,框架和架構設計也是同樣的,不要作象牙塔裏的架構師)
2,實踐的過程你總會遇見疑惑和挫折,好比徹底不知道如何拆分上下文,也不知道該如何使用那些戰術模式,這個時候再把那幾本書拿出來翻翻,你就會發出「啊,原來這種場景還能夠這樣處理」的感概。
那句話怎麼說來着,
The one trying to wear the crown must withstand the weight.