戲說領域驅動設計(廿二)——聚合

SKevin發表於2022-04-11

聚合的自白

  大家好,我是聚合,在你們的期盼之下我終於出來了。其實早就想和大家見一面,不過作者每天總想著水流量,到現在才讓我出來。他把實體和值物件這兩個我家庭內的成員先介紹讓我感覺非常的不公平。沒有國哪有家?沒有家庭,生活也不會溫暖。好多的工程師眼裡只想著實體他們,讓我難受的想要哭泣。明明是由於我的存在才讓實體和值物件得到保護,才讓家庭內的各個成員不遭受風雨的侵襲,可偏偏總受不到重視。當然,我知道我是一種虛擬的存在,不像實體那樣看得見摸得著,這可能也是人們無法快速理解我的主要原因。

  我的家庭不大,我也不喜歡太龐大的家庭,每個人在其中都承擔著不同的責任、都在努力的工作。無論外面變化多麼快速,在家的保護下每個人都很幸福。我多想永遠的將這種平靜保持下去,可是樹欲靜而風不止,總會遇到一些不負責的主人,他們隨意的肆虐著我的家庭,不是把我的家人分裂出去就是隨意的往家裡面新增不屬於我們的人員。我們很內向,很害怕與陌生人溝通,也不希望沒有關係的人隨意的進出我的家門。各位人員都駐紮在這個小小的家中,它讓這個家庭的負荷遠遠超出了我們所能承受的上限,也讓家庭成員得不到應有的愛護,家已經不是幸福的代表了。

  我的家庭成員都很羞怯,可是過日子總是需要與外人打交道,所以我選擇了一個叫“聚合根”的人作為整個家庭的代表,這是一個體面、漂亮、善於溝通的小夥子,喜歡他的人都叫他“阿根”。他是我們的寄託,也是我們與外界溝通的唯一通道。雖然每個家庭成員仍然需要為工作而努力,雖然阿根時常也會把活推給其它人去幹,可是大家都很開心,因為他是公正的,他也在默默的為整個家挺付出,承擔了一個家庭代表該乾的事情。我生活的這個世界有許多不同的家庭,每個家庭都有自己的習慣、喜好和隱私,所以當你需要與我們的家庭合作的時候,請不要直接插入到我的家中來,也不要試圖繞過阿根而聯絡我的家人。您可以聯絡阿根同志,電話:010-12345678,他是一個誠實的人,一定會在不違反原則的情況下百分百完成您所囑託的事情。

  談到家規,這是我引以為豪壯的,儘管我從來沒擁有過七房太太。您知道嗎?之所以這個家庭能夠工作的很好,一是因為我明確規定了每位成員的責任,令人開心的是每位家人都可以自覺在阿根的指揮下工作;另一方面,我們有一套自己的家庭規則,人人都在遵守。儘管外面形勢惡劣,風大雨大,每個家庭成員都不需要擔心會受到影響,還是因為阿根,他可以幫我們擋住所有可能破壞家庭正常運轉的外部侵擾以及那些影響家庭穩定的人或事件。什麼?家庭規則是什麼?這個啊,就是一叫法啦,其實人類生活的世界中有許多類似的東西。國家的層面叫憲法,家庭的層叫家法,公司的層面叫制度,軟體中叫業務規則。如果把我的家庭放到軟體中,那麼就是由我來維護業務規則的一致性嘍,雖然我不像阿根那麼能說會道,我可是規範著整個家庭的運轉呢。

  馬克思不是說“一切事物都是運動”的嘛?其實我的家庭和家庭成員也一直都在變化著。同人類一樣,我也會隨著時間而變化,也會生、老、病、死;有需要我們參與的活動時,所有的人都會根據阿根的安排各自調整自己的狀態,當然也包括阿根自己。我希望我的家庭是一個整體,我不喜歡個人主義,有了問題大家一起承擔,我決不允許某人得利某人卻利益受損。我有一個做軟體開發的朋友,他說很羨慕我家的一損俱損、一榮俱榮的模式,還說在他們的工作領域有個叫“事務”的東西,和我的家庭是一樣的,看來我的家規還是很科學的嘛。

  其實我是一個極端的家庭生活擁護者,我十分不喜歡被隨意打擾,也很討厭被人管制,總會有些好事之人跳出來讓我們和其它的家庭合作。增加人際交往其實挺好的,但我頂不喜歡他還讓我們和別的家庭成為一種所謂的“利益共同體”。真是搞笑, 我就是一個自私的人。我只關心自己家的利益,別人的生死與我無關,我又不是葉文潔這個聖母婊。再說了,你想要利益共同體也得找個專門的人進行負責協調啊,不是有什麼“Saga”嗎?那個天天嘴裡喊著:“我是大公之人,我要以德服人”;要不你也可以找“領域事件”這個碎催啊,他最喜歡幹這種跑腿兒送信兒的事情。其實我挺冤枉的,讓我變得自私也是有原因的。幾年前我有過一些不堪回首的經歷,有人傢伙強制的促成了我和另外家庭的合作,期間大家都挺快樂,可一到決策拍板時刻就總會出現變卦,有另外一個傢伙暗中使用手段更改了合作伙伴的資訊使得他單方面悔約,雖然我家並未受到影響,但每個成員還是很沮喪的。那個做軟體的朋友又跳出來說這個叫“併發”,也不知道為什麼怎麼哪裡都有他,反正從那之後我就拒絕再同其它的家庭做什麼所謂的“利益共同體”。再有人這樣亂指揮, 我就用那個朋友教我的用“最終一致性”回懟,雖然我不知道這是什麼意思,但想來朋友應該不會欺騙我吧。

  儘管我不喜歡被打擾,但活在這個世上總會需要和別人打交道,我們其實也不願意孤獨地生活著。幸好,還是小根同志。當我們需要找到對方家庭時,只需要記住那個家庭代表的身份證號就行了;如果對方需要找到我們,也只需要記住阿根的身份證號。放心吧,我們生活的世界雖然亂了一點,但還不會總幹一些洩露身份證號賣錢的事情。我喜歡通過只記住對方家庭代表身份證的方式聯絡彼此,因為我是個單純的人,也喜歡單純的事情。我不想進入別人家庭的內部,也不喜歡別人進入我家。有了對方的身份證號,我只需要在黃頁中搜尋一下就知道對方的資訊了,也許不是全部,但誰家沒有點祕密呢。

  說了這麼多,我還沒有介紹自己的家庭成員構成。我的家中只有實體和值物件這些成員,總有些人喜歡往我的家中強行塞入其它的東西比如聯絡人黃頁。我可不喜歡這樣幹,那個又不是我家的私有財產,是多個家庭共用的,我的自私只是因為我想保護自己的家。很多人沒有明白,每一個聚合家庭都是獨立自主的,雖然我們也會和外界溝通。再次鄭重提醒一下:請不要隨意安插外界成員到我的家中,有事兒請電聯阿根。對了,我生活在資料的世界中,這個世界有無數和我們類似的家庭。統治者為了避免人口的膨脹,我們沒有工作的時候常常會進行休眠,有了任務了我們就會被喚醒,不過請一定要注意我的家庭的完整性。所有的成員都是一體的,不要讓一個殘缺的我們去面對這個紛擾的世界。

  以上是我——一個資料世界中聚合的自白,希望你喜歡!

IT朋友的自白

  大家好,我是聚合的朋友,無名氏而矣。我這個朋友腦子那裡有點問題,囉囉嗦嗦的不知道說什麼呢,今天早上還說趙家的狗又多看了他一眼,瘋了。我們們這可是技術性文章,哪裡管得著個人的榮辱。可作為朋友,也不能對他不尊重,所以還是讓我來說一下我的聚合朋友在DDD中的角色和設計原則,我不像他一樣腦子有問題,技術型文章自然要用技術語言。

1、隔離

  聚合最為重要的特質是隔離,近一步說是把實體和值物件進行分組。還記得我們前面文章中說過的DDD中的四個隔離級別,聚合隔離是最低層隔離,成本最低但效果也顯著。客觀來說,聚合其實是一個虛的概念,我們並沒有一個叫做聚合或分組的物件把領域模型進行分割。能進入到某一個聚合中的物件都應當在業務上是高內聚的,在匯聚在一起的模型中選擇一個代表作為訪問聚合的入口,這個代表稱之為聚合根,也是我朋友說的“阿根”。雖然在聚合內部可能會包含許多的值物件或實體,但一旦聚合成立,你就需要把這個聚合作為整體來看。任何的改變都不可以違反聚合自身的業務約束也就是不變條件。此外,你還需要保障聚合在持久化時使用事務來管理資料一致性,不過儘量不要使用一個事務同時管理多個聚合,這樣會使得事務範圍大,加大了併發的概率,效能也不好。這個原則放在過去10年,也許您還有打破的理由。當今的系統動輒數十上百臺伺服器,也不差再多個幾臺來安裝MQ。返回到隔離這個主題:聚合通過對領域模型分組以實現業務上的隔離;通過使用單一事務原則,能夠實現資料處理上的隔離。這兩重隔離,可以讓領域模型間相互影響變得非常小,實際上做到了最大化的解耦。

2、規模

  由於沒有硬性條件來限制聚合的大小,所以在設計過程中一不小心就可能把聚合設計的非常大,如下列程式碼所示。

public class Account extends EntityModel<Long> {
    private Role role;
    private Department department
}

  客觀來講,這段的程式碼非常符合物件導向設計精神,但在DDD中這種設計問題卻非常大,其中最重要的一點就是聚合的粒度不合理。上面強調過聚合的整體性,如果換成技術的語言就是當你儲存或載入一個聚合的時候,必須要全部載入或儲存其內包含的各類值物件或實體物件。您也許聽說過“懶載入”概念,這並不適用於聚合,因為這個概念是技術層次的,通常會是用在資料實體的操作上。極端情況下一個聚合可能包含了數以萬計的值物件,此時您需要進行妥協,比如把這個聚合進行拆分,也不要使用懶載入的方法。

  以上面的程式碼為例,由於聚合的整體性約束,您在反序列化時除了考慮“Account”所關聯的內聚屬性,還需要把“Role”和“Department”兩個實體一併載入。我們就不說別的,至少查詢三個表吧?問題是花了不少力氣把資料查詢出來了,您可能根本就不去使用,每一次載入的動作大概率有60%的工作是徒勞的。查詢還相對簡單一些,那如果涉及更新或儲存的時候呢?“Role”和“Department”這兩個實體需不需要和“Account”一起更新?不更新吧,那你這個還算是一個聚合體嗎?更新吧,你憑什麼跨BC更新其它的實體?即便是有這個需要,微服務架構下你就得使用分散式事務,系統的效能又降了好多。再退一步,即使這些聚合都在一個BC內,聚合越大事務的範圍越大,影響的表越多,怎麼看大聚合都是費力不討好的。

  所以結論出來了,你需要保持對聚合規模的關注,尤其是大聚合的時候需要考慮是否有必要。當然,你也別極端的讓每個聚合都只包含一個實體,那樣會造成業務內聚性不足。另外需要說明的是聚合中所包含的物件,一般應以值型別為主。由於聚合根是實體,所以很少會出現聚合中再包含另外的實體,在我所做過的專案中的確有遇到過,但總的來講比例非常少。如果不負責任的去評估,一個聚合中90%都是值物件,這個比例只高不低。

3、聚合根

  聚合中需要指定一個實體作為聚合根來作為整個聚合的對外觸點,也就是說外部只能通過聚合根實現對內部物件的訪問,這樣的限制可以對內部物件實現最大化的保護。此外,聚合根在確保物件不變特性方面起到的作用是巨大的,來自於外部的不合理請求完全可以在聚合根這一層面上進行攔截。不論聚合的規模大與小,都需要選擇一個聚合根。假如你用過類似的Axon框架,聚合根都要求從Axon中定義的聚合根抽象類繼承。拋開框架,實際上並沒有哪個標記限定或標明誰是聚合根,所以一般可以從物件的方法上體現,比如公有方法多比較多;也可以從業務上定義上出來,誰是業務主體誰是聚合根。

  如果一個聚合需要與另外的聚合建立關聯關係,只能使用聚合的ID。通過聚合ID方式進行關聯有點類似於資料庫中外來鍵,其實並不太符合物件導向精神。設計小聚合是在聚合設計過程中要遵循的規範,而通過聚合ID的方式實現聚合間關聯可以在程式碼層次上有效的限制住聚合的大小。此外,由於我們沒有持有另外聚合的引用,也自然不會發生同一個事務更新多個聚合的情況,可以避免前面所說的由於大聚合所帶來的各種負面問題。

  我在初次使用DDD落地的時候,那個時候其實還沒有IDDD這本書,聚合之間的關聯使用了引用的方式。當時遇到的一個需求是:僱員包含了角色,角色中包含了部門物件(因為要限制角色的範圍),部門物件又包含了僱員物件列表。在測試“查詢使用者”的場景時系統直接丟擲了一個StackOverflowException,雖然找到了問題,也修改成使用ID作為關聯。但當時我看著這種設計特別彆扭,覺得有反OOD原則,又是初次深度物件導向程式設計,只想追求純粹,糾結了好久。直到後來在看到IDDD的時候,才發現當時自己的行為是歪打正著。之所以舉這個案例,是想讓大家瞭解理論的重要性。如果你做的東西有了理論基礎的支撐,後面就會少走很多的彎路。

  迴歸正題,我們要求聚合只能通過ID進行關聯,但物件導航還是一個剛性的需求。比如賬戶中關聯了角色ID,我們在查詢賬戶許可權的時候還是需要通過角色ID把許可權實體查詢出來。此等場景下,請將查詢的操作放到應用服務中,千萬別在你的聚合中引入資源倉庫物件。

  另外需要再補充說明的是聚合根的識別,這個沒有公式。首先,聚合根一定是實體;其次,通常會使用聚合中的業務主體來承擔聚合根的角色,而聚合的主體通常也是聚合的名稱。比如訂單聚合,聚合根是訂單實體;賬戶聚合,賬戶實體是聚合根。應該沒有人會傻到使用實體中的某一個值物件作為聚合根吧 ?

4、事務

  前面我們已經討論過聚合的事務,但還是有必要再著重解釋一下。一個事務中更新多個聚合所引發的負作用我們已經說過,所以不作過多的解釋。在單體架構情況下,程式設計師習慣使用資料庫這種剛性事務來保障資料的一致性。到了微服務架構下,這種手段往往不能生效。兩個服務很可能使用了不同的資料庫,如果都為關係型資料庫還能通過使用如2PC、3PC這種基於XA協議的分散式事務,但引入的效能問題還是很大的;如果使用了NoSql,那資料庫級別事務也基本不用考慮了。微服務架構的引入,其實也要求我們在考慮問題的時候可以轉換一下思路,畢竟事務還是很重要的,因此也就出現了“柔性事務”的概念,也就是我們常說的“最終一致性”。新思想的出現,又引出了所謂的“領域事件”的概念,分散式事務在微服務的架構下也由原來的2PC這種變成了Saga。當然,對於事務其實您有很多種選擇,比如TCC,但Saga已經成為了事實上的標準。可是,事情並不如想像中那麼美妙,領域事件通常會基於訊息佇列來實現,在提交本地事務後需要傳送一個訊息讓其它的事務參與者進行自己的事務。那麼問題來了?如果本地事務提交成功,但傳送訊息到佇列失敗或訊息伺服器當機,要如何解決?如果同一個訊息被髮送了多次要怎麼辦?這些問題又引入了一系列的解決方案比如:本地訊息表、支援事務型訊息佇列、訊息冪處理等。您看吧,雖然只是一個事務問題,確引發了一場不小的技術改革。

  再回到聚合事務的話題中,兩個原則需要遵守:1)一個事務只能更新一個聚合;2)使用最終一致性實現多個聚合的事務,即使在一個BC中也儘量不要使用資料庫的強一致性事務。

  謝謝各位觀眾,我的自白完畢。我覺得相對於聚合的自白,我的應該更加通俗易懂,我們好賴也是個老司機了。其實如果再細琢磨琢磨他的話,貌似多少也有一些道理,只可惜他那種表達不夠理性,缺乏科技型文件所要求的嚴謹性。

總結

  本章使用了一種獨特的方式書寫,純粹的理論而且幾乎沒有任何程式碼。在此不免吐槽幾句,許多的程式設計師太急於求成了,在沒有理論的情況就開始落地DDD,難免他們會報怨說DDD不靠譜。主要原因還是缺少理論的支援或理解不夠。DDD最為核心的部分是其指導思想,作者常年在OO的圈子混,他說的一些東西我們不瞭解很正常,即使是從科班出來的工程師最開始學習的也是基於資料庫的程式設計方法,所以就需要對DDD理論進行細緻解讀才能更有效的在建設系統時使用。比如IDDD(實現領域驅動設計)這本書,裡面幾乎全是精華,字很小還500多頁呢。您可能也看到網上有許多關於DDD的文章,大部分都以理論為主,也不是說作者寫的不好,而是DDD本身就是理論的集合,你很難在不積累理論的情況下來有效的實施DDD,僅僅在看一些程式碼案例後就開搞,最終出來的東西也是東施效顰,不會在DDD中獲益。另外,您也可能在微信或QQ群中見到一些大牛,不要只羨慕他們的能力,人家瘋狂學習的時候您都沒看到。最後勸您一句:把浮躁的心收一收,踏下心來學習一下理論,不僅DDD如此,任何一門學問也都是一樣的。

 

相關文章