領域驅動設計,盒馬技術團隊這麼做

領域驅動設計,盒馬技術團隊這麼做

阿里妹導讀:好的設計模式、代碼架構可以大大降低產品的故障率,提高產品的質量。大家都使用的熟悉的設計模式未必是最好的設計模式,引入新的思想,並借鑑應用到自己的設計中,是正道。

今天,我們邀請盒馬資深技術專家輝子,分享他眼中的領域驅動設計及實踐經驗。

前言

從事技術多年,看了不少代碼,寫了不少代碼,在如何設計一個優秀軟件上也跟若干高手們做過各種討論和pk。在DDD(領域驅動設計)理念上各路高手也是觀點各異。

DDD只是一個流派,談不上壓倒性優勢,更不是完美無缺。 我更想跟大家分享的是我們是否關注設計本身,不管什麼流派的設計,有設計就是好的。

從我看到的代碼上來講,大部分代碼都不屬於DDD類型,有設計的也不多,更多的像“麵條代碼”,從端上一條線殺到數據庫完成一個操作。設計集中在數據庫(有時候數據庫設計都沒有,一堆字段也不知道是幹嘛用的),代碼更多是自我修養。我們依靠強大的測試保證了軟件的外部質量(向苦逼的測試們致敬),而內部質量在緊張的項目週期中屢屢得不到重視,陷入日復一日的技術負債中。

盒馬的業務更面向B端。從供應到配送證鏈條,整體性很強,關係複雜,不整理清楚,誰也搞不明白髮生什麼了。所以這裡設計很重要,不要給未來的兄弟挖坑。在我負責的模塊裡,我們完整地應用了DDD的方式去完成整個系統,其中有我們自己的思考和改變,在這裡我想給大家分享一下,他山之石可以攻玉。

領域模型探討

1、領域模型設計:基於對象vs基於數據庫

設計上我們通常從兩種維度入手:

a. Data Modeling:通過數據抽象系統關係,也就是數據庫設計

b. Object Modeling:通過面向對象方式抽象系統關係,也就是面向對象設計

大部分架構師都是從data modeling開始設計軟件系統,少部分人通過object modeling方式開始設計軟件系統。這兩種建模方式並不互相沖突,都很重要,但從哪個方向開始設計,對系統最終形態有很大的區別。

★ Data Model

領域模型(在這裡叫數據模型)對所有軟件從業者來講都不是一個陌生的名詞,一個軟件產品的內在質量好壞可能被領域模型清晰與否所決定,好的領域模型可以讓產品結構清楚,修改更方便,演進成本更低。在一個開發團隊裡,架構師很重要,他決定了軟件結構,這個結構決定了軟件未來的可讀性,可擴展性和可演進性。通常來說架構師設計領域模型,開發人員基於這個領域模型進行開發。“領域模型”是個潮流名詞,如果拉回到10幾年前,這個模型我們叫“數據字典”,說白了,領域模型就是數據庫設計。

架構師們在需求討論的過程中不停地演進更新這個數據字典,有些設計師會把這些字典寫成sql語句,這些語句形成了產品/項目數據庫的發育史,就像人類胚胎髮育:一個細胞(一個表),多個細胞(多個表),長出尾巴(設計有問題),又把尾巴縮掉(更新設計),最後哇哇落地(上線)。傳統項目中,架構師交給開發的一般是一本厚厚的概要設計文檔,裡面除了密密麻麻的文字就是分好了域的數據庫表設計。言下之意:數據庫設計是根本,一切開發圍繞著這本數據字典展開,形成類似於如下的架構圖:

領域驅動設計,盒馬技術團隊這麼做

在service層通過我們非常喜歡的manager去manage大部分的邏輯,POJO(稍後章節裡的失血模型)作為數據在manager手(上帝之手)裡不停地變換和組合,service層在這裡是一個巨大的加工工廠(很重的一層),圍繞著數據庫這份DNA,完成業務邏輯。舉個不恰當的例子:假如有父親和兒子這兩個表,生成的POJO應該是:

領域驅動設計,盒馬技術團隊這麼做

這時候兒子犯了點什麼錯,老爸非常不爽的扇了兒子一個耳光,老爸手疼,兒子臉疼。Manager通常這麼做:

領域驅動設計,盒馬技術團隊這麼做

這裡,manager充當了上帝的角色,扇個耳光都得他老人家幫忙。

★ Object Model

2004年,Eric Evans 發表了Domain-Driven Design –Tackling Complexity in the Heart of Software (領域驅動設計),簡稱Evans DDD,先在這裡給大家推薦這本書,書裡對領域驅動做了開創性的理論闡述。

在聊到DDD的時候,我經常會做一個假設:假設你的機器內存無限大,永遠不宕機

,在這個前提假設下,我們是不需要持久化數據的,也就是我們可以不需要數據庫,那麼你將會怎麼設計你的軟件?這就是我們說的Persistence Ignorance:持久化無關設計。

沒了數據庫,領域模型就要基於程序本身來設計了,熱愛設計模式的同學們可以在這裡大顯身手。在面向過程,面向函數,面向對象的編程語言中,面向對象無疑是領域建模最佳方式。類與表有點像(不少人認為表和類就是對應的,行row和對象object就是對應的),我個人強烈地不認同這種等同關係,這種認知直接導致了軟件設計變得沒有意義。類和表有以下幾個顯著區別,這些區別對領域建模的表達豐富度有顯著的差別,有了封裝、繼承、多態,我們對領域模型的表達要生動得多,對SOLID原則的遵守也會嚴謹很多。

  • 【引用】關係數據庫表表示多對多的關係是第三張表來實現,這個領域模型表示不具象化, 業務同學看不懂。
  • 【封裝】類可以設計方法,數據並不能完整地表達領域模型,數據表可以知道一個人三維,並不知道“一個人是可以跑的
    ”。
  • 【繼承、多態】類可以多態,數據上無法識別人與豬除了三維數據還有行為的區別,數據表不知道“一個人跑起來和一頭豬跑起來是不一樣的”。

再看看老子生氣扇兒子的例子:

領域驅動設計,盒馬技術團隊這麼做

根據這個思路,慢慢地,我們在面向對象的世界裡設計了栩栩如生的領域模型,service層就是基於這些模型做的業務操作(它變薄了,很多動作交給了domain objects去處理):

領域模型並不完成業務,每個domain object都是完成屬於自己應有的行為(single responsibility),就如同人跑這個動作,person.run是一個與業務無關的行為,但這個時候manger或者service在調用 some person.run的時候可能完成的100米比賽這個業務,也可能是完成跑去送外賣這個業務。這樣的話形成了類似於如下的架構圖:

領域驅動設計,盒馬技術團隊這麼做

我們回到假設,假設你的機器內存無限大,永遠不宕機,現在把假設去掉,沒有誰的機器是內存無限大,永遠不宕機的。去掉這個假設,我們需要數據庫,但數據庫的職責不再承載領域模型這個沉重的包袱了,數據庫迴歸persistence的本質,完成以下兩個事情:

【存】將對象數據持久化到存儲介質中

【取】高效地把數據查詢返回到內存中

由於不再承載領域建模這個特性,數據庫的設計可以變得天馬行空,任何可以加速存儲和搜索的手段都可以用上,我們可以用column數據庫,可以用document數據庫,可以設計非常精巧的中間表去完成大數據的查詢。總之數據庫設計要做的事情就是儘可能的高效存取,而不是完美表達領域模型(此言論有點反動,大家看看就好),這樣我們再看看架構圖:

領域驅動設計,盒馬技術團隊這麼做

這裡我想跟大家強調的是:

  • 領域模型是用於領域操作的,當然也可以用於查詢(read),不過這個查詢是代價的。在這個前提下,一個aggregate可能內含了若干數據,這些數據除了類似於getById這種方式,不適用多樣化查詢(query),領域驅動設計也不是為多樣化查詢設計的。
  • 查詢是基於數據庫的,所有的複雜變態查詢其實都應該繞過Domain層,直接與數據庫打交道。
  • 再精簡一下:領域操作->objects, 數據查詢->table rows。

2. 領域模型:失血、貧血、充血模型

失血、貧血、充血、脹血模型應該是Martin Fowler提出的,講述的是基於領域模型的豐滿程度下如何定義一個模型,有點像:瘦、中等、健壯、胖。【脹血(胖)模型太胖,在這裡我們不做討論】。

失血模型:基於數據庫的領域設計方式其實就是典型的失血模型

,以java為例,POJO只有簡單的基於field的setter,getter方法,POJO之間的關係隱藏在對象的某些ID裡,由外面的manager解釋,比如son.fatherId,Son並不知道他跟Father有關係,但manager會通過son.fatherId得到一個Father。

貧血模型:【盒馬流程中心】兒子不知道自己的父親是誰是不對的,不能每次都通過中間機構(Manager)驗DNA(son.fatherId)來找爸爸,領域模型可以更豐富一點,給son這個類修改一下:

領域驅動設計,盒馬技術團隊這麼做

son這個類變得豐富起來了,但還有一個小小的不方便,就是通過father無法獲得son(爸爸怎麼可以不知道兒子是誰),這樣我們再給Father添加這個屬性:

領域驅動設計,盒馬技術團隊這麼做

現在看著兩個類就豐滿多了,這也就是我們要說的貧血模型,在這個模型下家庭還算完美,父子相認。然而仔細研究這兩個類我們會發現一點問題:通常一個object是通過一個repository(數據庫查詢),或者factory(內存新建)得到的:

領域驅動設計,盒馬技術團隊這麼做

這個方法可以將一個son object從數據庫裡取出來,為了構建完整的son對象,sonRepo裡需要一個fatherRepo來構建一個father去賦值son.father。而fatherRepo在構建一個完整father的時候又需要sonRepo去構建一個son來賦值father.son。這形成了一個無向有環圈,這個循環調用問題是可以解決的,但為了解決這個問題,領域模型會變得有些噁心和將就。有向無環才是我們的設計目標,為了防止這個循環調用,我們是否可以在father和son這兩個類裡省略掉一個引用?修改一下Father這個類:

領域驅動設計,盒馬技術團隊這麼做

這樣在構造Father的時候就不會再構造一個Son了,但代價是我們在Father這個類裡引入了一個SonRepository, 也就是我們在一個domain對象裡引用了一個持久化操作,這就是我們說的充血模型

充血模型:【盒馬基礎資料中心】充血模型的存在讓domain object失去了血統的純正性,他不再是一個純的內存對象,這個對象裡埋藏了一個對數據庫的操作,這對測試是不友好的,我們不應該在做快速單元測試的時候連接數據庫,這個問題我們稍後來講。為保證模型的完整性,充血模型在有些情況下是必然存在的,比如在一個盒馬門店裡可以售賣好幾千個商品,每個商品有好幾百個屬性。如果我在構建一個店的時候把所有商品都拿出來,這個效率就太差了:

領域驅動設計,盒馬技術團隊這麼做

3. 領域模型下的依賴注入

簡單地對依賴注入說一說:

  • 依賴注入在runtime是一個singleton對象,只有在spring掃描範圍內的對象(@Component)才能通過annotation(@Autowired)用上依賴注入,通過new出來的對象是無法通過annotation得到注入的。
  • 個人推薦構造器依賴注入,這種情況下測試友好,對象構造完整性好,顯式地告訴你必須mock/stub哪個對象。

說完依賴注入我們再看剛才的充血模型

領域驅動設計,盒馬技術團隊這麼做

新建一個Father的時候需要賦值一個SonRepository,這顯然在寫代碼的時候是非常讓人惱火的事情,那麼我們是否希望可以通過依賴注入的方式把SonRepository注入進去呢?Father在這裡不可能是一個singleton對象,它可能在兩個場景下被new出來:新建、查詢,從Father的構造過程,SonRepository是無法注入的。這時工廠模式就顯示出其意義了(很多人認為工廠模式就是一擺設)

領域驅動設計,盒馬技術團隊這麼做

由於FatheFactory是系統生成的singleton對象,SonRepository自然可以注入到Factory裡,newFather方法隱藏了這個注入的sonRepo,這樣new一個Father對象就變乾淨了。

4. 領域模型:測試友好

失血模型和貧血模型是天然測試友好的(其實失血模型也沒啥好測試的),因為他們都是純內存對象。但實際應用中充血模型是存在的,要不就是把domain對象拆散,變得稍微不那麼優雅(當然可以,貧血和充血的戰爭從來就沒有斷過)。那麼在充血模型下,對象裡帶上了persisitence特性,這就對數據庫有了依賴,mock/stub掉這些依賴是高效單元化測試的基本要求,我們再看Father這個例子:

領域驅動設計,盒馬技術團隊這麼做

把SonRepository放到構造函數的意義就是為了測試的友好性,通過mock/stub這個Repository,單元測試就可以順利完成。

5. 領域模型:盒馬模式下repository的實現方式

按照object domain的思路,領域模型存在於內存對象裡,這些對象最終都要落到數據庫,由於擺脫了領域模型的束縛,數據庫設計是靈活多變的。在盒馬,domain object是怎麼進入到數據庫的呢?

領域驅動設計,盒馬技術團隊這麼做

在盒馬,我們獨特的設計了Tunnel這個接口,通過這個接口我們可以實現對domain對象在不同類型數據庫的存取。Repository並沒有直接進行持久化工作,而是將domain對象轉換成POJO交給Tunnel去做持久化工作,Tunnel具體實現可以在任何包實現,這樣,部署上,domain領域模型(domain objects+repositories)和持久化(Tunnels)完全的分開,domain包成為了單純的內存對象集。

6. 領域模型下的部署架構

盒馬業務具有很強的整體性:從供應商採購,到商品快遞到用戶手上,對象之間關係是比較明確的,原則上可以採用一個大而全的領域模型,也可以運用boundedContext方式拆分子域,並在交接處處理好數據傳送,這裡引用Martin Fowler的一幅圖:

領域驅動設計,盒馬技術團隊這麼做

我個人傾向於大domain的做法,我傾向(所以實際情況不是這樣的)的部署結構是:

領域驅動設計,盒馬技術團隊這麼做

說在結束的話

盒馬在架構設計上還在做更多的探索,在2B+互聯網的嶄新業務模式下,有很多可以深入探討的細節。DDD在盒馬已經邁出了堅實的第一步,並且在業務擴展性上,系統穩定性上經受了實戰的考驗。基於互聯網分佈式的工作流引擎(Noble),完全互聯網的圖形繪製引擎(Ivy)都在精心打磨中,期待在未來,盒馬工程師們給大家奉獻更多的設計作品。

領域驅動設計,盒馬技術團隊這麼做

每天一篇技術文章,

看不過癮?

發現更多AI乾貨。


分享到:


相關文章: