從MVC到DDD的架構(gòu)演進(jìn)

DDD這幾年越來(lái)越火渐排,資料也很多屈暗,大部分的資料都偏向于理論介紹妨蛹,有給出的代碼與傳統(tǒng)MVC的三層架構(gòu)差異較大屏富,再加上大量的新概念很容易讓初學(xué)者望而卻步。本文從MVC架構(gòu)角度來(lái)講解如何演進(jìn)到DDD架構(gòu)蛙卤。

從DDD的角度看MVC架構(gòu)的問題

代碼角度:

  • 瘦實(shí)體模型:只起到數(shù)據(jù)類的作用狠半,業(yè)務(wù)邏輯散落到service,可維護(hù)性越來(lái)越差表窘;
  • 面向數(shù)據(jù)庫(kù)表編程典予,而非模型編程;
  • 實(shí)體類之間的關(guān)系是復(fù)雜的網(wǎng)狀結(jié)構(gòu)乐严,成為大泥球瘤袖,牽一發(fā)而動(dòng)全身,導(dǎo)致不敢輕易改代碼昂验;
  • service類承接的所有的業(yè)務(wù)邏輯捂敌,越來(lái)越臃腫艾扮,很容易出現(xiàn)幾千行的service類;
  • 對(duì)外接口直接暴露實(shí)體模型占婉,導(dǎo)致不必要開放內(nèi)部邏輯對(duì)外暴露泡嘴,就算有DTO類一般也是實(shí)體類的直接copy;
  • 外部依賴層直接從service層調(diào)用逆济,字段轉(zhuǎn)換酌予、異常處理大量充斥在service方法中;

項(xiàng)目管理角度:

  • 交付效率:越來(lái)越低奖慌;
  • 穩(wěn)定性差:不好測(cè)試抛虫,代碼改動(dòng)的影響范圍不好預(yù)估;
  • 理解成本高:新成員介入成本高简僧,長(zhǎng)期會(huì)導(dǎo)致模塊只有一個(gè)人最熟悉建椰,離職成本很大;

第一層:初出茅廬

以上的問題越來(lái)越嚴(yán)重岛马,很多人開始把眼光轉(zhuǎn)向DDD棉姐,于是埋頭啃了幾本大部頭的書,對(duì)以下概念有了基本的了解:

  • 統(tǒng)一語(yǔ)言
  • 限界上下文
  • 領(lǐng)域啦逆、子域伞矩、支撐域
  • 聚合、實(shí)體蹦浦、值對(duì)象
  • 分層:用戶接口層扭吁、應(yīng)用層、領(lǐng)域?qū)用は狻⒒A(chǔ)層

于是把MVC架構(gòu)進(jìn)行了改造,演進(jìn)成DDD的分層架構(gòu)蝌诡。

DDD分層架構(gòu):

MVC架構(gòu)到DDD分層架構(gòu)的映射:

至此溉贿,算了基本入門了DDD架構(gòu),擴(kuò)展性也得到了一定的提升浦旱。不過(guò)隨著業(yè)務(wù)的發(fā)展宇色,不斷冒出新的問題:

  • 一段業(yè)務(wù)邏輯代碼,到底應(yīng)該放到應(yīng)用層還是領(lǐng)域?qū)樱?/li>
  • 領(lǐng)域服務(wù)當(dāng)成原來(lái)的MVC中的service層颁湖,隨著業(yè)務(wù)不斷發(fā)展宣蠕,類也在不斷膨脹,好像還是老樣子吧唷抢蚀?
  • 聚合包含多個(gè)實(shí)體類,這個(gè)接口用不到這么多實(shí)體镰禾,為了性能還是直接寫個(gè)SQL返回必要的操作吧皿曲,不過(guò)這樣貌似又回到了MVC模式
  • 既然實(shí)體類可以包含業(yè)務(wù)邏輯唱逢、領(lǐng)域服務(wù)也可以放業(yè)務(wù)邏輯,那到底放哪里屋休?
  • 資料上說(shuō)領(lǐng)域?qū)硬荒苡型獠恳蕾囄牍牛龅?00%單測(cè)覆蓋,可是我的領(lǐng)域服務(wù)中需要用到外部接口劫樟、中央緩存等等痪枫,那這不就有了外部依賴了嗎?

第二層:草船借箭(戰(zhàn)術(shù)設(shè)計(jì))

帶著問題不斷學(xué)習(xí)他人經(jīng)驗(yàn)叠艳,并不斷的嘗試听怕,逐漸get到以下技能:

1、領(lǐng)域?qū)?/h4>

領(lǐng)域(domain)是個(gè)模塊虑绵,包含以下組成部分尿瞭,傳統(tǒng)的service按功能可能拆分到任何一個(gè)地方,各司其職翅睛。

  • 1個(gè)聚合
  • 1到多個(gè)實(shí)體
  • 若干值對(duì)象
  • 多個(gè)DomainService
  • 1個(gè)Factory:新建聚合
  • 1個(gè)Repository:聚合倉(cāng)儲(chǔ)服務(wù)

聚合根(AggregateRoot)

聚合本身也是一個(gè)實(shí)體声搁,聚合可以包含其他實(shí)體,其他實(shí)體不能脫離聚合而單獨(dú)提供服務(wù)捕发,比如一篇文章下的評(píng)論疏旨,評(píng)論必須從屬于文章,沒有文章也就沒有評(píng)論扎酷。倉(cāng)庫(kù)層(repository)也必須是以聚合為核心提供服務(wù)的檐涝;

實(shí)體:可以理解為一張數(shù)據(jù)庫(kù)表,必須有主鍵法挨;

值對(duì)象:沒有主鍵谁榜,依附于實(shí)體而存在,比如用戶實(shí)體下住址對(duì)象凡纳,一般在數(shù)據(jù)庫(kù)中已json字符串的形式存在窃植;最常見的值對(duì)象是枚舉;

倉(cāng)庫(kù)服務(wù)(repository)

資源庫(kù)是聚合的倉(cāng)儲(chǔ)機(jī)制荐糜,外部世界通過(guò)資源庫(kù)巷怜,而且只能通過(guò)資源庫(kù)來(lái)完成對(duì)聚合的訪問。資源庫(kù)以聚合的整體管理對(duì)象暴氏。因此延塑,一個(gè)聚合只能有一個(gè)資源庫(kù)對(duì)象,那就是以聚合根命名的資源庫(kù)答渔。除此之外的其他對(duì)象关带,都不應(yīng)該提供資源庫(kù)對(duì)象。倉(cāng)儲(chǔ)服務(wù)的實(shí)現(xiàn)一般有Spring Data JPA研儒、Mybatis兩種方式豫缨。

如果是用Spring Data JPA實(shí)現(xiàn)独令,直接使用JPA注解@OneToOne、@OneToMany好芭,配合fetch配置燃箭,即可一個(gè)方法查詢出所有的關(guān)聯(lián)實(shí)體。

如果是用Mybatis實(shí)現(xiàn)舍败,那么repository需要加入多個(gè)mapper的引用招狸,再手動(dòng)做拼裝。

這里有一個(gè)經(jīng)典的Hibernate笛卡爾積問題邻薯,答案是在聚合根中裙戏,一般不會(huì)加在大量的關(guān)聯(lián)實(shí)體對(duì)象。如果確實(shí)需要查詢關(guān)聯(lián)對(duì)象而關(guān)聯(lián)對(duì)象又比較多怎么辦呢厕诡?在DDD中有一個(gè)CQRS(Command-Query Responsibility Segregation)模式累榜,是一種讀寫分離模式,在此場(chǎng)景中需要將查詢操作放到查詢命令中分頁(yè)查詢灵嫌。

當(dāng)然CQRS也是一個(gè)很復(fù)雜模式壹罚,不應(yīng)照搬他人方案,而是根據(jù)自己的業(yè)務(wù)場(chǎng)景選擇適合自己的方案寿羞,以下列舉了CQRS的幾種應(yīng)用模式:

工廠服務(wù)(factory)

作用是創(chuàng)建聚合猖凛,只傳入必要的參數(shù),工廠服務(wù)內(nèi)部隱藏復(fù)雜的創(chuàng)建邏輯绪穆。簡(jiǎn)單的聚合可以直接通過(guò)new辨泳、靜態(tài)方法等創(chuàng)建,不是必須由factory創(chuàng)建玖院。

領(lǐng)域服務(wù)

單個(gè)實(shí)體對(duì)象能處理的邏輯放到實(shí)體里菠红,多個(gè)實(shí)體或有交互的場(chǎng)景放到領(lǐng)域服務(wù)里。

領(lǐng)域服務(wù)可不可以調(diào)用倉(cāng)儲(chǔ)層或外部接口司恳? 可以途乃,但不能直接和領(lǐng)域服務(wù)代碼放一起,領(lǐng)域服務(wù)模塊存放API扔傅,實(shí)現(xiàn)放基礎(chǔ)層(infrastructure)。

領(lǐng)域服務(wù)對(duì)象不建議直接以聚合名+DomainService命名烫饼,而要以操作命令關(guān)聯(lián)猎塞,比如用戶保存服務(wù)命名為:UserSaveService, 審核服務(wù):UserAuditSerivce。

2杠纵、應(yīng)用層

應(yīng)用層通過(guò)應(yīng)用服務(wù)接口來(lái)暴露系統(tǒng)的全部功能荠耽。在應(yīng)用服務(wù)的實(shí)現(xiàn)中,它負(fù)責(zé)編排和轉(zhuǎn)發(fā)比藻,它將要實(shí)現(xiàn)的功能委托給一個(gè)或多個(gè)領(lǐng)域?qū)ο髞?lái)實(shí)現(xiàn)铝量,它本身只負(fù)責(zé)處理業(yè)務(wù)用例的執(zhí)行順序以及結(jié)果的拼裝倘屹。通過(guò)這樣一種方式,它隱藏了領(lǐng)域?qū)拥膹?fù)雜性及其內(nèi)部實(shí)現(xiàn)機(jī)制慢叨。

比如下訂單服務(wù)的方法:

public void submitOrder(Long orderId) {    Order order = OrderFetchService.fetchById(orderId);   //獲取訂單對(duì)象    OrderCheckSerivce.check(order);    //驗(yàn)證訂單是否有效    OrderSubmitSerivce.submit(order);  //提交訂單    ShoppingCartClearService.clear(order);  //移除購(gòu)物車中已購(gòu)商品    NotifySerivce.emailNotify(order.getUser());  //發(fā)送郵件通知買家}

對(duì)于復(fù)雜的業(yè)務(wù)來(lái)說(shuō)纽匙,應(yīng)用層也有幾種模式:

  • 編排服務(wù):最典型比如Drools;
  • Command拍谐、Query命令模式烛缔;
  • 業(yè)務(wù)按Rhase、Step逐層拆分模式轩拨;

3践瓷、Maven模塊劃分

基礎(chǔ)層是比較簡(jiǎn)單一層,不過(guò)這里還有個(gè)比較疑惑的問題:按照DDD的四層架構(gòu)圖去劃分Maven模塊亡蓉,基礎(chǔ)層是最上的一層晕翠,但是基礎(chǔ)層也要包含基礎(chǔ)組件供其他層使用,這時(shí)基礎(chǔ)層應(yīng)該是放到最下層砍濒,直接按照這樣構(gòu)建Maven模塊會(huì)造成循環(huán)依賴淋肾。

image

相比來(lái)說(shuō),另一個(gè)架構(gòu)圖更準(zhǔn)確一些梯影,不過(guò)依然沒有直觀體現(xiàn)Maven模塊如何劃分巫员。

我的最佳實(shí)踐是將基礎(chǔ)層拆分兩部分,一部分是基礎(chǔ)的組件+倉(cāng)儲(chǔ)API甲棍,一部分是實(shí)現(xiàn)简识,maven模塊劃分圖如下所示:

第三層:運(yùn)籌帷幄(戰(zhàn)略設(shè)計(jì))

經(jīng)過(guò)以上的兩層的磨煉,恭喜你把DDD戰(zhàn)術(shù)都學(xué)習(xí)完了感猛,應(yīng)付日常的代碼開發(fā)也夠了七扰,不過(guò)作為架構(gòu)師來(lái)說(shuō),探索的道路還不能止步于此陪白,接下來(lái)會(huì)DDD戰(zhàn)略部分颈走。戰(zhàn)略部分關(guān)注點(diǎn)有3個(gè):

  • 統(tǒng)一語(yǔ)言
  • 領(lǐng)域
  • 限界上下文
1、統(tǒng)一語(yǔ)言

統(tǒng)一語(yǔ)言的重要性可以根據(jù)Jeff Patton 在《用戶故事地圖》中給出的一副漫畫來(lái)直觀的描述:

統(tǒng)一語(yǔ)言是提煉領(lǐng)域知識(shí)的輸出結(jié)果咱士,也是進(jìn)行后續(xù)需求迭代及重構(gòu)的基礎(chǔ)立由,統(tǒng)一語(yǔ)言的建立有以下幾個(gè)要點(diǎn):

  • 統(tǒng)一語(yǔ)言必須以文檔的形式提供出來(lái),并且在整個(gè)項(xiàng)目組的各團(tuán)隊(duì)達(dá)成共識(shí)序厉;
  • 統(tǒng)一語(yǔ)言必須每個(gè)中文名有對(duì)應(yīng)的英文名锐膜,并且在整個(gè)技術(shù)棧保持一致;
  • 統(tǒng)一語(yǔ)言必須是完整的弛房,包含以下要素:
    1. 領(lǐng)域模型的概念與邏輯道盏;
    2. 界限上下文(Bounded Context);
    3. 系統(tǒng)隱喻;
    4. 職責(zé)的分層荷逞;
    5. 模式(patterns)與慣用法媒咳。
2、領(lǐng)域劃分

以事件風(fēng)暴的形式(Event Storming)种远,列出所有的用戶故事(Use Story)涩澡,用戶故事可通過(guò)6W模型來(lái)構(gòu)建,即描寫場(chǎng)景的 Who院促、What筏养、Why、Where常拓、When 與 hoW 六個(gè)要素渐溶。然后圈選功能相近的部分,就形成了領(lǐng)域弄抬,領(lǐng)域又根據(jù)職能不同劃分為:核心域茎辐、支撐域、通用域掂恕,

具體的過(guò)程有很多參考資料拖陆,這里不再細(xì)講,最終的輸出是領(lǐng)域劃分圖懊亡,以下是一個(gè)保險(xiǎn)業(yè)務(wù)示例:

3依啰、限界上下文

限界上下文包含兩部分:上下文(Context)是業(yè)務(wù)目標(biāo),限界(Bounded)則是保護(hù)和隔離上下文的邊界店枣。

比如上圖中的實(shí)現(xiàn)部分即是限界上下文的邊界速警,虛線部分代表了領(lǐng)域的邊界。限界上下文沒有統(tǒng)一的劃分標(biāo)準(zhǔn)鸯两,需要的讀者根據(jù)自己的業(yè)務(wù)場(chǎng)景來(lái)甄別如何劃分闷旧。

一個(gè)上下文中包含了相同的領(lǐng)域知識(shí),角色在上下文中完成動(dòng)作目標(biāo)钧唐;

邊界體現(xiàn)在以下幾方面:

  • 領(lǐng)域邏輯層:確定了領(lǐng)域模型的業(yè)務(wù)邊界忙灼,維護(hù)了模型的完整性與一致性,從而降低系統(tǒng)的業(yè)務(wù)復(fù)雜度钝侠;
  • 團(tuán)隊(duì)合作層:限界上下文一般也是用戶換分團(tuán)隊(duì)的依據(jù)该园;
  • 技術(shù)實(shí)現(xiàn)層:限界上下文可當(dāng)成是微服務(wù)的劃分邊界;

DDD的不足

DDD架構(gòu)作為一套先進(jìn)的方法論帅韧,在很多場(chǎng)景能發(fā)揮很大價(jià)值爬范,但是DDD也不是銀彈。高級(jí)的架構(gòu)師把DDD架構(gòu)當(dāng)成一種工具弱匪,結(jié)合其他架構(gòu)經(jīng)驗(yàn)一起為業(yè)務(wù)服務(wù)。

DDD的不足有幾個(gè)方面:

  1. 性能:DDD是基于聚合來(lái)組織代碼,對(duì)于高性能場(chǎng)景下萧诫,加載聚合中大量的無(wú)用字段會(huì)嚴(yán)重影響性能斥难,比如報(bào)表場(chǎng)景中,直接寫SQL會(huì)更簡(jiǎn)單直接帘饶;
  2. 事務(wù):DDD中的事務(wù)被限定在限界上下文中哑诊,跨多個(gè)限界上下文的場(chǎng)景需要開發(fā)者額外考慮分布式事務(wù)問題;
  3. 難度系數(shù)高及刻,推廣成本大:DDD項(xiàng)目需要領(lǐng)域?qū)<覍<叶瓶悖倚枰貏e熟悉業(yè)務(wù)、建模缴饭、OOP暑劝,對(duì)于管理者來(lái)說(shuō)評(píng)估一個(gè)人是否真的能勝任也是一件困難的事情;

本文從MVC架構(gòu)開始講述了如何從演進(jìn)到DDD架構(gòu)颗搂,限于篇幅很多DDD的知識(shí)點(diǎn)沒有講到担猛,希望大家在實(shí)踐過(guò)程中能靈活運(yùn)用,盡享DDD給業(yè)務(wù)帶來(lái)的價(jià)值丢氢。本文如有不足之處敬請(qǐng)反饋傅联。

作者簡(jiǎn)介:木小豐,快手架構(gòu)師疚察,專注分享軟件研發(fā)實(shí)踐蒸走、架構(gòu)思考。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末貌嫡,一起剝皮案震驚了整個(gè)濱河市比驻,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌衅枫,老刑警劉巖嫁艇,帶你破解...
    沈念sama閱讀 221,406評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異弦撩,居然都是意外死亡步咪,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,395評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門益楼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)猾漫,“玉大人,你說(shuō)我怎么就攤上這事感凤∶踔埽” “怎么了?”我有些...
    開封第一講書人閱讀 167,815評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵陪竿,是天一觀的道長(zhǎng)禽翼。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么闰挡? 我笑而不...
    開封第一講書人閱讀 59,537評(píng)論 1 296
  • 正文 為了忘掉前任锐墙,我火速辦了婚禮,結(jié)果婚禮上长酗,老公的妹妹穿的比我還像新娘溪北。我一直安慰自己,他們只是感情好夺脾,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,536評(píng)論 6 397
  • 文/花漫 我一把揭開白布之拨。 她就那樣靜靜地躺著,像睡著了一般咧叭。 火紅的嫁衣襯著肌膚如雪蚀乔。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,184評(píng)論 1 308
  • 那天佳簸,我揣著相機(jī)與錄音乙墙,去河邊找鬼。 笑死生均,一個(gè)胖子當(dāng)著我的面吹牛听想,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播马胧,決...
    沈念sama閱讀 40,776評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼汉买,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了佩脊?” 一聲冷哼從身側(cè)響起蛙粘,我...
    開封第一講書人閱讀 39,668評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎威彰,沒想到半個(gè)月后出牧,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,212評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡歇盼,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,299評(píng)論 3 340
  • 正文 我和宋清朗相戀三年舔痕,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片豹缀。...
    茶點(diǎn)故事閱讀 40,438評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡伯复,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出邢笙,到底是詐尸還是另有隱情啸如,我是刑警寧澤,帶...
    沈念sama閱讀 36,128評(píng)論 5 349
  • 正文 年R本政府宣布氮惯,位于F島的核電站叮雳,受9級(jí)特大地震影響想暗,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜债鸡,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,807評(píng)論 3 333
  • 文/蒙蒙 一江滨、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧厌均,春花似錦、人聲如沸告唆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,279評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)擒悬。三九已至模她,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間懂牧,已是汗流浹背侈净。 一陣腳步聲響...
    開封第一講書人閱讀 33,395評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留僧凤,地道東北人畜侦。 一個(gè)月前我還...
    沈念sama閱讀 48,827評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像躯保,于是被迫代替她去往敵國(guó)和親旋膳。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,446評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容