天天看點

當中台過氣,微服務回歸單體,DDD的意義何在?

作者:閃念基因

2015年之後,随着雲原生、微服務、大中台等一系列技術名詞誕生的同時,還有一個耳熟能詳的名詞“領域驅動”也開始被捧上神壇。筆者初次聽到領域驅動是參加一個技術分享會,當時給我的直覺感受就是:好像說了什麼,但又好像什麼都沒說,很多概念很"形而上學",在天空中飄啊飄,無法落地。

十年過去了,中台已經過氣,微服務回歸單體也一度成為技術圈讨論的熱點話題,曾經神壇上雲遮霧繞的 DDD 在今天看來是否還有讨論的意義?在過去一兩年的實踐中,筆者對 DDD 有了更深的體會,本文将闡述我的一些淺見,如果有了解不到位的地方,也希望同學們一起讨論。

01

領域驅動的理念

領域驅動這個概念一開始是由大神 Eric Evans 在2003釋出他的名著《Domain Driven Design:Tackling the Complexity in the Heart of Software》中提出,從标題中可以直覺的知道 DDD 是為了解決軟體系統的複雜性問題,是一種降低業務系統複雜度的方法論,許多同學認為領域驅動難以了解,是因為他提出了很多抽象的概念,我們不妨先抛開這些概念,先去了解一下它去解決問題的思路。

1.1 統一語言與模型

對于一個開發來說,我們的工作一句話就是:用代碼實作需求,在實作的過程中不同的人、不同的團隊,可以有不同的實踐,領域驅動就是其中的一種實作路徑。領域驅動在需求到代碼之間試圖建立起一條橋梁,橋梁的名字叫統一語言和模型。

當中台過氣,微服務回歸單體,DDD的意義何在?

什麼是統一語言?軟體開發的核心難度就在于處理隐藏在業務知識中的複雜度,想要處理這種複雜度,首先需要打破業務與技術之間溝通壁壘,在一個項目中,不光是有開發人員,還有測試、運維、産品、pm 等等,能把事做成的前提是可以把事情說清楚,我們知道中華文化博大精深一句話在不同的環境、場合下完全有不同的語意,統一語言的思想就是提倡團隊内通過不斷溝通去确定在一個業務領域中的術語或概念有唯一明确的語意。

那什麼是模型呢?我總結為:一種化繁就簡的抽象,抽象是目的是為了簡化問題,先忽略細節,從頂層思考問題, 抽象不在乎形式的表達,而取決于如何看待問題和分析問題的角度,我們舉幾個例子說明:

  • 把大象裝進冰箱需要幾步?一共三步,打開冰箱->放入大象->關上冰箱。

雖然這是一個梗,但不得不說,這是很好的一種面向過程的抽象。

  • 程式=資料結構+算法。

這已經變成了計算機科學中最基本的一條原則,即任何程式都可以分解為算法+資料結構,雖然程式要解決的問題都沒有确定,但是已經有了一個思考問題的方向。

  • 類圖、流程圖、架構圖...

對于一個比較複雜的系統,我們很難通過幾句話把它講清楚,這個時候,畫圖就成為了一個好的表達方式,而畫圖就是一種抽象,根據你想抽象角度的不同使用不同類型的圖。例如有人讓你講講購物網站做了什麼,你可以把問題簡化并用下圖表示,即描述使用者、商家、平台之間的關系,這正是面向對象的模組化思路,而領域驅動本質上也是一種面向對象的模組化方法。

當中台過氣,微服務回歸單體,DDD的意義何在?

在引入統一語言和模型抽象的思路之後,就可以把需求到實作的這個過程用下圖表示,技術和業務的相關同學通過統一語言去溝通交流需求,通過模型抽象描述需求,最後按照模型去實作相應的代碼,領域驅動的一大目标是:修改需求即修改統一語言,修改統一語言即修改模型,修改模型即修改代碼,這也就實作了從需求到代碼的有效資訊傳遞。

當中台過氣,微服務回歸單體,DDD的意義何在?

1.2 分而治之:再談歸并排序

為什麼這裡要談起歸并算法,因為領域驅動所提倡解決問題的思維方式和歸并排序算法如出一轍,可以總結為一句話:自頂向下拆分、自低向上合并。

當中台過氣,微服務回歸單體,DDD的意義何在?

我們簡單回顧一下歸并排序的思路:

  1. 明确主函數的功能、輸入、輸出;
  2. 分解問題,确定分解後子函數的功能、輸入、輸出;
  3. 合并子函數的傳回,僞代碼如下:
//主函數
void mergeSort(std::vector<int>& arr, int left, int right) {
   int mid = left + (right - left) / 2;


    //拆分過程 一拆二
    mergeSort(arr, left, mid); //子函數1 
    mergeSort(arr, mid + 1, right);//子函數2


    //合并過程 
    merge(arr, left, mid, right);


}
//合并函數
void merge(std::vector<int>& arr, int left, int mid, int right) {
    //實作
}           

而領域驅動在實作的過程中依舊沿用了這個思路即:定義問題、分解問題、合并結果。

定義問題:當我們面對一個複雜場景時,首先需要确定面對的問題是什麼?問題的邊界(限界上下文)在哪裡,我們很容易了解解決問題帶來的價值,但是很容易忽略定義問題帶來的價值。在項目實踐中,不知道你有沒有遇到過這樣的一種場景:技術同學會根據産品同學的一段描述立馬會陷入到技術實作中,等到驗收的過程中才會說出:“哦,原來你隻需要實作這種需求呀”的感歎,這就是沒有找到核心問題所在。

分解問題:定義好問題即明确了問題的邊界,我們就可以在邊界内進行問題的劃分,領域驅動的核心思想是分治,即拆分“邊界分明”的子問題,再針對子問題進行解決。這裡分解問題的思路,也和微服務拆分的思路有異曲同工之秒(說到微服務這裡抛出兩個有趣的問題 ,第一:領域驅動是03年就提出來了的概念,為什麼一直到15年左右才漸漸被大家熟知;第二:微服務和領域驅動有什麼關系,我相信在讀完本文後,你心中自有答案)

合并結果:解決完一個一個的子問題還不夠,如何把資訊串起來才是難點,在串起來的過程中就會出現耦合問題,這就又回到了一個軟體實踐中一個普遍的問題:如何做到“高内聚、低耦合”,其實細心的小夥伴已經發現了,這裡的分解問題的目标就對應着高内聚,而合并結果的目标則是低耦合。

02

那些晦澀難懂的術語

上一節我們講解了領域驅動所想解決的問題:降低系統複雜度,以及它解決問題的思路:統一語言、模型抽象、分而治之。明确了這兩點,我們再去看看其中的一些抽象的概念,相信你會有更深的了解。

2.1 在邊界内做事:領域與子域

概念上領域指:從事一種專門活動或者事業的範圍,這裡的重點在于範圍兩個字,範圍即邊界。不管我們去解決什麼問題,問題總是有邊界的,邊界越清晰,解決問題的思路則越清晰,再簡單點說,領域就是在一個邊界内要解決的問題。針對一個複雜問題,使用分治的思想,還可以進一步拆分成子問題。這種研究問題的思路其實已經司空見慣,假如我們需要研究人體,那麼人即問題的研究對象,我們可以按照不同的方法把把問題拆分成子問題,以下是兩個不同的思路,左圖是按照“系統” 劃分,右圖是按照“組成”劃分。

當中台過氣,微服務回歸單體,DDD的意義何在?

如何分解,沒有所謂的标準答案,拆分的方式不同,其實也可以說是抽象的角度的不同,因為抽象的角度不同,研究的方法也會有不同。比如中醫研究人體會側重于整體和部分的關系,西醫則側重于定量分析,我們不能說那種好或者不好,隻是看待問題的角度不同,角度即抽象。

當完成分解過程,我們在針對子問題,再尋求對應的解決思路,這個過程就是從問題域到解決域的過程,以下圖可以更直接的幫助你了解。

當中台過氣,微服務回歸單體,DDD的意義何在?

2.2 領域按功能再劃分:核心域、通用域、支援域

在不斷劃分的過程中,還可以按照功能性的不同把子領域再次的化為:核心域、通用域、支援域,這裡需要強調的是子域的劃分完全建立在對于業務的了解之上,基于業務,而非技術。

  • 核心域

是指富有競争力的領域,這裡是仁者見仁、智者見智,不同的人對于競争力有着不同的了解,比如還是拿人來舉例,身體、認知、财富到底哪一個是一個人的核心的競争力,當認為是身體是核心的人就會側重于鍛煉健身;認為認知是核心的人則會側重于看書學習;認為财富重要的人則會側重于事業.... 總體看并不能說誰對誰錯,這是看待問題的方式不同。

而對于公司也是一樣的道理,我們看很多公司的業務和産品,表面上很相似,但是其實有着完全不同的商業模式,就以電商平台舉例,有的核心領域在物流服務、高端的品質;有的核心領域則是更加便宜好用的貨物上,對于一個公司來說,劃定了一個核心領域,其實也就确定了資源投入的方向,把好鋼用在刀刃上,提供差異化的價值服務。

  • 通用域

高複用能力或者沒有太多個性化需求的領域,比如所謂的“中台”概念就是指高複用的子產品化服務,整合所有底層能力,快速疊代前台功能。

  • 支撐域

支撐域是對核心域有所支援,但不是業務的核心競争力的部分。這部分的業務規則相對簡單,通常不需要深入了解業務需求,隻需要滿足基本的業務需求即可。

2.3 實體和值對象

什麼是實體?在業務中有具有唯一辨別的對象。比如在電商場景下,一個物品對象就可以是一個實體,物品有唯一辨別符(物品 id),物品的業務表現可能會發生變化,但是辨別符在整個業務周期中是保持一緻,比如一個物品在購買前是商品、購買後就變成了需發貨的貨物、如果要起退款就變成了一個需要召回的物品,但始終物品的辨別符不會改變。

什麼是值對象?針對一個實體對象,光有一個唯一辨別是不夠的,它不足于描述對象的特性,是以就有了屬性,比如一個商品的屬性一般有名稱、價格、圖檔、生産地,而值對象就是一個業務實體屬性的集合。

在實踐中,業務實體往往對應着一個實體類,這個實體類有唯一的辨別、屬性、以及其所有的業務方法。領域驅動提倡使用充血模型的方式,即在類中實作所有相關的業務方法,而不是隻把資料直接對外暴漏,這樣可以很好的保證了業務資料的一緻性和封裝的特性,以下是一個物品對象的類實作。

//實體 
//物品類
public class Product{
    private String productId; //唯一主鍵 唯一辨別 
    private String productName;     
    private String productUrl; 
    private String productPrice; 
    Private Address  productAddress;// 屬性集合
    // get set 業務行為 ...
    public function(){}
}
//值對象 
//倉庫位址類 (無主鍵id)
public class ProductAddress{
   private String Province;
   private String City;
   private String District;
}           

2.4 聚合和聚合根

當我們需要完成一個業務功能時,往往不是一個人就可以完成,而是大家協同工作,一起完成目标,在領域驅動中,實體就好像我們每一個人,聚合就是可以讓我們協同工作的組織,聚合根就是這個組織的上司者,是以聚合其實就是由業務邏輯緊密關聯的實體和值對象的集合,每個聚合有唯一的聚合根和業務邊界,聚合一般會根據業務單一職責和高内聚原則設計,來确定其需要包括哪些業務實體以及值對象。

我們以購物車場景為例來體會一下聚合和聚合根的含義,在購物車中,加入購物車的商品清單構成了一個聚合,購物車 id 即聚合根,通過購物車 id,外界可以通路購買物品的清單資訊、狀态、下單總金額等。

當中台過氣,微服務回歸單體,DDD的意義何在?

如果要用代碼實作這個簡單的場景,我們很自然地想到可以把購物車的相關邏輯實作在一個微服務裡,實際上,在領域驅動中,一組相關的業務聚合往往通過一個微服務來實作。再初步了解領域驅動的相關概念後,我們梳理一下它們之間的關系,如圖所示。

當中台過氣,微服務回歸單體,DDD的意義何在?

03

拆分與合并

上一節我們已經講解了一些領域驅動中一些重要的概念,這一節我們會介紹領域驅動中,關于分治與合并思想的落地,下面我們就分别讨論這兩個過程 。

3.1 拆分與微服務架構

我們還是回到歸并排序的案例中,思考一下平時所寫代碼與歸并排序的相似之處,我們簡單的對歸并排序代碼做一些改造,如下:

void mergeSort(std::vector<int>& arr, int left, int right) {
    //拆分過程
    //把mergeSort(arr, left, mid)改寫成 
    int resA=rpc.funtionA();    
    //把mergeSort(arr, mid + 1, right)改寫成 
    int resB=rpc.funtionB();    


    //合并過程 merge(arr, left, mid, right)改為
    func(resA,resB);   
}           

想想看,這不就是我們平常寫的應用層代碼嗎,先遠端同步調用 A 服務擷取資訊,再同步調用 B 服務,再組合所有結果資料進行運算并傳回,如下圖所示。

當中台過氣,微服務回歸單體,DDD的意義何在?

這不就是微服務的分層架構嘛!領域驅動最後的落地實作形式就是微服務,到這裡就可以回答上面提出的一個問題,為什麼領域驅動的理念是02年提出了,但是到了15年後才被人熟知,就是因為雲原生、微服務架構的發展是在15年左右,這給領域驅動的理念提供了一片可以生存的土壤;相反領域驅動也給微服務的設計提供了必要的方法論。

清楚微服務架構的同學一定知道:微服務架構設計的難點之一是拆分服務的力度大小,拆多了會導緻運維難度呈指數提高,拆少了又回到了單體架構的模式、不夠靈活,中間這個度就需要一種方法論來指導,而這個方法論就可以是領域驅動。對比領域分析模型和微服務架構,你會發現其實都是互相對應的,隻是一種是從業務角度出發描述問題,一種是從技術實作角度描述問題,而這也是理論和實踐的一種結合。

當中台過氣,微服務回歸單體,DDD的意義何在?

3.2 合并的最佳實踐:領域事件

在描述業務的過程中,往往會有這樣一種描述:當某種事件發生後,會觸發後續的事件或者使用者進一步的行為操作,在領域驅動中會把這種有明顯先後因果邏輯的事件稱之為領域事件。

在領域事件中,會發現不同僚件往往屬于不同的領域服務之間,比如使用者在購買物品支付成功後,會觸發發貨流程,這裡的支付和發貨就屬于不同的領域,并在邏輯上有先後的順序。

針對以上事件,領域驅動提倡:領域事件的資料通信方式使用事件釋出訂閱的方式進行,不直接同步調用,而事件釋出的本質則是一種低耦合的異步資料溝通方式。

同步調用有兩點需要考慮的問題:分布式事務以及時耗。針對事務問題一般需要引入第三方元件或者在業務層處理各種逾時失敗等異常的場景,可以說相當的複雜,維護成本也很高;而在分布式系統中時耗問題會被放大,一個請求可能跨越十幾個甚至幾十個服務,高并發的場景下,逾時的風險會增大,上遊的接口可能會被拖死。這都是同步調用需要考慮的問題。

在領域事件這種場景下,有一個更好技術選擇,則是使用事件釋出訂閱的方式,還是拿使用者購買物品支付發貨場景為例,看看其實作過程:

  1. 使用者支付下單後,支付域建立事件,持久化事件狀态,在支付成功後釋出事件,支付行為結束。
  2. 發貨域訂閱支付事件,在收到使用者支付成功事件後,觸發使用者所購買物品的發貨,持久化事件狀态并結束。
  3. 使用者收到發貨成功通知,等待收貨。
當中台過氣,微服務回歸單體,DDD的意義何在?

領域事件的本質其實是通過分析使用者旅程找到領域之間的因果邏輯鍊,再通過事件釋出訂閱機制去實作流程上的解耦合。

那我們如何找到領域事件呢?一種最佳的實踐是在領域專家的主導下項目相關的同學一起進行頭腦風暴,聯想和關聯到和業務有關的所有事件,但是這裡的難點并不是如何發散,而是發散後如何收斂事件,收斂的本質是對于事件的有效分類,這需要可以洞悉業務本質的人才可以做到,是以這就是為什麼領域驅動中有一種角色叫領域專家 ,這個過程我也用圖來表示。

當中台過氣,微服務回歸單體,DDD的意義何在?

04

總結

以上就是我對于領域驅動的一些淺見,如果你看完後還是感覺領域驅動有點形而上學,沒關系,隻要你記住,不管是技術還是生活,遇到事情多溝通,複雜問題先分解。

作者:呂昊俣

來源-微信公衆号:騰訊雲開發者

出處:https://mp.weixin.qq.com/s/izLddcVkGx94LoJmFSzR4A

繼續閱讀