前言 設(shè)計(jì)是把雙刃劍,沒有最好的,也沒有更好的,而是條條大路到杭州。同時不設(shè)計(jì)和過度設(shè)計(jì)都是有問題的,恰到好處的設(shè)計(jì)才是我們追求的極致。 DDD(Domain-Driven Design,領(lǐng)域驅(qū)動設(shè)計(jì))只是一個流派,談不上壓倒性優(yōu)勢,更不是完美無缺。 我更想跟大家分享的是我們是否關(guān)注設(shè)計(jì)本身,不管什么流派的設(shè)計(jì),有設(shè)計(jì)就是好的。 從我看到的代碼上來講,阿里集團(tuán)內(nèi)部大部分代碼都不屬于 一直想寫點(diǎn)什么喚起大家的設(shè)計(jì)意識,但不知道寫點(diǎn)什么合適。去年轉(zhuǎn)到盒馬,有了更多的機(jī)會寫代碼,可以從無到有去構(gòu)建一個系統(tǒng)。盒馬跟集團(tuán)大多數(shù)業(yè)務(wù)不同,盒馬的業(yè)務(wù)更面向 B 端,從供應(yīng)到配送鏈條,整體性很強(qiáng),關(guān)系復(fù)雜,不整理清楚,誰也搞不明白發(fā)生什么了。所以這里設(shè)計(jì)很重要,不設(shè)計(jì)的代碼今天不死也是拖到明天去死,不管我們在盒馬待多久,不能給未來的兄弟挖坑啊。在我負(fù)責(zé)的模塊里,我們完整地應(yīng)用了 DDD 的方式去完成整個系統(tǒng),其中有我們自己的思考和改變,在這里我想給大家分享一下,他山之石可以攻玉,大家可以借鑒。 領(lǐng)域模型探討 1. 領(lǐng)域模型設(shè)計(jì):基于數(shù)據(jù)庫 vs 基于對象 設(shè)計(jì)上我們通常從兩種維度入手: Data Modeling: 通過數(shù)據(jù)抽象系統(tǒng)關(guān)系,也就是數(shù)據(jù)庫設(shè)計(jì) Object Modeling: 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)隊(duì)里,架構(gòu)師很重要,他決定了軟件結(jié)構(gòu),這個結(jié)構(gòu)決定了軟件未來的可讀性、可擴(kuò)展性和可演進(jìn)性。通常來說架構(gòu)師設(shè)計(jì)領(lǐng)域模型,開發(fā)人員基于這個領(lǐng)域模型進(jìn)行開發(fā)?!邦I(lǐng)域模型”是個潮流名詞,如果拉回到 10 幾年前,這個模型我們叫“數(shù)據(jù)字典”,說白了,領(lǐng)域模型就是數(shù)據(jù)庫設(shè)計(jì)。 架構(gòu)師們在需求討論的過程中不停地演進(jìn)更新這個數(shù)據(jù)字典,有些設(shè)計(jì)師會把這些字典寫成 傳統(tǒng)項(xiàng)目中,架構(gòu)師交給開發(fā)的一般是一本厚厚的概要設(shè)計(jì)文檔,里面除了密密麻麻的文字就是分好了域的數(shù)據(jù)庫表設(shè)計(jì)。言下之意:數(shù)據(jù)庫設(shè)計(jì)是根本,一切開發(fā)圍繞著這本數(shù)據(jù)字典展開,形成類似于下邊的架構(gòu)圖: 在 service 層通過我們非常喜歡的 manager 去 manage 舉個不恰當(dāng)?shù)睦樱杭偃缬懈赣H和兒子這兩個表,生成的 POJO 應(yīng)該是: public class Father{…} public class Son{ private String fatherId;//son 表里有 fatherId 作為 Father 表 id 外鍵 public String getFatherId(){ return fatherId; } …… } 這時候兒子犯了點(diǎn)什么錯,老爸非常不爽地扇了兒子一個耳光,老爸手疼,兒子臉疼。Manager 通常這么做: public class SomeManager{ public void fatherSlapSon(Father father, Son son){ // 如果邏輯上說不通,大家忍忍 father.setPainOnHand(); son.setPainOnFace();// 假設(shè) painOnHand, painOnFace 都是數(shù)據(jù)庫字段 } } 這里,manager 充當(dāng)了上帝的角色,扇個耳光都得他老人家?guī)兔Α?/p> Object Model 2004 在聊到 DDD 的時候,我經(jīng)常會做一個假設(shè):假設(shè)你的機(jī)器內(nèi)存無限大,永遠(yuǎn)不宕機(jī),在這個前提下,我們是不需要持久化數(shù)據(jù)的,也就是我們可以不需要數(shù)據(jù)庫,那么你將會怎么設(shè)計(jì)你的軟件?這就是我們說的 Persistence Ignorance:持久化無關(guān)設(shè)計(jì)。 沒了數(shù)據(jù)庫,領(lǐng)域模型就要基于程序本身來設(shè)計(jì)了,熱愛設(shè)計(jì)模式的同學(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è)計(jì)變得沒有意義。 類和表有以下幾個顯著區(qū)別,這些區(qū)別對領(lǐng)域建模的表達(dá)豐富度有顯著的差別,有了封裝、繼承和多態(tài),我們對領(lǐng)域模型的表達(dá)要生動得多,對 SOLID 原則的遵守也會嚴(yán)謹(jǐn)很多: 引用:關(guān)系數(shù)據(jù)庫表表示多對多的關(guān)系是用第三張表來實(shí)現(xiàn),這個領(lǐng)域模型表示不具象化, 業(yè)務(wù)同學(xué)看不懂。 封裝:類可以設(shè)計(jì)方法,數(shù)據(jù)并不能完整地表達(dá)領(lǐng)域模型,數(shù)據(jù)表可以知道一個人的三維,但并不知道“一個人是可以跑的”。 繼承、多態(tài):類可以多態(tài),數(shù)據(jù)上無法識別人與豬除了三維數(shù)據(jù)還有行為的區(qū)別,數(shù)據(jù)表不知道“一個人跑起來和一頭豬跑起來是不一樣的”。 再看看老子生氣扇兒子的例子: public class Father{ // 教訓(xùn)兒子是自己的事情,并不需要別人幫忙,上帝也不行 public void slapSon(Son son){ this.setPainOnHand(); son.setPainOnFace(); } } 根據(jù)這個思路,慢慢地,我們在面向?qū)ο蟮氖澜缋镌O(shè)計(jì)了栩栩如生的領(lǐng)域模型,service 我們回到剛才的假設(shè),現(xiàn)在把假設(shè)去掉,沒有誰的機(jī)器是內(nèi)存無限大,永遠(yuǎn)不宕機(jī)的,那么我們需要數(shù)據(jù)庫,但數(shù)據(jù)庫的職責(zé)不再承載領(lǐng)域模型這個沉重的包袱了,數(shù)據(jù)庫回歸 persistence 的本質(zhì),完成以下兩個事情: 存:將對象數(shù)據(jù)持久化到存儲介質(zhì)中。 取:高效地把數(shù)據(jù)查詢返回到內(nèi)存中。 由于不再承載領(lǐng)域建模這個特性,數(shù)據(jù)庫的設(shè)計(jì)可以變得天馬行空,任何可以加速存儲和搜索的手段都可以用上,我們可以用 這里我想跟大家強(qiáng)調(diào)的是: 領(lǐng)域模型是用于領(lǐng)域操作的,當(dāng)然也可以用于查詢(read),不過這個查詢是有代價的。在這個前提下,一個 查詢是基于數(shù)據(jù)庫的,所有的復(fù)雜變態(tài)查詢其實(shí)都應(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è)計(jì)方式其實(shí)就是典型的失血模型,以 貧血模型:兒子不知道自己的父親是誰是不對的,不能每次都通過中間機(jī)構(gòu)(Manager)驗(yàn) DNA(son.fatherId) 來找爸爸,領(lǐng)域模型可以更豐富一點(diǎn),給 son 這個類修改一下: public class Son{ private Father father; public Father getFather(){return this.father;} } Son 這個類變得豐富起來了,但還有一個小小的不方便,就是通過 Father 無法獲得 Son,爸爸怎么可以不知道兒子是誰?這樣我們再給 Father 添加這個屬性: public class Father{ private Son son; private Son getSon(){return this.son;} } 現(xiàn)在看著兩個類就豐滿多了,這也就是我們要說的貧血模型,在這個模型下家庭還算完美,父子相認(rèn)。然而仔細(xì)研究這兩個類我們會發(fā)現(xiàn)一點(diǎn)問題:通常一個 object 是通過一個 repository(數(shù)據(jù)庫查詢),或者 factory(內(nèi)存新建)得到的: Son someSon = sonRepo.getById(12345); 這個方法可以將一個 public class Father{ //private Son son; 刪除這個引用 private SonRepository sonRepo;// 添加一個 Son 的 repo private getSon(){return sonRepo.getByFatherId(this.id);} } 這樣在構(gòu)造 Father 的時候就不會再構(gòu)造一個 Son 了,但代價是我們在 Father 這個類里引入了一個 SonRepository,也就是我們在一個 domain 對象里引用了一個持久化操作,這就是我們說的充血模型。 充血模型:充血模型的存在讓 public class Shop{ //private List products; 這個商品列表在構(gòu)建時太大了 private ProductRepository productRepo; public List getProducts(){ //return this.products; return productRepo.getShopProducts(this.id); } } 3. 領(lǐng)域模型:依賴注入 簡單說一說依賴注入: 依賴注入在 個人推薦構(gòu)造器依賴注入,這種情況下測試友好,對象構(gòu)造完整性好,顯式地告訴你必須 mock/stub 哪個對象。 說完依賴注入我們再看剛才的充血模型: public class Father{ private SonRepository sonRepo; private Son getSon(){return sonRepo.getByFatherId(this.id);} public Father(SonRepository sonRepo){this.sonRepo = sonRepo;} } 新建一個 @Component public class FatherFactory{ private SonRepository sonRepo; @Autowired public FatherFactory(SonRepository sonRepo){} public Father createFather(){ return new Father(sonRepo); } } 由于 FatheFactory 是系統(tǒng)生成的 singleton 對象,SonRepository 自然可以注入到 Factory 里,newFather 方法隱藏了這個注入的 sonRepo,這樣 new 一個 Father 對象就變干凈了。 4. 領(lǐng)域模型:測試友好 失血模型和貧血模型是天然測試友好的(其實(shí)失血模型也沒啥好測試的),因?yàn)樗麄兌际羌儍?nèi)存對象。但實(shí)際應(yīng)用中充血模型是存在的,要不就是把 public class Father{ private SonRepository sonRepo;//=new SonRepository() 這里不能構(gòu)造 private getSon(){return sonRepo.getByFatherId(this.id);} // 放到構(gòu)造函數(shù)里 public Father(SonRepository sonRepo){this.sonRepo = sonRepo;} } 把 SonRepository 放到構(gòu)造函數(shù)的意義就是為了測試的友好性,通過 mock/stub 這個 Repository,單元測試就可以順利完成。 5. 領(lǐng)域模型:盒馬模式下 repository 的實(shí)現(xiàn)方式 按照 object domain 的思路,領(lǐng)域模型存在于內(nèi)存對象里,這些對象最終都要落到數(shù)據(jù)庫,由于擺脫了領(lǐng)域模型的束縛,數(shù)據(jù)庫設(shè)計(jì)是靈活多變的。在盒馬,domain object 是怎么進(jìn)入到數(shù)據(jù)庫的呢。 在盒馬,我們設(shè)計(jì)了 Tunnel 這個獨(dú)特的接口,通過這個接口我們可以實(shí)現(xiàn)對 domain 6. 領(lǐng)域模型:部署架構(gòu) 盒馬業(yè)務(wù)具有很強(qiáng)的整體性:從供應(yīng)商采購,到商品快遞到用戶手上,對象之間關(guān)系是比較明確的,原則上可以采用一個大而全的領(lǐng)域模型,也可以運(yùn)用 boundedContext 方式拆分子域,并在交接處處理好數(shù)據(jù)傳送,這里引用老馬的一幅圖: 我個人傾向于大 domain 的做法,我傾向(所以實(shí)際情況不是這樣的)的部署結(jié)構(gòu)是: 結(jié)語 盒馬在架構(gòu)設(shè)計(jì)上還在做更多的探索,在 2B+ 作者介紹 張群輝,阿里盒馬架構(gòu)總監(jiān)。10 多年技術(shù)及管理實(shí)戰(zhàn)經(jīng)驗(yàn),前阿里基礎(chǔ)機(jī)構(gòu)事業(yè)部工程效率總監(jiān),長期在一線指導(dǎo)大型復(fù)雜系統(tǒng)的架構(gòu)設(shè)計(jì)。DevOps、微服務(wù)架構(gòu)及領(lǐng)域驅(qū)動設(shè)計(jì)國內(nèi)最早的實(shí)踐者一員。崇尚實(shí)踐出真知,一直奮斗在技術(shù)一線。 |
|
來自: 萬皇之皇 > 《IT互聯(lián)》