领域驱动设计,盒马技术团队这么做
?
阿里妹導(dǎo)讀:好的設(shè)計模式、代碼架構(gòu)可以大大降低產(chǎn)品的故障率,提高產(chǎn)品的質(zhì)量。大家都使用的熟悉的設(shè)計模式未必是最好的設(shè)計模式,引入新的思想,并借鑒應(yīng)用到自己的設(shè)計中,是正道。
今天,我們邀請盒馬資深技術(shù)專家輝子,分享他眼中的領(lǐng)域驅(qū)動設(shè)計及實踐經(jīng)驗。
前言
從事技術(shù)多年,看了不少代碼,寫了不少代碼,在如何設(shè)計一個優(yōu)秀軟件上也跟若干高手們做過各種討論和pk。在DDD(領(lǐng)域驅(qū)動設(shè)計)理念上各路高手也是觀點(diǎn)各異。
DDD只是一個流派,談不上壓倒性優(yōu)勢,更不是完美無缺。 我更想跟大家分享的是我們是否關(guān)注設(shè)計本身,不管什么流派的設(shè)計,有設(shè)計就是好的。
從我看到的代碼上來講,大部分代碼都不屬于DDD類型,有設(shè)計的也不多,更多的像“面條代碼”,從端上一條線殺到數(shù)據(jù)庫完成一個操作。設(shè)計集中在數(shù)據(jù)庫(有時候數(shù)據(jù)庫設(shè)計都沒有,一堆字段也不知道是干嘛用的),代碼更多是自我修養(yǎng)。我們依靠強(qiáng)大的測試保證了軟件的外部質(zhì)量(向苦逼的測試們致敬),而內(nèi)部質(zhì)量在緊張的項目周期中屢屢得不到重視,陷入日復(fù)一日的技術(shù)負(fù)債中。
盒馬的業(yè)務(wù)更面向B端。從供應(yīng)到配送證鏈條,整體性很強(qiáng),關(guān)系復(fù)雜,不整理清楚,誰也搞不明白發(fā)生什么了。所以這里設(shè)計很重要,不要給未來的兄弟挖坑。在我負(fù)責(zé)的模塊里,我們完整地應(yīng)用了DDD的方式去完成整個系統(tǒng),其中有我們自己的思考和改變,在這里我想給大家分享一下,他山之石可以攻玉。
領(lǐng)域模型探討
1、領(lǐng)域模型設(shè)計:基于對象vs基于數(shù)據(jù)庫
設(shè)計上我們通常從兩種維度入手:
a. Data Modeling:通過數(shù)據(jù)抽象系統(tǒng)關(guān)系,也就是數(shù)據(jù)庫設(shè)計
b. Object Modeling:通過面向?qū)ο蠓绞匠橄笙到y(tǒng)關(guān)系,也就是面向?qū)ο笤O(shè)計
大部分架構(gòu)師都是從data modeling開始設(shè)計軟件系統(tǒng),少部分人通過object modeling方式開始設(shè)計軟件系統(tǒng)。這兩種建模方式并不互相沖突,都很重要,但從哪個方向開始設(shè)計,對系統(tǒng)最終形態(tài)有很大的區(qū)別。
★ Data Model
領(lǐng)域模型(在這里叫數(shù)據(jù)模型)對所有軟件從業(yè)者來講都不是一個陌生的名詞,一個軟件產(chǎn)品的內(nèi)在質(zhì)量好壞可能被領(lǐng)域模型清晰與否所決定,好的領(lǐng)域模型可以讓產(chǎn)品結(jié)構(gòu)清楚,修改更方便,演進(jìn)成本更低。在一個開發(fā)團(tuán)隊里,架構(gòu)師很重要,他決定了軟件結(jié)構(gòu),這個結(jié)構(gòu)決定了軟件未來的可讀性,可擴(kuò)展性和可演進(jìn)性。通常來說架構(gòu)師設(shè)計領(lǐng)域模型,開發(fā)人員基于這個領(lǐng)域模型進(jìn)行開發(fā)。“領(lǐng)域模型”是個潮流名詞,如果拉回到10幾年前,這個模型我們叫“數(shù)據(jù)字典”,說白了,領(lǐng)域模型就是數(shù)據(jù)庫設(shè)計。
架構(gòu)師們在需求討論的過程中不停地演進(jìn)更新這個數(shù)據(jù)字典,有些設(shè)計師會把這些字典寫成sql語句,這些語句形成了產(chǎn)品/項目數(shù)據(jù)庫的發(fā)育史,就像人類胚胎發(fā)育:一個細(xì)胞(一個表),多個細(xì)胞(多個表),長出尾巴(設(shè)計有問題),又把尾巴縮掉(更新設(shè)計),最后哇哇落地(上線)。傳統(tǒng)項目中,架構(gòu)師交給開發(fā)的一般是一本厚厚的概要設(shè)計文檔,里面除了密密麻麻的文字就是分好了域的數(shù)據(jù)庫表設(shè)計。言下之意:數(shù)據(jù)庫設(shè)計是根本,一切開發(fā)圍繞著這本數(shù)據(jù)字典展開,形成類似于如下的架構(gòu)圖:
在service層通過我們非常喜歡的manager去manage大部分的邏輯,POJO(稍后章節(jié)里的失血模型)作為數(shù)據(jù)在manager手(上帝之手)里不停地變換和組合,service層在這里是一個巨大的加工工廠(很重的一層),圍繞著數(shù)據(jù)庫這份DNA,完成業(yè)務(wù)邏輯。舉個不恰當(dāng)?shù)睦?#xff1a;假如有父親和兒子這兩個表,生成的POJO應(yīng)該是:
這時候兒子犯了點(diǎn)什么錯,老爸非常不爽的扇了兒子一個耳光,老爸手疼,兒子臉疼。Manager通常這么做:
這里,manager充當(dāng)了上帝的角色,扇個耳光都得他老人家?guī)兔Α?/p>
★ Object Model
2004年,Eric Evans 發(fā)表了Domain-Driven Design –Tackling Complexity in the Heart of Software (領(lǐng)域驅(qū)動設(shè)計),簡稱Evans DDD,先在這里給大家推薦這本書,書里對領(lǐng)域驅(qū)動做了開創(chuàng)性的理論闡述。
在聊到DDD的時候,我經(jīng)常會做一個假設(shè):假設(shè)你的機(jī)器內(nèi)存無限大,永遠(yuǎn)不宕機(jī),在這個前提假設(shè)下,我們是不需要持久化數(shù)據(jù)的,也就是我們可以不需要數(shù)據(jù)庫,那么你將會怎么設(shè)計你的軟件?這就是我們說的Persistence Ignorance:持久化無關(guān)設(shè)計。
沒了數(shù)據(jù)庫,領(lǐng)域模型就要基于程序本身來設(shè)計了,熱愛設(shè)計模式的同學(xué)們可以在這里大顯身手。在面向過程,面向函數(shù),面向?qū)ο蟮木幊陶Z言中,面向?qū)ο鬅o疑是領(lǐng)域建模最佳方式。類與表有點(diǎn)像(不少人認(rèn)為表和類就是對應(yīng)的,行row和對象object就是對應(yīng)的),我個人強(qiáng)烈地不認(rèn)同這種等同關(guān)系,這種認(rèn)知直接導(dǎo)致了軟件設(shè)計變得沒有意義。類和表有以下幾個顯著區(qū)別,這些區(qū)別對領(lǐng)域建模的表達(dá)豐富度有顯著的差別,有了封裝、繼承、多態(tài),我們對領(lǐng)域模型的表達(dá)要生動得多,對SOLID原則的遵守也會嚴(yán)謹(jǐn)很多。
- 【引用】關(guān)系數(shù)據(jù)庫表表示多對多的關(guān)系是第三張表來實現(xiàn),這個領(lǐng)域模型表示不具象化, 業(yè)務(wù)同學(xué)看不懂。
- 【封裝】類可以設(shè)計方法,數(shù)據(jù)并不能完整地表達(dá)領(lǐng)域模型,數(shù)據(jù)表可以知道一個人三維,并不知道“一個人是可以跑的”。
- 【繼承、多態(tài)】類可以多態(tài),數(shù)據(jù)上無法識別人與豬除了三維數(shù)據(jù)還有行為的區(qū)別,數(shù)據(jù)表不知道“一個人跑起來和一頭豬跑起來是不一樣的”。
再看看老子生氣扇兒子的例子:
根據(jù)這個思路,慢慢地,我們在面向?qū)ο蟮氖澜缋镌O(shè)計了栩栩如生的領(lǐng)域模型,service層就是基于這些模型做的業(yè)務(wù)操作(它變薄了,很多動作交給了domain objects去處理):領(lǐng)域模型并不完成業(yè)務(wù),每個domain object都是完成屬于自己應(yīng)有的行為(single responsibility),就如同人跑這個動作,person.run是一個與業(yè)務(wù)無關(guān)的行為,但這個時候manger或者service在調(diào)用 some person.run的時候可能完成的100米比賽這個業(yè)務(wù),也可能是完成跑去送外賣這個業(yè)務(wù)。這樣的話形成了類似于如下的架構(gòu)圖:
我們回到假設(shè),假設(shè)你的機(jī)器內(nèi)存無限大,永遠(yuǎn)不宕機(jī),現(xiàn)在把假設(shè)去掉,沒有誰的機(jī)器是內(nèi)存無限大,永遠(yuǎn)不宕機(jī)的。去掉這個假設(shè),我們需要數(shù)據(jù)庫,但數(shù)據(jù)庫的職責(zé)不再承載領(lǐng)域模型這個沉重的包袱了,數(shù)據(jù)庫回歸persistence的本質(zhì),完成以下兩個事情:
【存】將對象數(shù)據(jù)持久化到存儲介質(zhì)中
【取】高效地把數(shù)據(jù)查詢返回到內(nèi)存中
由于不再承載領(lǐng)域建模這個特性,數(shù)據(jù)庫的設(shè)計可以變得天馬行空,任何可以加速存儲和搜索的手段都可以用上,我們可以用column數(shù)據(jù)庫,可以用document數(shù)據(jù)庫,可以設(shè)計非常精巧的中間表去完成大數(shù)據(jù)的查詢。總之?dāng)?shù)據(jù)庫設(shè)計要做的事情就是盡可能的高效存取,而不是完美表達(dá)領(lǐng)域模型(此言論有點(diǎn)反動,大家看看就好),這樣我們再看看架構(gòu)圖:
這里我想跟大家強(qiáng)調(diào)的是:
- 領(lǐng)域模型是用于領(lǐng)域操作的,當(dāng)然也可以用于查詢(read),不過這個查詢是代價的。在這個前提下,一個aggregate可能內(nèi)含了若干數(shù)據(jù),這些數(shù)據(jù)除了類似于getById這種方式,不適用多樣化查詢(query),領(lǐng)域驅(qū)動設(shè)計也不是為多樣化查詢設(shè)計的。
- 查詢是基于數(shù)據(jù)庫的,所有的復(fù)雜變態(tài)查詢其實都應(yīng)該繞過Domain層,直接與數(shù)據(jù)庫打交道。
- 再精簡一下:領(lǐng)域操作->objects, 數(shù)據(jù)查詢->table rows。
2. 領(lǐng)域模型:失血、貧血、充血模型
失血、貧血、充血、脹血模型應(yīng)該是Martin Fowler提出的,講述的是基于領(lǐng)域模型的豐滿程度下如何定義一個模型,有點(diǎn)像:瘦、中等、健壯、胖。【脹血(胖)模型太胖,在這里我們不做討論】。
失血模型:基于數(shù)據(jù)庫的領(lǐng)域設(shè)計方式其實就是典型的失血模型,以java為例,POJO只有簡單的基于field的setter,getter方法,POJO之間的關(guān)系隱藏在對象的某些ID里,由外面的manager解釋,比如son.fatherId,Son并不知道他跟Father有關(guān)系,但manager會通過son.fatherId得到一個Father。
貧血模型:【盒馬流程中心】兒子不知道自己的父親是誰是不對的,不能每次都通過中間機(jī)構(gòu)(Manager)驗DNA(son.fatherId)來找爸爸,領(lǐng)域模型可以更豐富一點(diǎn),給son這個類修改一下:
son這個類變得豐富起來了,但還有一個小小的不方便,就是通過father無法獲得son(爸爸怎么可以不知道兒子是誰),這樣我們再給Father添加這個屬性:
現(xiàn)在看著兩個類就豐滿多了,這也就是我們要說的貧血模型,在這個模型下家庭還算完美,父子相認(rèn)。然而仔細(xì)研究這兩個類我們會發(fā)現(xiàn)一點(diǎn)問題:通常一個object是通過一個repository(數(shù)據(jù)庫查詢),或者factory(內(nèi)存新建)得到的:
這個方法可以將一個son object從數(shù)據(jù)庫里取出來,為了構(gòu)建完整的son對象,sonRepo里需要一個fatherRepo來構(gòu)建一個father去賦值son.father。而fatherRepo在構(gòu)建一個完整father的時候又需要sonRepo去構(gòu)建一個son來賦值father.son。這形成了一個無向有環(huán)圈,這個循環(huán)調(diào)用問題是可以解決的,但為了解決這個問題,領(lǐng)域模型會變得有些惡心和將就。有向無環(huán)才是我們的設(shè)計目標(biāo),為了防止這個循環(huán)調(diào)用,我們是否可以在father和son這兩個類里省略掉一個引用?修改一下Father這個類:
這樣在構(gòu)造Father的時候就不會再構(gòu)造一個Son了,但代價是我們在Father這個類里引入了一個SonRepository, 也就是我們在一個domain對象里引用了一個持久化操作,這就是我們說的充血模型。
充血模型:【盒馬基礎(chǔ)資料中心】充血模型的存在讓domain object失去了血統(tǒng)的純正性,他不再是一個純的內(nèi)存對象,這個對象里埋藏了一個對數(shù)據(jù)庫的操作,這對測試是不友好的,我們不應(yīng)該在做快速單元測試的時候連接數(shù)據(jù)庫,這個問題我們稍后來講。為保證模型的完整性,充血模型在有些情況下是必然存在的,比如在一個盒馬門店里可以售賣好幾千個商品,每個商品有好幾百個屬性。如果我在構(gòu)建一個店的時候把所有商品都拿出來,這個效率就太差了:
3. 領(lǐng)域模型下的依賴注入
簡單地對依賴注入說一說:
- 依賴注入在runtime是一個singleton對象,只有在spring掃描范圍內(nèi)的對象(@Component)才能通過annotation(@Autowired)用上依賴注入,通過new出來的對象是無法通過annotation得到注入的。
- 個人推薦構(gòu)造器依賴注入,這種情況下測試友好,對象構(gòu)造完整性好,顯式地告訴你必須mock/stub哪個對象。
說完依賴注入我們再看剛才的充血模型
新建一個Father的時候需要賦值一個SonRepository,這顯然在寫代碼的時候是非常讓人惱火的事情,那么我們是否希望可以通過依賴注入的方式把SonRepository注入進(jìn)去呢?Father在這里不可能是一個singleton對象,它可能在兩個場景下被new出來:新建、查詢,從Father的構(gòu)造過程,SonRepository是無法注入的。這時工廠模式就顯示出其意義了(很多人認(rèn)為工廠模式就是一擺設(shè))
由于FatheFactory是系統(tǒng)生成的singleton對象,SonRepository自然可以注入到Factory里,newFather方法隱藏了這個注入的sonRepo,這樣new一個Father對象就變干凈了。
4. 領(lǐng)域模型:測試友好
失血模型和貧血模型是天然測試友好的(其實失血模型也沒啥好測試的),因為他們都是純內(nèi)存對象。但實際應(yīng)用中充血模型是存在的,要不就是把domain對象拆散,變得稍微不那么優(yōu)雅(當(dāng)然可以,貧血和充血的戰(zhàn)爭從來就沒有斷過)。那么在充血模型下,對象里帶上了persisitence特性,這就對數(shù)據(jù)庫有了依賴,mock/stub掉這些依賴是高效單元化測試的基本要求,我們再看Father這個例子:
把SonRepository放到構(gòu)造函數(shù)的意義就是為了測試的友好性,通過mock/stub這個Repository,單元測試就可以順利完成。
5. 領(lǐng)域模型:盒馬模式下repository的實現(xiàn)方式
按照object domain的思路,領(lǐng)域模型存在于內(nèi)存對象里,這些對象最終都要落到數(shù)據(jù)庫,由于擺脫了領(lǐng)域模型的束縛,數(shù)據(jù)庫設(shè)計是靈活多變的。在盒馬,domain object是怎么進(jìn)入到數(shù)據(jù)庫的呢?
在盒馬,我們獨(dú)特的設(shè)計了Tunnel這個接口,通過這個接口我們可以實現(xiàn)對domain對象在不同類型數(shù)據(jù)庫的存取。Repository并沒有直接進(jìn)行持久化工作,而是將domain對象轉(zhuǎn)換成POJO交給Tunnel去做持久化工作,Tunnel具體實現(xiàn)可以在任何包實現(xiàn),這樣,部署上,domain領(lǐng)域模型(domain objects+repositories)和持久化(Tunnels)完全的分開,domain包成為了單純的內(nèi)存對象集。
6. 領(lǐng)域模型下的部署架構(gòu)
盒馬業(yè)務(wù)具有很強(qiáng)的整體性:從供應(yīng)商采購,到商品快遞到用戶手上,對象之間關(guān)系是比較明確的,原則上可以采用一個大而全的領(lǐng)域模型,也可以運(yùn)用boundedContext方式拆分子域,并在交接處處理好數(shù)據(jù)傳送,這里引用Martin Fowler的一幅圖:
我個人傾向于大domain的做法,我傾向(所以實際情況不是這樣的)的部署結(jié)構(gòu)是:
說在結(jié)束的話
盒馬在架構(gòu)設(shè)計上還在做更多的探索,在2B+互聯(lián)網(wǎng)的嶄新業(yè)務(wù)模式下,有很多可以深入探討的細(xì)節(jié)。DDD在盒馬已經(jīng)邁出了堅實的第一步,并且在業(yè)務(wù)擴(kuò)展性上,系統(tǒng)穩(wěn)定性上經(jīng)受了實戰(zhàn)的考驗。基于互聯(lián)網(wǎng)分布式的工作流引擎(Noble),完全互聯(lián)網(wǎng)的圖形繪制引擎(Ivy)都在精心打磨中,期待在未來,盒馬工程師們給大家奉獻(xiàn)更多的設(shè)計作品。
?
每天一篇技術(shù)文章,
看不過癮?
關(guān)注“阿里巴巴機(jī)器智能”微信公眾號
發(fā)現(xiàn)更多AI干貨。
總結(jié)
以上是生活随笔為你收集整理的领域驱动设计,盒马技术团队这么做的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 深度解析 | 基于DAG的分布式任务调度
- 下一篇: 从技术角度聊聊,短视频为何让人停不下来?