什么是DDD領域驅動設計?

什么是DDD?

DDD全稱為(Domain-Driven Design,簡稱DDD),領域驅動設計

為什么要學習DDD領域驅動設計?

在早期軟件開發(fā),對于一些簡單業(yè)務,只需要使用一個模塊,編寫多個業(yè)務邏輯就可以搞定。但是隨著業(yè)務增長,當需要修改其中某一項功能,則修改困難,原因是 該功能可能會侵蝕其他代碼模塊。修改需要謹慎,投入時間成本,人力成本過高。DDD模型可以很好的解決這個問題。

DDD模型解決了什么問題?

粒度更小,架構更加清晰,業(yè)務需求變化的時候,系統(tǒng)架構也能隨之變化。DDD所呈現(xiàn)的系統(tǒng)必然是高內聚,低耦合的,在業(yè)務系統(tǒng)中,不會因為修改A模塊影響到了B模塊的使用。

1.過度耦合

在系統(tǒng)創(chuàng)建初期,業(yè)務初期,功能對于基礎設計都非常簡單,普通的CRUD就可以滿足業(yè)務需要,但是隨著系統(tǒng)的迭代,業(yè)務邏輯變得復雜,此時系統(tǒng)的冗余程度也會隨之增加。此時需要修改其中某個節(jié)點的邏輯,可能伴隨著影響到其他模塊的業(yè)務邏輯。此問題的根源出現(xiàn)于 系統(tǒng)架構不清晰,劃分出來的模塊內聚度低,高耦合。

有一種解決方案,按照演進式設計的理論,讓系統(tǒng)的設計隨著系統(tǒng)的實現(xiàn)的增長而增長。不需要提前設計,就讓系統(tǒng)伴隨業(yè)務成長而演進。敏捷實踐中的重構、測試驅動設計及持續(xù)集成可以對付各種混亂問題。 重構--保持行為不變的代碼改善清除了額不協(xié)調的局部設計,測試驅動設計確保對系統(tǒng)的更改不會導致系統(tǒng)丟失或破壞現(xiàn)有功能,持續(xù)集成則為團隊提供了同一代碼庫。

事實上,在解決現(xiàn)實問題的時候,我們會將問題映射到腦海中的概念模型,在模型中解決問題,再將解決方案轉換為實際的代碼。上述問題 在于我們解決了設計到代碼之間的重構,但提煉出來的設計模型,并不具有實際的業(yè)務含義,這就導致在開發(fā)需求的時候,其他同學不能自然的將業(yè)務問題映射到該設計模型。并不具有實際的業(yè)務含義。

用DDD則可以很好的解決領域驅動模型到設計模型的同步、演化,最后再將反映了領域的設計模型轉為實際的代碼。

注: 模型是我們解決實際問題所抽象出來的概念模型,領域模型則表達與業(yè)務相關的事實;設計模型則描述了所要構建的系統(tǒng)。

貧血癥和失憶癥

貧血領域對象: 貧血領域對象(Anemic Domain Object)是指僅用做數(shù)據(jù)載體,而沒有行為和動作的領域對象

  • 場景需求

獎池里配置了很多獎項,我們需要按運營預先配置的概率抽中一個獎項。 實現(xiàn)非常簡單,生成一個隨機數(shù),匹配符合該隨機數(shù)生成概率的獎項即可。

  • 貧血模型實現(xiàn)方案

先設計獎池和獎項的庫 2張數(shù)據(jù)庫表

class AwardPool {
    int awardPoolId;
    List<Award> awards;
    public List<Award> getAwards() {
        return awards;
    }
  
    public void setAwards(List<Award> awards) {
        this.awards = awards;
    }
    ......
}
class Award {
   int awardId;
   int probability;//概率
  
   ......
}
Service的實現(xiàn)
AwardPool awardPool = awardPoolDao.getAwardPool(poolId);//sql查詢,將數(shù)據(jù)映射到AwardPool對象
for (Award award : awardPool.getAwards()) {
   //尋找到符合award.getProbability()概率的award
}

按照傳統(tǒng)開發(fā)思想,可以發(fā)現(xiàn):我們的業(yè)務邏輯都是再Service中去編寫的,Award只是一個數(shù)據(jù)載體,沒有任何行為。簡單的業(yè)務系統(tǒng)采用這種貧血模型和過程化設計是沒有問題的 但是業(yè)務邏輯一旦復雜了,業(yè)務邏輯,狀態(tài)會散落在大量的方法中,原本的代碼意圖會漸漸不明確,我們將這種情況稱為由貧血引起的失憶癥

更好的是采用領域模型的開發(fā)方式,將數(shù)據(jù)和行為封裝在一起,并與現(xiàn)實世界中的業(yè)務對象映射各類具備明確的職責劃分,將領域邏輯分散到領域對象中。按照這種思想,上述的例子 就應該把概率放在AwardPool當中

軟件系統(tǒng)復雜性應對

解決復雜和大規(guī)模軟件的武器可以被粗略地歸為三類:抽象、分治和知識

1.分治:把問題空間分割為規(guī)模更下且易于處理的若干子問題。分割后的問題需要足夠小,以便一個人單槍匹馬也可以解決。其次,必須考慮如何將分割后的各個部分裝配為整體。分割的越合理越易于理解,在裝配整體時候,需要跟蹤的細節(jié)也就越小。即更容易設計各部分的協(xié)作方式。評判什么是分治的好,即高內聚,低耦合。

2.抽象: 使用抽象能夠精簡問題空間,而且問題越小越容易理解,舉個例子,從北京到上海出差,可以先理解為使用交通工具,但不需要一開始就確定是采用飛機,自駕,高鐵的形式。

3.知識: DDD可以認為是知識的一種。

DDD提供了這樣的知識手段,讓我們知道如何抽象出上下限的界限以及如何去分治。

與微服務架構相得益彰

在創(chuàng)建微服務的時候,需要創(chuàng)建一個高內聚,低耦合的微服務。而DDD中的限界上下文則完美匹配微服務要求,可以將該限界上下文理解為一個微服務進程。

在系統(tǒng)復雜之后,都需要分治來拆解問題,一般有兩種方式技術維度和業(yè)務維度,技術維度是類似mvc的樣子,業(yè)務維度則是指按照業(yè)務領域來劃分系統(tǒng)。

微服務架構更強調從業(yè)務維度去做分治來應對系統(tǒng)復雜度,而DDD也是同樣的著重業(yè)務視角,如果兩者在追求目標(業(yè)務維度)達到了統(tǒng)一在具體的做法下面可能有如下不同點。

我們將架構設計活動精簡為以下層面:

  • 業(yè)務架構:根據(jù)業(yè)務需求設計業(yè)務模塊和關系
  • 系統(tǒng)架構:設計系統(tǒng)和子系統(tǒng)的模塊
  • 技術架構:決定采用的技術及框架

以上三種活動在實際開發(fā)中是有先后順序的但不一定誰先誰后。在解決常規(guī)套路問題的時候,很自然地往熟悉的分層架構套。在業(yè)務不復雜的時候這樣是合理的。

跳過業(yè)務架構設計出來的架構關注點不在業(yè)務響應上,在面臨需求迭代響應市場變化地時候就很痛苦

DDD地核心訴求就是將業(yè)務架構映射到系統(tǒng)架構上,在響應業(yè)務變化調整架構的時候,也隨之變化系統(tǒng)架構。而微服務追求業(yè)務層面的服用,設計出來的系統(tǒng)架構和業(yè)務一致;在技術架構上則系統(tǒng)模塊之間 充分解耦,可以自由選擇合適的技術架構,去中心化地治理技術和數(shù)據(jù)。
[圖片上傳失敗...(image-a96260-1634801926133)]

設計領域模型的一般步驟如下:

  1. 根據(jù)需求劃分出初步的領域和限界上下文,以及上下文之間的關系;
  2. 進一步分析每個上下文內部,識別出哪些是實體,哪些是值對象
  3. 對實體、值對象進行關聯(lián)聚合,劃分出聚合的范疇和聚合根
  4. 為聚合根設計倉儲,并思考實體或值對象的創(chuàng)建方式
  5. 在工程中實踐領域模型,并且在實踐中檢驗模型的合理性,倒推模型中的不足的地方并重構

戰(zhàn)略建模

戰(zhàn)略和戰(zhàn)術設計是站在DDD的角度進行劃分。戰(zhàn)略設計側重于高層次、宏觀上去劃分和集成限界上下文,而戰(zhàn)術設計則關注更具體使用建模工具來細化上下文。

領域

現(xiàn)實世界中,領域包含了問題域和解系統(tǒng)。一般認為軟件是對現(xiàn)實世界的部分模擬,在DDD中,解系統(tǒng)可以映射為一個個限界上下文,限界上下文就是軟件對于問題域的一個特定的、有限的解決方案。

限界上下文

一個由邊界限定的特定職責。領域模型便在于這個邊界之類。在邊界內,每一個模型概念,包括它的屬性和操作都具有特殊的含義

一個給定的業(yè)務領域會包含多個上下文,想與一個限界上下文溝通,則需要通過現(xiàn)實邊界進行通信。系統(tǒng)通過確定的限界上下文來進行解耦,而每一個上下文內部緊密組織,職責明確,具有較高的內聚性。

一個很形象的比喻:細胞之所以能夠存在,是因為細胞膜限定了在什么細胞內,什么在細胞外,并且確定了什么物質可以通過細胞膜。

劃分限界上下文

劃分限界上下文,不應該采用技術架構或者開發(fā)任務來創(chuàng)建限界上下文,應該按照語義的邊界來考慮。我們的實踐,考慮產品所講的通用語言,從中提取一些術語稱之為概念對象,尋找對象之間的聯(lián)系;或者從需求里提取一些動詞,觀察動詞和對象之間的關系;我們將緊耦合的各自圈在一起,觀察他們內在的聯(lián)系,從而形成對應的界限上下文。形成之后,我們可以嘗試用語言來面熟下界上下文的職責,看它是否清晰、準確、簡潔和完整。簡言之,限界上下文應該從需求出發(fā),按領域劃分

前文提到的,用戶劃分分為運營和用戶。其中,運營對抽獎的活動的配置十分復雜,但相對低頻。用戶對這些抽獎的活動配置使用的是高頻次且無感知的。根據(jù)這些業(yè)務特點,首先將抽獎平臺劃分為C端和M端抽獎管理平臺2個子域,讓兩者完全解耦

[圖片上傳失敗...(image-57c2cd-1633749292025)]

在確認了M端領域和C端的限界上下文后,我們在對各自的上下文內部進行限界上下文的劃分。采用C端舉例

產品的需求概述如下:

  1. 抽獎活動有活動限制,例如用戶的抽獎次數(shù)限制,抽獎的開始和結束的時間等;
  1. 一個抽獎活動包含多個獎品,可以針對一個或多個用戶群體
  1. 獎品有自身的獎品配置,例如庫存量,被抽中的概率等,最多被一個用戶抽中的次數(shù)等等;
  1. 用戶群體有多種區(qū)別方式,如按照用戶所在城市區(qū)分,按照新老客區(qū)分等;
  1. 活動具有風控配置,能夠限制用戶參與抽獎的頻率。

根據(jù)產品的需求提取出一些關鍵性的概念作為子域,形成限界上下文

[圖片上傳失敗...(image-d3758f-1633749292025)]

首先把抽獎作為整個子域 的核心,承擔著用戶抽獎的核心業(yè)務,抽獎中包含了獎品和用戶群體的概念。曾考慮分出抽獎和發(fā)獎2個領域,前者復雜抽獎 后者負責將獎品發(fā)送出去。但在實際開發(fā)過程中,我們發(fā)現(xiàn)這兩部分的邏輯緊密連接,難以拆分。

對于活動的限制,定義了活動準入的通用語言,將活動開始/結束實踐,活動可參與次數(shù)等限制條件都收攏到活動準入上下文中。

對于抽獎的庫存量,由于庫存的行為與獎品本身相對解耦,庫存關注點更多是庫存內的數(shù)量核銷,且?guī)齑姹旧砭哂型ㄓ眯?,可以唄獎品之外的內容使用,因此可以定義一個庫存上下文。

由于C端存在一些刷單行為,根據(jù)產品需求定義風控上下文,對于活動進行風控。最后,活動準入,風控抽獎等領域設計到了計數(shù)的限制,因此定義了計數(shù)上下文。

可以看到通過,DDD模型的限界上下文的劃分,界定出抽獎、活動準入、風控、計數(shù)、庫存等五個上下文每個上下文在系統(tǒng)都高度內聚。

上下文映射圖

在進行上下文劃分之后,我們還需要進一步梳理上下文之間的關系。

康威定律: 任何組織在設計一套系統(tǒng)時,所交付的設計方案在機構上都與該組織的溝通結構保持一致。

康威定律告訴我們,系統(tǒng)結構應盡量的與組織機構保持一致這里我們認為團隊結構就是組織結構,限界上下文就是系統(tǒng)的業(yè)務結構。因此,團隊結構應該和限界上下文保持一致。

梳理清楚上下文之間的關系,從團隊內部的關系來看。

  1. 任務更好拆分,一個開發(fā)人員可以全身心的投入到相關的一個單獨的上下文中
  1. 溝通更加順暢,一個上下文可以明確自己對其他上下文的依賴關系,從而使得團隊內開發(fā)直接更好的對接,從團隊關系來看,明確的上下文關系能夠帶來如下幫助
1.  每個團隊在它的上下文中能夠明確自己領域內的概念,因為上下文是領域的解系統(tǒng)。
    
    
2.  對于限界上下文之間發(fā)生交互,團隊與上下文的一致性,能夠保證我們明確對接的團隊和依賴的上下游。

限界上下文之間的依賴關系

  • 合作關系:兩個上下文緊密合作的關系,一榮俱榮,一損俱損
  • 共享內核: 兩個上下文依賴部分共享的模型
  • 客戶方—>供應方開發(fā):上下文之間有組織的上下游依賴。
  • 遵奉者 :下游上下文只能盲目依賴上游上下文
  • 防腐層: 一個上下文通過一些適配和轉換與另一個上下文交互。
  • 開放主機服務:定義一種協(xié)議來讓其他上下文來對本上下文進行訪問
  • 發(fā)布語言:通常OHS一起使用,用于定義開放主機的協(xié)議
  • 大泥球: 混雜再一起的上下文關系,邊界不清晰
  • 另謀他路: 兩個完全沒有任何聯(lián)系的上下文。

上文定義了上下文映射間的關系,經過反復斟酌,抽獎平臺上下文的映射關系圖如下:

[圖片上傳失敗...(image-19ac1a-1634801926133)]

由于抽獎,風控,活動準入,庫存,計數(shù)上下文都處再抽獎領域的內部,所以它們之間符合"一榮俱榮,一損俱損"的合作關系 PS。

同時,抽獎上下文再進行發(fā)卷的時候會依賴于,卷碼,等上下文,抽獎上下文通過防腐層作為發(fā)布語言對抽獎上下文提供訪問機制

通過上下文映射關系,明確限制了上下文的耦合性,在抽獎平臺中,無論是上下文內部交互還是與外部上下文交互,耦合度都限定在數(shù)據(jù)耦合的層級

戰(zhàn)術建模-細化上下文

梳理清楚上下文之間的關系后,我們需要從戰(zhàn)術層面上剖析上下文內部的組織關系。

實體:

當一個對象由其表示區(qū)分時,這種對象稱為實體。

最簡單的,公安系統(tǒng)的身份信息錄入,對于人的模擬,即認為是實體,因為每個人是獨一無二的,且其具有唯一標識。

在時間上建議將熟悉的驗證放到試題中

值對象:

當一個對象用于對事物進行描述而沒有唯一標識時,它被稱為值對象,

比如顏色信息:我們只需要知道{"name":"黑色","css":"#000000"}這樣的值信息就能夠滿足要求了,這避免了我們對標識追蹤帶來的系統(tǒng)復雜性。

值對象很重要,在習慣了使用數(shù)據(jù)庫的數(shù)據(jù)建模后,很容易將所有對象看作實體。使用值對象可以更好地系統(tǒng)優(yōu)化,精簡設計

它具有不變性、相等性和可替換性

在實踐中,需要保證值對象創(chuàng)建后就不能被修改,即不允許外部再修改其屬性。在不同上下文集成時,會出現(xiàn)模型概念的公用,如商品模型會存在于電商的各個上下文中,在訂單上下文中如果你只關注下單時地商品信息快照,那么將商品對象視為值對象是很好的選擇。

聚合根:

Aggregate是一組相關對象地集合,作為一個整體被外界訪問,聚合根是這個聚合地根節(jié)點。

聚合是一個非常重要的概念,核心領域往往都需要聚合來表達。其次,聚合在技術上有非常高的價值,可以指導詳細設計。

聚合由根實體,值對象和實體組成。

如何創(chuàng)建好的聚合?
  • 邊界內的內容具有一致性·:在一個事務只修改一個聚合實例,如果你發(fā)現(xiàn)邊界內很難接受強一致,不管是出于性能或產品需求的考慮,應該考慮剝離出獨立的聚合,采用最終一致的方式。
  • 設計小聚合: 大部分的聚合都可以只包含根實體,而無需包含其他實體。即使一定要包含,可以考慮將其創(chuàng)建為值對象。
  • 通過唯一標識來引用其他聚合或實體:當存在對象之間的關聯(lián)時,建議引用其唯一標識而非引用其整體對象。如果是外部上下文中的實體,引用其唯一標識或將需要的熟悉構造值對象,如果聚合創(chuàng)建復雜,推薦使用工廠方法來屏蔽內部復雜的創(chuàng)建邏輯。

聚合內部多個組成對象的關系可以用來指導數(shù)據(jù)庫創(chuàng)建,但不可避免存在一定的抗阻。如聚合中存在List<值對象>那么在數(shù)據(jù)庫中建立 1:N的關聯(lián)需要將值對象單獨建表,此時是有id的 建議不要將改id暴露到資源庫外部,對外隱蔽

領域服務:一些重要的領域行為或操作,可以歸類為領域服務,它既不是實體,也不是值對象的范疇。

當我們采用了微服務架構風格,一切領域邏輯的對外暴露均需要通過領域服務來進行。如原本由聚合根暴露的業(yè)務邏輯也需要依托于領域服務

領域事件:領域事件是對領域內發(fā)生的活動進行的建模。

抽獎平臺的核心上下文是抽獎。接下來介紹一下對抽獎上下文的建模。

[圖片上傳失敗...(image-e79b38-1634801926133)]

抽獎上下文中,通過DrawLottery 這個聚合根來控制抽獎的行文??梢钥吹揭粋€抽獎包括了抽獎ID(LotteryId)以及多個獎池(AwardPool),而一個獎池針對一個特定的用戶群體(UserGroup)設置了多個獎品(Award)。

另外,在抽獎領域中,我們還會使用抽獎結果(SendResult)作為輸出信息,使用用戶領獎記錄(UserLotteryLog)作為領獎憑據(jù)和存根。

謹慎使用值對象

在實踐中,我們發(fā)現(xiàn)雖然一些領域對象符合值對象的概念,但是隨著業(yè)務的變動,很多原有的定義會發(fā)生變更,值對象可能需要在業(yè)務意義具有唯一標識,而對這類值對象的重構往往需要較高成本。因此在特定的情況下,我們也要根據(jù)實際情況來權衡領域對象的選型。

DDD工程實現(xiàn)

模塊

模塊(Module)是DDD中明確提到的一種控制限界上下文的手段,在我們的工程中,一般盡量用一個模塊來標識一個領域的限界上下文

如代碼中所示,一般的工程包的組織方式為{com.公司名.組織架構.業(yè)務.上下文.*},這樣的組織結構能夠明確將一個上下文限定在包的內部

import com.company.team.bussiness.lottery.*;//抽獎上下文
import com.company.team.bussiness.riskcontrol.*;//風控上下文
import com.company.team.bussiness.counter.*;//計數(shù)上下文
import com.company.team.bussiness.condition.*;//活動準入上下文
import com.company.team.bussiness.stock.*;//庫存上下文

對于模塊內的組織機構,一般情況下我們是按照領域對象、領域服務、領域資源庫、防腐層等組織方式定義的

import com.company.team.bussiness.lottery.domain.valobj.*;//領域對象-值對象
import com.company.team.bussiness.lottery.domain.entity.*;//領域對象-實體
import com.company.team.bussiness.lottery.domain.aggregate.*;//領域對象-聚合根
import com.company.team.bussiness.lottery.service.*;//領域服務
import com.company.team.bussiness.lottery.repo.*;//領域資源庫
import com.company.team.bussiness.lottery.facade.*;//領域防腐層
領域對象

領域驅動要解決的一個重要問題就是解決貧血問題。這里采用之前定義的抽獎聚合根和獎池AwardPool值對象來具體說明

抽獎聚合根持有了抽獎的活動的id,和所有可用的獎池列表,它的一個最主要的領域功能就是根據(jù)一個抽獎發(fā)生場景,選出一個適配的獎池。chooseAwardPool方法

chooseAwardPool的邏輯是這樣的:DrawLotteryContext會帶有用戶抽獎時的場景信息,DrawLottery會根據(jù)這個場景信息,匹配一個可以給用戶發(fā)獎的AwardPool。

package com.company.team.bussiness.lottery.domain.aggregate;
import ...;
  
public class DrawLottery {
    private int lotteryId; //抽獎id
    private List<AwardPool> awardPools; //獎池列表
  
    //getter & setter
    public void setLotteryId(int lotteryId) {
        if(id<=0){
            throw new IllegalArgumentException("非法的抽獎id"); 
        }
        this.lotteryId = lotteryId;
    }
  
    //根據(jù)抽獎入?yún)ontext選擇獎池
    public AwardPool chooseAwardPool(DrawLotteryContext context) {
        if(context.getMtCityInfo()!=null) {
            return chooseAwardPoolByCityInfo(awardPools, context.getMtCityInfo());
        } else {
            return chooseAwardPoolByScore(awardPools, context.getGameScore());
        }
    }
     
    //根據(jù)抽獎所在城市選擇獎池
    private AwardPool chooseAwardPoolByCityInfo(List<AwardPool> awardPools, MtCifyInfo cityInfo) {
        for(AwardPool awardPool: awardPools) {
            if(awardPool.matchedCity(cityInfo.getCityId())) {
                return awardPool;
            }
        }
        return null;
    }
  
    //根據(jù)抽獎活動得分選擇獎池
    private AwardPool chooseAwardPoolByScore(List<AwardPool> awardPools, int gameScore) {...}
}

在匹配到一個具體的獎池之后,需要確定給用戶的獎品,這部分的領域功能放在AwardPool內

package com.company.team.bussiness.lottery.domain.valobj;
import ...;
  
public class AwardPool {
    private String cityIds;//獎池支持的城市
    private String scores;//獎池支持的得分
    private int userGroupType;//獎池匹配的用戶類型
    private List<Awrad> awards;//獎池中包含的獎品
  
    //當前獎池是否與城市匹配
    public boolean matchedCity(int cityId) {...}
  
    //當前獎池是否與用戶得分匹配
    public boolean matchedScore(int score) {...}
  
    //根據(jù)概率選擇獎池
    public Award randomGetAward() {
        int sumOfProbablity = 0;
        for(Award award: awards) {
            sumOfProbability += award.getAwardProbablity();
        }
        int randomNumber = ThreadLocalRandom.current().nextInt(sumOfProbablity);
        range = 0;
        for(Award award: awards) {
            range += award.getProbablity();
            if(randomNumber<range) {
                return award;
            }
        }
        return null;
    }
}

與以往的getter、setter業(yè)務對象不同,領域對象具有了行為,對象更加豐滿,同時,比起將這些邏輯寫在service內,領域功能的內聚性更強,職責更加明確。

資源庫

領域對象需要資源庫,存儲的手段可以是多樣化的,常見的無非是數(shù)據(jù)庫,分布式緩存,本地緩存等。資源庫的作用,是對領域的存儲和訪問進行統(tǒng)一管理的對象。在抽獎平臺上,是通過如下的方式組織資源庫的

//數(shù)據(jù)庫資源
import com.company.team.bussiness.lottery.repo.dao.AwardPoolDao;//數(shù)據(jù)庫訪問對象-獎池
import com.company.team.bussiness.lottery.repo.dao.AwardDao;//數(shù)據(jù)庫訪問對象-獎品
import com.company.team.bussiness.lottery.repo.dao.po.AwardPO;//數(shù)據(jù)庫持久化對象-獎品
import com.company.team.bussiness.lottery.repo.dao.po.AwardPoolPO;//數(shù)據(jù)庫持久化對象-獎池
  
import com.company.team.bussiness.lottery.repo.cache.DrawLotteryCacheAccessObj;//分布式緩存訪問對象-抽獎緩存訪問
import com.company.team.bussiness.lottery.repo.repository.DrawLotteryRepository;//資源庫訪問對象-抽獎資源庫

資源庫對外的整體訪問由Respository提供,它聚合了各個資源庫的數(shù)據(jù)信息,同時也承擔了資源庫存儲的邏輯(例如緩存更新機制等)

在抽獎資源庫中,我們屏蔽了對底層獎池和獎品的直接訪問,僅對抽獎的聚合根進行資源管理。代碼示例中展示了抽獎資源獲取的方法(最常見的Cache Aside Pattern)

比起以往將資源管理放在服務的做法,由資源庫進行管理,職責更加明確,代碼可讀性和可維護性更強。

package com.company.team.bussiness.lottery.repo;
import ...;
  
@Repository
public class DrawLotteryRepository {
    @Autowired
    private AwardDao awardDao;
    @Autowired
    private AwardPoolDao awardPoolDao;
    @AutoWired
    private DrawLotteryCacheAccessObj drawLotteryCacheAccessObj;
  
    public DrawLottery getDrawLotteryById(int lotteryId) {
        DrawLottery drawLottery = drawLotteryCacheAccessObj.get(lotteryId);
        if(drawLottery!=null){
            return drawLottery;
        }
        drawLottery = getDrawLotteyFromDB(lotteryId);
        drawLotteryCacheAccessObj.add(lotteryId, drawLottery);
        return drawLottery;
    }
  
    private DrawLottery getDrawLotteryFromDB(int lotteryId) {...}
}
防腐層

亦稱適配層,在一個上下文中,有時需要對外部上下文進行訪問,通常會引入防腐層的概念來對外部上下文的訪問進行一次轉移。

有以下幾種情況會考慮引入防腐層。

  • 需要將外部上下文的模型翻譯成本上下文理解的模型
  • 不同上下文之間的團隊協(xié)作關系,如果是供奉者關系,建議引入防腐層,避免外部上下文變化對本上下文的侵蝕
  • 該訪問本上下文使用廣泛,為了避免改動影響范圍太大。

如果內部多個上下文對外部上下文需要訪問,那么可以考慮將其放到通用上下文中

在抽獎平臺中,定義了用戶城市信息防腐層,用于外部的用戶城市信息上下文。以用戶信息防腐層距離,它以抽獎請求參數(shù)為參,以城市信息MtCityInfo為輸出

package com.company.team.bussiness.lottery.facade;
import ...;
  
@Component
public class UserCityInfoFacade {
    @Autowired
    private LbsService lbsService;//外部用戶城市信息RPC服務
     
    public MtCityInfo getMtCityInfo(LotteryContext context) {
        LbsReq lbsReq = new LbsReq();
        lbsReq.setLat(context.getLat());
        lbsReq.setLng(context.getLng());
        LbsResponse resp = lbsService.getLbsCityInfo(lbsReq);
        return buildMtCifyInfo(resp);
    }
  
    private MtCityInfo buildMtCityInfo(LbsResponse resp) {...}
}
領域服務

上文中,我們將領域行為封裝到領域對象中,將資源管理行為封裝到資源庫中,將外部上下文的交互行為封裝到防腐層中。此時,我們再回過頭來看領域服務時,能夠發(fā)現(xiàn)領域服務本身所承載的職責也就更加清晰了,即就是通過串聯(lián)領域對象、資源庫和防腐層等一系列領域內的對象的行為,對其他上下文提供交互的接口。

我們以抽獎服務為例(issueLottery),可以看到在省略了一些防御性邏輯(異常處理,空值判斷等)后,領域服務的邏輯已經足夠清晰明了。

package com.company.team.bussiness.lottery.service.impl
import ...;
  
@Service
public class LotteryServiceImpl implements LotteryService {
    @Autowired
    private DrawLotteryRepository drawLotteryRepo;
    @Autowired
    private UserCityInfoFacade UserCityInfoFacade;
    @Autowired
    private AwardSendService awardSendService;
    @Autowired
    private AwardCounterFacade awardCounterFacade;
  
    @Override
    public IssueResponse issueLottery(LotteryContext lotteryContext) {
        DrawLottery drawLottery = drawLotteryRepo.getDrawLotteryById(lotteryContext.getLotteryId());//獲取抽獎配置聚合根
        awardCounterFacade.incrTryCount(lotteryContext);//增加抽獎計數(shù)信息
        AwardPool awardPool = lotteryConfig.chooseAwardPool(bulidDrawLotteryContext(drawLottery, lotteryContext));//選中獎池
        Award award = awardPool.randomChooseAward();//選中獎品
        return buildIssueResponse(awardSendService.sendAward(award, lotteryContext));//發(fā)出獎品實體
    }
  
    private IssueResponse buildIssueResponse(AwardSendResponse awardSendResponse) {...}
}
數(shù)據(jù)流轉

[圖片上傳失敗...(image-32b3d5-1634801926133)]
在抽獎平臺的實踐中,我們的數(shù)據(jù)流轉如上圖所示。 首先領域的開放服務通過信息傳輸對象(DTO)來完成與外界的數(shù)據(jù)交互;在領域內部,我們通過領域對象(DO)作為領域內部的數(shù)據(jù)和行為載體;在資源庫內部,我們沿襲了原有的數(shù)據(jù)庫持久化對象(PO)進行數(shù)據(jù)庫資源的交互。同時,DTO與DO的轉換發(fā)生在領域服務內,DO與PO的轉換發(fā)生在資源庫內。
與以往的業(yè)務服務相比,當前的編碼規(guī)范可能多造成了一次數(shù)據(jù)轉換,但每種數(shù)據(jù)對象職責明確,數(shù)據(jù)流轉更加清晰。

上下文集成

通常集成上下文的手段有多種,常見的手段包括開放領域服務接口、開放HTTP服>務以及消息發(fā)布-訂閱機制。
在抽獎系統(tǒng)中,我們使用的是開放服務接口進行交互的。最明顯的體現(xiàn)是計數(shù)上下文,它作為一個通用上下文,對抽獎、風控、活動準入等上下文都提供了訪問接口。 同時,如果在一個上下文對另一個上下文進行集成時,若需要一定的隔離和適配,可以引入防腐層的概念。這一部分的示例可以參考前文的防腐層代碼示例。

分離領域

接下來講解在實施領域模型的過程中,如何應用到系統(tǒng)架構中。
我們采用的微服務架構風格,與Vernon在《實現(xiàn)領域驅動設計》并不太一致,更具體差異可閱讀他的書體會。
如果我們維護一個從前到后的應用系統(tǒng):
下圖中領域服務是使用微服務技術剝離開來,獨立部署,對外暴露的只能是服務接口,領域對外暴露的業(yè)務邏輯只能依托于領域服務。而在Vernon著作中,并未假定微服務架構風格,因此領域層暴露的除了領域服務外,還有聚合、實體和值對象等。此時的應用服務層是比較簡單的,獲取來自接口層的請求參數(shù),調度多個領域服務以實現(xiàn)界面層功能。
[圖片上傳失敗...(image-c1e1c4-1634801926133)]
隨著業(yè)務發(fā)展,業(yè)務系統(tǒng)快速膨脹,我們的系統(tǒng)屬于核心時:
應用服務雖然沒有領域邏輯,但涉及到了對多個領域服務的編排。當業(yè)務規(guī)模龐大到一定程度,編排本身就富含了業(yè)務邏輯(除此之外,應用服務在穩(wěn)定性、性能上所做的措施也希望統(tǒng)一起來,而非散落各處),那么此時應用服務對于外部來說是一個領域服務,整體看起來則是一個獨立的限界上下文。
此時應用服務對內還屬于應用服務,對外已是領域服務的概念,需要將其暴露為微服務。
[圖片上傳失敗...(image-325711-1634801926133)]
注:具體的架構實踐可按照團隊和業(yè)務的實際情況來,此處僅為作者自身的業(yè)務實踐。除分層架構外,如CQRS架構也是不錯的選擇

以下是一個示例。我們定義了抽獎、活動準入、風險控制等多個領域服務。在本系統(tǒng)中,我們需要集成多個領域服務,為客戶端提供一套功能完備的抽獎應用服務。這個應用服務的組織如下:

package ...;
  
import ...;
  
@Service
public class LotteryApplicationService {
    @Autowired
    private LotteryRiskService riskService;
    @Autowired
    private LotteryConditionService conditionService;
    @Autowired
    private LotteryService lotteryService;
     
    //用戶參與抽獎活動
    public Response<PrizeInfo, ErrorData> participateLottery(LotteryContext lotteryContext) {
        //校驗用戶登錄信息
        validateLoginInfo(lotteryContext);
        //校驗風控 
        RiskAccessToken riskToken = riskService.accquire(buildRiskReq(lotteryContext));
        ...
        //活動準入檢查
        LotteryConditionResult conditionResult = conditionService.checkLotteryCondition(otteryContext.getLotteryId(),lotteryContext.getUserId());
        ...
        //抽獎并返回結果
        IssueResponse issueResponse = lotteryService.issurLottery(lotteryContext);
        if(issueResponse!=null && issueResponse.getCode()==IssueResponse.OK) {
            return buildSuccessResponse(issueResponse.getPrizeInfo());
        } else {   
            return buildErrorResponse(ResponseCode.ISSUE_LOTTERY_FAIL, ResponseMsg.ISSUE_LOTTERY_FAIL)
        }
    }
  
    private void validateLoginInfo(LotteryContext lotteryContext){...}
    private Response<PrizeInfo, ErrorData> buildErrorResponse (int code, String msg){...}
    private Response<PrizeInfo, ErrorData> buildSuccessResponse (PrizeInfo prizeInfo){...}
}

在本文中,我們采用了分治的思想,從抽象到具體闡述了DDD在互聯(lián)網真實業(yè)務系統(tǒng)中的實踐。通過領域驅動設計這個強大的武器,我們將系統(tǒng)解構的更加合理。

但值得注意的是,如果你面臨的系統(tǒng)很簡單或者做一些SmartUI之類,那么你不一定需要DDD。盡管本文對貧血模型、演進式設計提出了些許看法,但它們在特定范圍和具體場景下會更高效。讀者需要針對自己的實際情況,做一定取舍,適合自己的才是最好的。

本篇通過DDD來講述軟件設計的術與器,本質是為了高內聚低耦合,緊靠本質,按自己的理解和團隊情況來實踐DDD即可。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容