閱讀此文需要一定的DDD基礎,如果你是第一次接觸DDD讀者,建議先去閱讀一些DDD相關的書籍或者文章之后再來閱讀本文。
背景
自從我在團隊中推行DDD以來,我們團隊經歷了一系列的磨難——先是把核心項目重構,接着又在一些衍生項目中嘗試全面落地DDD, 最終探索了一些經驗出來,特此記錄一下。
本文采用語言無關的角度陳述,無論你是Java或者c#的開發同學相信都可以無障礙閱讀。
請注意本文並不是介紹如何實現DDD,因為這個話題實在太大了。
這次的主題是分享一些我們團隊在實踐DDD過程中碰到問題和如何克服它們,以及介紹一下我們所使用的架構體系。
先說說為什么標題限定在“單體應用”這個范圍內,
- 我們團隊這次實踐的應用全是單體應用
-
如果是分布式的應用,那么拆分限界上下文(BoundedContext)的最佳實踐是什么?當然是微服務!
我相信現在討論微服務的文章肯定不在少數,微軟也專門出過容器化微服務架構的電子書。傳送門點我。
資源如此豐富,當然就不需要我畫蛇添足了。
領域模型
領域模型的分析可以說是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.
出處:https://www.cnblogs.com/RobotZero/p/8304980.html