日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

阿里領(lǐng)域驅(qū)動設(shè)計(jì)實(shí)踐

 萬皇之皇 2018-01-15

前言

設(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)部大部分代碼都不屬于
DDD
類型,有設(shè)計(jì)的也不多,更多的像“面條代碼”,從端上一條線殺到數(shù)據(jù)庫完成一個操作,僅有的一些設(shè)計(jì)集中在數(shù)據(jù)庫上。我們依靠強(qiáng)大的測試保證了軟件的外部質(zhì)量(向苦逼的測試們致敬),而內(nèi)部質(zhì)量在緊張的項(xiàng)目周期中屢屢得不到重視,陷入日復(fù)一日的技術(shù)負(fù)債中。

一直想寫點(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:
通過面向?qū)ο蠓绞匠橄笙到y(tǒng)關(guān)系,也就是面向?qū)ο笤O(shè)計(jì)大部分架構(gòu)師都是從 Data Modeling 開始設(shè)計(jì)軟件系統(tǒng),少部分人通過 Object
Modeling 方式開始設(shè)計(jì)軟件系統(tǒng)。這兩種建模方式并不互相沖突,都很重要,但從哪個方向開始設(shè)計(jì),對系統(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)隊(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ì)師會把這些字典寫成
SQL 語句,這些語句形成了產(chǎn)品 /
項(xiàng)目數(shù)據(jù)庫的發(fā)育史,就像人類胚胎發(fā)育:一個細(xì)胞(一個表),多個細(xì)胞(多個表),長出尾巴(設(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
大部分的邏輯,POJO(后文失血模型會講到)作為數(shù)據(jù)在 manager 手(上帝之手)里不停地變換和組合,service
層在這里是一個巨大的加工工廠(很重的一層),圍繞著數(shù)據(jù)庫這份 DNA,完成業(yè)務(wù)邏輯。

舉個不恰當(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
年,Eric Evans 發(fā)表了《Domain-Driven Design –Tackling Complexity in the Heart
of Software》(領(lǐng)域驅(qū)動設(shè)計(jì)),簡稱 Evans DDD,先在這里給大家推薦這本書,書里對領(lǐng)域驅(qū)動做了開創(chuàng)性的理論闡述。

在聊到 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
層就是基于這些模型做的業(yè)務(wù)操作(它變薄了,很多動作交給了 domain objects 去處理):領(lǐng)域模型并不完成業(yè)務(wù),每個 domain
object 都是完成屬于自己應(yīng)有的行為(single responsibility),就如同人跑這個動作,person.run
是一個與業(yè)務(wù)無關(guān)的行為,但這個時候 manager 或者 service 在調(diào)用 some person.run 的時候可以完成 100
米比賽這個業(yè)務(wù),也可以完成跑去送外賣這個業(yè)務(wù)。這樣的話形成了類似于下邊的架構(gòu)圖:

我們回到剛才的假設(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ì)可以變得天馬行空,任何可以加速存儲和搜索的手段都可以用上,我們可以用
column 數(shù)據(jù)庫,可以用 document
數(shù)據(jù)庫,可以設(shè)計(jì)非常精巧的中間表去完成大數(shù)據(jù)的查詢。總之?dāng)?shù)據(jù)庫設(shè)計(jì)要做的事情就是盡可能高效存取,而不是完美表達(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è)計(jì)也不是為多樣化查詢設(shè)計(jì)的。

查詢是基于數(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í)就是典型的失血模型,以
Java 為例,POJO 只有簡單的基于 field 的 setter、getter 方法,POJO 之間的關(guān)系隱藏在對象的某些 ID
里,由外面的 manager 解釋,比如 son.fatherId,Son 并不知道他跟 Father 有關(guān)系,但 manager 會通過
son.fatherId 得到一個 Father。

貧血模型:兒子不知道自己的父親是誰是不對的,不能每次都通過中間機(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);

這個方法可以將一個
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è)計(jì)目標(biāo),為了防止這個循環(huán)調(diào)用,我們是否可以在
Father 和 Son 這兩個類里省略掉一個引用?修改一下 Father 這個類:

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 對象里引用了一個持久化操作,這就是我們說的充血模型。

充血模型:充血模型的存在讓
domain object
失去了血統(tǒng)的純正性,他不再是一個純的內(nèi)存對象,這個對象里埋藏了一個對數(shù)據(jù)庫的操作,這對測試是不友好的,我們不應(yīng)該在做快速單元測試的時候連接數(shù)據(jù)庫,這個問題我們稍后來講。為保證模型的完整性,充血模型在有些情況下是必然存在的,比如在一個盒馬門店里可以售賣好幾千個商品,每個商品有好幾百個屬性。如果我在構(gòu)建一個店的時候把所有商品都拿出來,這個效率就太差了:

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)域模型:依賴注入

簡單說一說依賴注入:

依賴注入在
runtime 是一個 singleton 對象,只有在 spring 掃描范圍內(nèi)的對象(@Component)才能通過
annotation(@Autowired)用上依賴注入,通過 new 出來的對象是無法通過 annotation 得到注入的。

個人推薦構(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;}

}

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

@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)用中充血模型是存在的,要不就是把
domain 對象拆散,變得稍微不那么優(yōu)雅(當(dāng)然可以,貧血和充血的戰(zhàn)爭從來就沒有斷過)。那么在充血模型下,對象里帶上了
persisitence 特性,這就對數(shù)據(jù)庫有了依賴,mock/stub 掉這些依賴是高效單元化測試的基本要求,我們再看 Father
這個例子:

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
對象在不同類型數(shù)據(jù)庫的存取。Repository 并沒有直接進(jìn)行持久化工作,而是將 domain 對象轉(zhuǎn)換成 POJO 交給 Tunnel
去做持久化工作,Tunnel 具體可以在任何包實(shí)現(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ù)傳送,這里引用老馬的一幅圖:

我個人傾向于大 domain 的做法,我傾向(所以實(shí)際情況不是這樣的)的部署結(jié)構(gòu)是:

結(jié)語

盒馬在架構(gòu)設(shè)計(jì)上還在做更多的探索,在 2B+
互聯(lián)網(wǎng)的嶄新業(yè)務(wù)模式下,有很多可以深入探討的細(xì)節(jié)。DDD
在盒馬已經(jīng)邁出了堅(jiān)實(shí)的第一步,并且在業(yè)務(wù)擴(kuò)展性和系統(tǒng)穩(wěn)定性上經(jīng)受了實(shí)戰(zhàn)的考驗(yàn)。基于互聯(lián)網(wǎng)分布式的工作流引擎(Noble),完全互聯(lián)網(wǎng)的圖形繪制引擎(Ivy)都在精心打磨中,期待在未來的就幾個月里,盒馬工程師們給大家奉獻(xiàn)更多的設(shè)計(jì)作品。

作者介紹

張群輝,阿里盒馬架構(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ù)一線。

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約