DDD(領(lǐng)域驅(qū)動設(shè)計(jì))簡介:
? ? Eric Evans世界著名軟件建模專家,2010年在其所著的《領(lǐng)域驅(qū)動設(shè)計(jì)》進(jìn)行了詳細(xì)的解釋和介紹。參照1970年代的這一軟件設(shè)計(jì)思想椎咧,形成了符合當(dāng)前復(fù)雜業(yè)務(wù)應(yīng)用場景的新“領(lǐng)域驅(qū)動設(shè)計(jì)”凌彬。
? ? 關(guān)于DDD的設(shè)計(jì)思路,原理最楷,內(nèi)容基本上分析的文章已經(jīng)到處可見了整份,大家會談到領(lǐng)域設(shè)計(jì)的幾種模型,甚至?xí)蔀榧軜?gòu)師崗位面試中設(shè)計(jì)思路面試題的一部分籽孙。
? ? 但是烈评!如何落地,有人落地了嘛犯建?落地效果怎么樣讲冠?到底能否實(shí)現(xiàn)?面對這些質(zhì)疑适瓦,基本上可以認(rèn)為每個(gè)人都可以自說自話竿开。
? ?本文會介紹我們搜索團(tuán)隊(duì)內(nèi)部對于DDD的一次工程化實(shí)踐的過程,希望能夠給大家一些參考玻熙。
面向本質(zhì):
? ? ?1.為何需要這樣的模型否彩?我們團(tuán)隊(duì)負(fù)責(zé)公司的全部搜索和列表展示場景的服務(wù)維護(hù),日積月累揭芍,不同入口胳搞,場景,版本称杨,業(yè)務(wù)模型導(dǎo)致冗余了非常多的業(yè)務(wù)流程代碼肌毅。我們做了業(yè)務(wù)代碼層的匯總分析,平均每個(gè)項(xiàng)目方法20-30行姑原,每個(gè)方法中嵌套的if/else和get,set方法平均15行左右悬而。我們發(fā)現(xiàn)隨著業(yè)務(wù)的增加,代碼的嵌套和耦合也指數(shù)倍增長锭汛。因此我們需要一套能夠提高復(fù)用率的業(yè)務(wù)沉淀代碼或架構(gòu)笨奠。
? ? 2.徹底的使用DDD還是局部使用?這個(gè)問題完全是玄學(xué)唤殴,就像業(yè)務(wù)系統(tǒng)技術(shù)棧選型一樣般婆,沒有最適合,只有更適合朵逝。我們調(diào)研了美團(tuán)在2017年的DDD工程化實(shí)踐項(xiàng)目蔚袍,得到了一個(gè)深刻的結(jié)論:那就是對于簡單場景,正常spring基礎(chǔ)下的三層架構(gòu)已經(jīng)滿足就不需要使用額外的炫技策略,否則會導(dǎo)致代碼跳來跳去反而增加了額外的復(fù)雜度啤咽;對于復(fù)雜場景晋辆,做好抽象、分治宇整,選擇和場景強(qiáng)關(guān)聯(lián)的屬性做輕度的DDD實(shí)現(xiàn)瓶佳。
? ? 3.同樣是CRUD為什么他做的比我開心,比我好鳞青?我相信這個(gè)問題霸饲,正常的85%業(yè)務(wù)線上的開發(fā)同學(xué)都會遇到,也都會問自己盼玄,那么這個(gè)過程中會出現(xiàn)類似的兩類人贴彼。一類,抱怨型:“天天CRUD埃儿,復(fù)制粘貼器仗,我真的是醉了”;另一類童番,探索型:“他是怎么做的精钮,為什么我的不如他,找找書籍剃斧,刷刷論壇尋找亮點(diǎn)轨香,驗(yàn)證并實(shí)踐”。我們團(tuán)隊(duì)的一個(gè)氛圍是:除了少數(shù)人天賦異稟幼东,大部分的人智商沒有特別的區(qū)別臂容,做得好無非就是方法論和學(xué)習(xí)的方式有區(qū)別而已。
面向?qū)嵺`:
? ?1.老員工其實(shí)是財(cái)富根蟹,一條生產(chǎn)線脓杉,哪里掉過坑,哪里最復(fù)雜大部分都在老員工的經(jīng)驗(yàn)之中简逮。這個(gè)老不是工作年限的長短球散,而是浸泡在一條業(yè)務(wù)線中的跟迭代開發(fā)的時(shí)間長短。經(jīng)過長期和業(yè)務(wù)線中有豐富經(jīng)驗(yàn)的開發(fā)溝通后散庶,我們找到了搜索場景中最復(fù)雜蕉堰,最重復(fù),也是嵌套業(yè)務(wù)最深的幾個(gè)模塊悲龟。以我所在業(yè)務(wù)線場景我們匯總了:搜索的存儲屋讶,數(shù)據(jù)狀態(tài)的變更,多場景分頁须教,渲染和組裝層這四個(gè)核心突破點(diǎn)丑婿。
? ? 1.1.數(shù)據(jù)的存儲復(fù)雜點(diǎn)在于搜索引擎索引的構(gòu)建,存儲數(shù)據(jù)的結(jié)構(gòu)化抽象没卸,數(shù)據(jù)源頭的多種類存儲羹奉。
? ? 1.2.數(shù)據(jù)狀態(tài)的變更復(fù)雜點(diǎn)在于多場景對于數(shù)據(jù)修改頻次高,一致性高的數(shù)據(jù)會出現(xiàn)索引大量的重建约计,導(dǎo)致實(shí)時(shí)查詢效果差诀拭。
? ? 1.3.多場景分頁的復(fù)雜點(diǎn)在于推薦,主要展示煤蚌,補(bǔ)充列表展示的多路查詢數(shù)據(jù)重復(fù)展示耕挨,亂分頁,無法分頁尉桩。
? ? 1.4.組裝和渲染數(shù)據(jù)展示層的復(fù)雜點(diǎn)在于多個(gè)場景的適配關(guān)系管理較難筒占,層層挖掘之后依舊難以確定最終表現(xiàn)層的模樣。
? ? 2.如何避免這種場景蜘犁?這種場景會出現(xiàn)什么樣的問題翰苫?從開發(fā)方面來說,代碼可讀性變差这橙,可維護(hù)性變低奏窑,終于梳理清楚之后開發(fā)一般會采用加塞或者拼接if/else的方式處理業(yè)務(wù)邏輯。從測試方面來說屈扎,測試場景存在N個(gè)階段一場景埃唯,M個(gè)階段二場景,Z個(gè)階段三場景出現(xiàn)測試用例數(shù)量T = N * M * Z 的情況鹰晨,歷史代碼的回歸變成快速迭代和敏捷開發(fā)的最大耗時(shí)點(diǎn)墨叛。如果說徹底解決,任何團(tuán)隊(duì)基本都是無解的狀態(tài)模蜡,因此我們嘗試將這個(gè)模型變更為 T = N + M + Z的效果漠趁。
? ?3.業(yè)務(wù)分層方面,我們識別了1.遠(yuǎn)程調(diào)用場景RPC哩牍,HTTP棚潦,MQ等場景類的數(shù)據(jù)調(diào)用;2.代碼中內(nèi)存值交換的數(shù)據(jù)調(diào)用膝昆。從經(jīng)典的spring架構(gòu)來看丸边,遠(yuǎn)程或外部調(diào)用需要依賴于中間層架構(gòu)的IOC,AOP來實(shí)現(xiàn)荚孵;而內(nèi)存值交換則可以參考JDK中源碼的設(shè)計(jì)思想妹窖,暴露最簡單的使用接口,隱藏復(fù)雜的交換值和計(jì)算收叶、校驗(yàn)邏輯骄呼。
? 4.效果,代碼的可讀性變高,邏輯性增強(qiáng)蜓萄,變更點(diǎn)減少隅茎。我們粗略的統(tǒng)計(jì)了后續(xù)業(yè)務(wù)開發(fā)的代碼量基本上減少了30%左右。執(zhí)行效果和邏輯也更加清晰嫉沽。
場景介紹:
? ?目前我們公司app端有多種多樣的商品辟犀,商品本身具備很多特殊的屬性,比如上架,下架狀態(tài)。例如拼多多的模式中還有多人拼單嘁酿,搶單這些因產(chǎn)品屬性衍生的特性狡逢。
? ? 搜索過程中,商品或訂單數(shù)據(jù)的存儲,用戶app端的列表陳列樣式和展示則成為了主流互聯(lián)網(wǎng)運(yùn)營中的核心點(diǎn)。我們把這兩個(gè)點(diǎn)歸納為:1.海量數(shù)據(jù)的存儲;2.單一到多內(nèi)容的展示表現(xiàn)形式税稼。誠然,復(fù)雜的排序以及千人千面這種推薦算法模型并沒有納入到本次的實(shí)踐和討論中刁赦,我們也會在后續(xù)的業(yè)務(wù)實(shí)踐探索中摸索更貼合的方式來嘗試娶聘。
? ? 1.海量數(shù)據(jù)的存儲--本文探討的也僅限于數(shù)據(jù)的流轉(zhuǎn)過程中組裝符合數(shù)據(jù)這一個(gè)環(huán)節(jié),對于強(qiáng)一致性甚脉,多點(diǎn)存儲丸升,異常和事務(wù)的細(xì)節(jié)問題不進(jìn)行詳細(xì)探討。
參考上圖牺氨,目前這個(gè)系統(tǒng)已知的數(shù)據(jù)來源分為了三個(gè)主要的外部渠道狡耻,三個(gè)主要的根據(jù)業(yè)務(wù)模型適配的生成機(jī)制。
? ? a. 通過http調(diào)用獲得的數(shù)據(jù);?
? ? b.通過組織內(nèi)部tcp服務(wù)調(diào)用獲得的數(shù)據(jù)猴凹;
? ? c.通過組織內(nèi)部消息隊(duì)列獲得的數(shù)據(jù)夷狰;
? ? d.系統(tǒng)自派生數(shù)據(jù),例如創(chuàng)建時(shí)間郊霎,更新時(shí)間沼头,消費(fèi)時(shí)間等數(shù)據(jù)與系統(tǒng)產(chǎn)生運(yùn)轉(zhuǎn)關(guān)系過程中可以被存儲的數(shù)據(jù);
? ? e.系統(tǒng)計(jì)算數(shù)據(jù)书劝,例如資金類統(tǒng)一單位分进倍,統(tǒng)一數(shù)據(jù)類型,長度統(tǒng)一米單位等數(shù)據(jù)购对;
? ? f.系統(tǒng)由于統(tǒng)一數(shù)據(jù)流入的順序和條件產(chǎn)生的不同狀態(tài)參數(shù)猾昆,例如訂單分為了下單,支付骡苞,成單垂蜗,履約單楷扬,訂單完成和基于過程出現(xiàn)的正向、逆向流轉(zhuǎn)過程數(shù)據(jù)贴见。
? ? 那么烘苹,如果只針對一條數(shù)據(jù),一個(gè)單一業(yè)務(wù)線蝇刀,我們會發(fā)現(xiàn)從頭到尾實(shí)現(xiàn)是一種簡單而輕松的方式螟加。但是在配套了復(fù)雜的業(yè)務(wù)場景之后,數(shù)據(jù)的流轉(zhuǎn)變成了極其復(fù)雜和龐大的工程吞琐。但是存儲數(shù)據(jù)的關(guān)鍵制約因素依舊是如何查詢(OLTP)或分析(OLAP)。這里涉及到了深入業(yè)務(wù)場景的數(shù)據(jù)存儲方式選型等拓展類的知識然爆,我推薦讀者可以閱讀《數(shù)據(jù)密集型應(yīng)用系統(tǒng)設(shè)計(jì)》一書站粟。
? ? 回歸數(shù)據(jù)的存儲流轉(zhuǎn)過程,清晰的可以歸納為兩大特點(diǎn):內(nèi)部自生成曾雕,外部依賴奴烙。
? ? 內(nèi)部自生成特點(diǎn)與業(yè)務(wù)模型完全關(guān)聯(lián),但是完全可以近似看作是一種賦值和取值抑或改變值的操作剖张。因此可以稱為對象值交換切诀,下文簡稱值交換。
? ? 外部依賴特點(diǎn)需要預(yù)先定義常規(guī)的接口或者交互字段文檔以做統(tǒng)一的序列化和反序列化基礎(chǔ)搔弄,這個(gè)必不可少也是目前軟件開發(fā)階段重要的設(shè)計(jì)環(huán)節(jié)幅虑。但是在不同系統(tǒng)數(shù)據(jù)的流入流出環(huán)節(jié),依舊存在值交換的過程顾犹,這是一定的倒庵,與內(nèi)部自生成數(shù)據(jù)的區(qū)別在于約定和串聯(lián)的關(guān)系。
? ? 我們的常規(guī)做法:數(shù)據(jù)copy炫刷,對象的創(chuàng)建擎宝,賦值,序列化等操作浑玛,可以解釋為構(gòu)造方法創(chuàng)建對象或執(zhí)行對象的get/set方法绍申。往往真實(shí)的業(yè)務(wù)場景中,參數(shù)的合法性校驗(yàn)顾彰,有效性校驗(yàn)也捆綁其中极阅。我相信讀者如果浸淫crud多年,對于參數(shù)校驗(yàn)的流程和環(huán)節(jié)的痛苦感慨頗深拘央。
拆解與抽象
? ? 流程化的數(shù)據(jù)流涂屁,一定是可以抽象的,一定是可以拆解的灰伟。這是閱讀大量JDK源碼之后得到的實(shí)踐經(jīng)驗(yàn)拆又。但往往過度的依賴spring全家桶的IOC思想儒旬,AOP思想,在實(shí)際操作過程中開發(fā)依舊喜歡大量的依賴注入+if/else+get/set方法使用帖族。本文并不是說此類方法或方式有誤栈源,只是針對極其復(fù)雜的場景,過多的上述方式造成了代碼的可維護(hù)性差竖般,可拓展性降低甚垦,甚至基本沒有可讀性。
? ? JDK是怎么做的呢涣雕?我認(rèn)為這就是領(lǐng)域設(shè)計(jì)的精髓艰亮。參考HashMap的源碼:
? ? 常規(guī)通用HashMap,我們高頻使用的如構(gòu)造方法挣郭,put迄埃,get,size等方法兑障,那么他的參數(shù)合法性侄非,存值,計(jì)算流译,取值逞怨,擴(kuò)容等等其他方法是否需要額外的多次使用和實(shí)現(xiàn)呢?答案當(dāng)然是否定的福澡!我想說這個(gè)其實(shí)就是領(lǐng)域的抽象叠赦。隱藏掉復(fù)雜的處理流程邏輯,只提供簡單可用的api竞漾,接口抽象如此眯搭,抽象類如此,輕度的DDD領(lǐng)域模型也應(yīng)當(dāng)如此业岁。
? ? 改變個(gè)思路:Java官方提供了JDK(java標(biāo)準(zhǔn)版開發(fā)包)鳞仙,上層 應(yīng)用語言使用的各家公司是否也可以培育和產(chǎn)出符合自身業(yè)務(wù)特點(diǎn)的CDK(自己公司標(biāo)準(zhǔn)開發(fā)包)呢?
? ? 按照這個(gè)思路笔时,我們嘗試性的抽象了小原子粒度的特征模塊棍好。這一層不依賴于任何業(yè)務(wù)層的服務(wù),大量的純對象類和對應(yīng)對象的BO業(yè)務(wù)邏輯封裝:
業(yè)務(wù)執(zhí)行步驟舉例:
? ? 1.商品新增允耿;? ? ? ? ? ? ? ? ? ? ? ? ? ?------RPC/MQ消費(fèi)對象BO執(zhí)行
? ? 2.商品自身屬性值對象賦值借笙;? ------BaseItemBO對象執(zhí)行
? ? 3.商品計(jì)算屬性值對象賦值;? ------CulculateBO對象執(zhí)行
? ? 4.商品狀態(tài)屬性值對象賦值较锡;? ------StateItemBO對象執(zhí)行
? ? 5.商品存儲到數(shù)據(jù)庫中业稼;? ? ? ? ?------StoreItemBO對象執(zhí)行
? ? 對于任意BO對象,它完成了對應(yīng)值的校驗(yàn)蚂蕴,處理低散,賦值俯邓,取值等操作,我們會發(fā)現(xiàn)這些通用的處理單元是每一套業(yè)務(wù)都需要使用的熔号,大部分可以通用稽鞭,少部分獨(dú)立于自身的業(yè)務(wù)。
? ? 使用了這樣的模式:業(yè)務(wù)邏輯關(guān)系引镊,整潔而清晰朦蕴,映射了真實(shí)業(yè)務(wù)場景的核心步驟,把具體的執(zhí)行環(huán)節(jié)交給面向?qū)ο笾械膶ο髞硗瓿傻芡罚ㄓ玫淖龀橄蠛统恋矸宰ィ煌ㄓ玫倪\(yùn)用封裝,繼承亮瓷,多態(tài)獨(dú)立實(shí)現(xiàn)琴拧。
2.單一到多內(nèi)容的展示表現(xiàn)形式
? ? 這個(gè)業(yè)務(wù)的場景,我們依舊沿著DDD領(lǐng)域設(shè)計(jì)模式的思想嘱支,做了最小化單元抽象。
? ? 基礎(chǔ)數(shù)據(jù)挣饥,既能使用到詳情也可以少量陳列在列表展示中除师;比如商品圖片,大小扔枫,尺寸等汛聚;
? ? 特征數(shù)據(jù),商品的屬類短荐,商品的相似品類等倚舀。
? ? 渲染數(shù)據(jù),展示文案字體大小忍宋,顏色痕貌,布局方式等。
? ? 多個(gè)單商品匯總成為了一個(gè)列表集合糠排,注意列表集合的排序結(jié)果不在本文進(jìn)行討論舵稠,理論上是可以依托于算法推薦,自然排名等屬性自由組合入宦。
關(guān)鍵抽象點(diǎn)舉例:
? ? ?介紹一下列表特征中哺徊,分頁BO的具體實(shí)現(xiàn)。分頁是一個(gè)偽命題乾闰,也基于空間換時(shí)間的概念落追。多次少量的操作模式減少底層存儲引擎和網(wǎng)絡(luò)交互的資源消耗。
? ? 我們提供了兩個(gè)核心的方法涯肩,generateResponsePageBO生成響應(yīng)體分頁內(nèi)容方法轿钠,resoluteRequestPageBO解析請求體分頁內(nèi)容方法巢钓。其余的方法和屬性,例如參數(shù)校驗(yàn)谣膳,是否可以查詢下一頁竿报,分頁大小的適配等,都依據(jù)公司的業(yè)務(wù)特性做了具體的隱方式包裝继谚×揖可以想像這個(gè)分頁領(lǐng)域的模型,針對于自己公司的業(yè)務(wù)場景花履,持續(xù)不斷的迭代是能夠滿足絕大多數(shù)的使用和適配的芽世。
關(guān)于DDD領(lǐng)域模式設(shè)計(jì)的一些探討
? ? 我們拋開程序本身的特殊屬性不談,僅僅針對于這一次的實(shí)現(xiàn)過程诡壁,我們深刻的發(fā)現(xiàn)济瓢,最小化單元是最難的一步。經(jīng)驗(yàn)在這里起到了絕對的作用妹卿,所謂技術(shù)沉淀應(yīng)當(dāng)也是如此旺矾。大家都在探討重復(fù)踩坑的場景,但是依然絕大多數(shù)公司的開發(fā)方式和架構(gòu)導(dǎo)致夺克,新人難以避免的再次走已經(jīng)有人走過的彎路箕宙。我相信軟件開發(fā)沒有捷徑,只有沉淀能夠幫助后續(xù)繼承者少走彎路铺纽,該走的路柬帕,請放心,還點(diǎn)走狡门。
? ? 當(dāng)這個(gè)想法已經(jīng)形成的時(shí)候陷寝,我聽到過反對和質(zhì)疑的聲音,誠然部分開發(fā)依然喜歡一條道走到黑其馏,踩到坑里再爬出來繼續(xù)踩新坑凤跑。理論的模型落地必然是無限的爭吵中,一步一步的緩慢發(fā)展尝偎。缺點(diǎn)也是有的饶火,無論市面上對于DDD領(lǐng)域模型探討的如何,它僅僅是一種設(shè)計(jì)的思路致扯,而非解決問題的手段肤寝。
? ? 不成熟的建議:
? ? 非復(fù)雜場景,對于Java語言的開發(fā)抖僵,我依然支持spring模式的三層架構(gòu)理論鲤看。簡單清晰解決就好,這一思想也符合快速迭代耍群。
? ? 復(fù)雜場景义桂,可以嘗試性的結(jié)合DDD領(lǐng)域設(shè)計(jì)模式進(jìn)行抽象找筝,最終的目標(biāo)是徹底的實(shí)現(xiàn)分治。抽象和分治其實(shí)才是我們本次實(shí)踐中最深刻的體會慷吊。
? ? 鑒于嘗試的效果還不錯將這段歷程記錄下來袖裕,更多的同路人一起合作商討,伴隨進(jìn)步溉瓶。