天天看點

在單體應用的一些DDD實踐經驗

閱讀此文需要一定的DDD基礎,如果你是第一次接觸DDD讀者,建議先去閱讀一些DDD相關的書籍或者文章之後再來閱讀本文。

背景

自從我在團隊中推行DDD以來,我們團隊經曆了一系列的磨難——先是把核心項目重構,接着又在一些衍生項目中嘗試全面落地DDD, 最終探索了一些經驗出來,特此記錄一下。

本文采用語言無關的角度陳述,無論你是Java或者c#的開發同學相信都可以無障礙閱讀。

請注意本文并不是介紹如何實作DDD,因為這個話題實在太大了。

這次的主題是分享一些我們團隊在實踐DDD過程中碰到問題和如何克服它們,以及介紹一下我們所使用的架構體系。

先說說為什麼标題限定在“單體應用”這個範圍内,

  1. 我們團隊這次實踐的應用全是單體應用
  2. 如果是分布式的應用,那麼拆分限界上下文(BoundedContext)的最佳實踐是什麼?當然是微服務!

    我相信現在讨論微服務的文章肯定不在少數,微軟也專門出過容器化微服務架構的電子書。傳送門點我。

    資源如此豐富,當然就不需要我畫蛇添足了。

領域模型

領域模型的分析可以說是DDD當中最為核心的部分,因為你整個系統的業務邏輯代碼都是基于領域模型而構成的。

而要将業務邏輯轉換成領域模型除了對業務的熟悉外還需要極高的抽象能力,是以一般需要業務專家和模組化專家共同完成。

怎樣提煉一個好的領域模型是一個非常大的話題,推薦你閱讀以下書籍:

  • 《領域驅動設計:軟體核心複雜性應對之道》Eric Evans
  • 《實作領域驅動設計》Vaughn Vernon
  • 《領域驅動設計與模式實戰》Jimmy Nilsson

另外微軟架構電子書上還有推薦其他幾本DDD的書籍,遺憾的是,JD和TB都沒搜到。

在團隊剛開始分析領域模型時,對所有相關者都是一個極大的挑戰,我這裡分享幾點經驗幫助團隊更好地度過這段時期:

  1. 不要想着能夠一次提煉出完美的領域模型(除非團隊中有着經驗豐富的DDD實踐者),通常來說,我們會在會議上決定一個粗略的模型,然後在開發過程中你會發現有一些不自然的地方,比如某些上下文頻繁地與其他上文通信,或者某個實體的行為不是很恰當,這個時候再去修正領域模型,這樣演進式的過程可以大大降低你們在初期的壓力。
  2. 如果你的團隊整體能力不足以支撐領域模型的推行,或者他們在初期的配合度不高時,你可以選擇把你的項目中業務邏輯最為複雜的部分使用弱化的領域模型拆解,比如僅使用充血模型和領域服務,這樣至少你可以對最為複雜的部分引入一些DDD戰術模式或設計模式。
  3. 就算你的團隊能力夠了,但大部分人都沒有DDD的經驗的話,我也建議先隻引入部分模式(比如隻引入實體,值對象和倉儲這類比較容易了解的模式)來提高團隊的敏感度之後再采用完整的領域模型。
  4. 領域模型會對查詢帶來一定的複雜性,這種時候你可以采用CQRS來分離Query和Command,隻有在Cammand的時候你才需要發揮領域模型的威力,至于Query,SQL語句顯然是更好選擇。

基礎架構

了解DDD的同學都應該知道,DDD當中最為重要的部分就是限界上下文(BoundedContext),在領域模型中我們區分好了上下文之後,下一步就是選擇一種技術手段來確定每個上下都是低耦合高内聚且自治的。

在分布式應用中,多數設計者和包括微軟架構的電子書都會推薦使用一個上下文對應一個微服務的方式來實作(确實微服務和上下文的設計需求不謀而合)。

但單體應用該怎麼辦呢?

有同學說,我們可以通過命名空間來隔離它們啊。

不錯,我們可以這樣做,但是有以下幾個缺點

  1. 在使用IDE的智能引用時,你得确認你引用的實體究竟是位于目前上下文之内還是之外。
  2. 會導緻你的項目結構層次過深,不便于檢視。(至于過深的标準是多少,看個人了,對于我來說,5層是可以接受的上限,理想是控制在4層以内)
  3. 不便于向微服務架構遷移

是以我們選擇了使用程式集(java是使用jar包)的方式來隔離每個上下文,這樣做克服了以上的缺點,但卻帶來了新的問題:動态加載這些上下文。

不過這種程度的問題比起帶來的收益幾乎可以忽視。

我們團隊使用一個基礎平台來動态加載這些上下文,

我們采用了 Abp 架構提供的插件功能來實作,如果你也是.net 的使用者,也可以采用 Abp 來建構這個應用。

當然自己寫一個動态加載功能也并不困難。

基礎架構如下圖所示:

在單體應用的一些DDD實踐經驗

可是我們的平台要承擔很多功能,比如開放RESTful的API與Webservice(為了相容老的接口), 同時還要提供授權(使用了基于Oauth2.0協定的三種模式)、資料庫初始化、處理請求上下文等等,我就不一一列出來了。

我們希望BC(BoundedContext,後文都會簡寫為BC)裡不需要關注網絡層面的東西而隻聚焦于應用,是以很多通用的事情都由平台來承擔, 而且有時還會有一些互動,比如在驗證權限時你得跟使用者權限上下文通信。

在這種前提下,我們抽出了一個用于連接配接平台和這些BC的互動層,我們把它稱作——橋接元件(BrigeComponent),它負責聯系起平台和這些BC,外加上一些共用的基礎設施,我們的架構圖變成了這樣:

在單體應用的一些DDD實踐經驗

這樣一來,你可以把每個BC都當作微服務來處理,每一個BC内的分層結構你可以按你的喜歡的來,如果你喜歡标準的三層架構(UI + BLL + DAL),你可以将BC設計那樣。

你甚至可以每個BC都采用不同的風格,比如一個采用N層架構,而另一個采用事件驅動架構(EDA)。

這裡我們的BC都用了相同的DDD推薦分層架構(這裡省去了 表現層, 因為現代應用大多都是前後端分離了的),如下圖所示:

在單體應用的一些DDD實踐經驗

好了,現在整體架構和領域模型都已經确定下來後,我們開始編碼了,但很快我們就遇到了阻礙。

“結算上下文需要通路使用者權限上下文,它需要知道這個使用者的機構資訊,我可以直接引用嗎?”

“帳戶上下文這裡輸出的資料需要通用上下文提供一些有效性校驗,我可以直接引用嗎?”

“我這裡也需要通路通用上下文!”

……

好吧,如果我們直接提供引用,會有以下問題:

  1. 由于我們采用了程式集分割上下文,是以互相引用是不被允許的。
  2. 就算克服了互相引用的問題,最終也會導緻引用拓撲圖混亂不堪。
  3. 強耦合,這會直接影響到以後的拓展性。

在微服務中,為了克服服務間的互相通信問題,目前我了解的有兩類解決方案,

一是類似于ESB(企業服務總線)的中心化通信模式,比如大名鼎鼎的SprinCloud。

二是現在微服務界炒得沸沸騰騰的ServiceMesh(服務網格),比如 Linkerd 和 Istio。

我們項目選擇了前者,使用了類似于ESB中心化通信方式來解決,簡單來說,你需要一個通信中介者(Mediator)來負責BC之間的互動,結構圖如下:

在單體應用的一些DDD實踐經驗

如果你是 .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.

歡迎轉載,注明出處即可。

如果你覺得這篇博文幫助到你了,請點下右下角的推薦讓更多人看到它。

繼續閱讀