前言
領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)是一項(xiàng)艱巨的技術(shù)挑戰(zhàn)维蒙,但它也會(huì)帶來豐厚的回報(bào)掰吕,當(dāng)大多數(shù)軟件項(xiàng)目開始僵化而成為遺留系統(tǒng)時(shí),它卻為你敞開了機(jī)會(huì)的大門颅痊。
現(xiàn)在面臨的問題
過度耦合
過度耦合有兩方面殖熟,一方面是領(lǐng)域之間沒有拆分,由于業(yè)務(wù)初期斑响,我們的功能大都非常簡(jiǎn)單菱属,普通的CRUD就能滿足,此時(shí)系統(tǒng)是清晰的舰罚。隨著迭代的不斷演化纽门,業(yè)務(wù)邏輯變得越來越復(fù)雜,我們的系統(tǒng)也越來越冗雜营罢。模塊彼此關(guān)聯(lián)赏陵,誰(shuí)都很難說清模塊的具體功能意圖是啥。修改一個(gè)功能時(shí)饲漾,往往光回溯該功能需要的修改點(diǎn)就需要很長(zhǎng)時(shí)間蝙搔,更別提修改帶來的不可預(yù)知的影響面。
另一方面是業(yè)務(wù)邏輯和一些膠水適配的邏輯耦合考传。有時(shí)候我們的代碼寫得不好吃型,往往是在我們代碼中依賴了大量的外部服務(wù),而這些服務(wù)又往往不是為我們定制的伙菊,我們需要寫大量的適配败玉。結(jié)果就是我們的核心業(yè)務(wù)邏輯被那些非核心業(yè)務(wù)邏輯所淹沒。比如我們有一個(gè)給用戶發(fā)逾期短信的用例镜硕,本來是很簡(jiǎn)單的一個(gè)業(yè)務(wù)邏輯运翼,跑出所有的逾期分期單并發(fā)出逾期事件,監(jiān)聽這些逾期事件去取手機(jī)手機(jī)兴枯,然后調(diào)用短信平臺(tái)的接口發(fā)送短信血淌。但是歷史原因我們的手機(jī)號(hào)碼保存在不同的地方,我們先取金融手機(jī)號(hào)碼,如果不存在金融手機(jī)號(hào)碼則查詢用戶開白條時(shí)實(shí)名手機(jī)號(hào)碼悠夯,如果不存在實(shí)名手機(jī)號(hào)碼則獲取申請(qǐng)分期貸款時(shí)填寫的手機(jī)號(hào)碼癌淮。結(jié)果就是我們的發(fā)逾期短信的業(yè)務(wù)邏輯中大部分都是取獲取手機(jī)號(hào)碼的邏輯。
貧血癥和失憶癥
在我們習(xí)慣了J2EE的開發(fā)模式后沦补,Action/Service/DAO這種分層模式乳蓄,會(huì)很自然地寫出過程式代碼,而學(xué)到的很多關(guān)于OO理論的也毫無用武之地夕膀。使用這種開發(fā)方式虚倒,對(duì)象只是數(shù)據(jù)的載體,沒有行為产舞。以數(shù)據(jù)為中心魂奥,以數(shù)據(jù)庫(kù)ER設(shè)計(jì)作驅(qū)動(dòng)。分層架構(gòu)在這種開發(fā)模式下易猫,可以理解為是對(duì)數(shù)據(jù)移動(dòng)耻煤、處理和實(shí)現(xiàn)的過程。
當(dāng)我們接到一個(gè)項(xiàng)目后准颓,我們很容易就會(huì)想到這個(gè)項(xiàng)目中涉及的數(shù)據(jù)的載體哈蝇,這個(gè)載體上應(yīng)該有什么數(shù)據(jù)。然后就基于這個(gè)數(shù)據(jù)進(jìn)行串連流程瞬场。
簡(jiǎn)單的業(yè)務(wù)系統(tǒng)采用這種貧血模型和過程化設(shè)計(jì)是沒有問題的买鸽,但在業(yè)務(wù)邏輯復(fù)雜了涧郊,業(yè)務(wù)邏輯贯被、狀態(tài)會(huì)散落到在大量方法中,原本的代碼意圖會(huì)漸漸不明確妆艘,我們將這種情況稱為由貧血癥引起的失憶癥彤灶。
業(yè)務(wù)規(guī)則泄露
在我們開始接觸軟件開發(fā)時(shí)就被告知,代碼復(fù)用可以極大的提高我們的工作效率批旺。所以大部分后端研發(fā)提供的API接口或者方法都是極其開放的幌陕,而且我們是直接使用數(shù)據(jù)庫(kù)模型,往往一個(gè)更新接口可以更新這個(gè)模型的所有字段汽煮,這樣幾乎可以適用于所有的更新場(chǎng)景搏熄。結(jié)果就是所有的調(diào)用方都可以修改這個(gè)模型的幾乎所有屬性。我甚至看到過可以修改數(shù)據(jù)庫(kù)自增ID的接口暇赤。這個(gè)是一個(gè)極端的例子心例。
回到業(yè)務(wù)上來。我們有一個(gè)訂單表鞋囊,里面有一個(gè)訂單的狀態(tài)止后。我們提供了一個(gè)更新訂單的接口,入?yún)⒕褪荗rder對(duì)象。
@Getter
@Setter
class?Order {
????private?int?status;
}
interface?OrderMapper {
????int?update(Order);
}
結(jié)果任何調(diào)用方都可以更新這個(gè)訂單的狀態(tài)译株。作為訂單的維護(hù)人員根本不知道這個(gè)訂單表的狀態(tài)是誰(shuí)修改了瓜喇,修改成了什么樣。這個(gè)就相當(dāng)于業(yè)務(wù)邏輯泄露了歉糜。作為這個(gè)訂單領(lǐng)域的負(fù)責(zé)人都已經(jīng)失去對(duì)這個(gè)訂單的把控了乘寒。失控是非常危險(xiǎn)的,這種不確定性增加了出問題的機(jī)率匪补。
軟件核心復(fù)雜性應(yīng)對(duì)之道
統(tǒng)一語(yǔ)言
同一對(duì)象在不同上下文中的概念可能是不同的肃续。
領(lǐng)域太復(fù)雜,只有在分割的上下文內(nèi)才可能形成統(tǒng)一語(yǔ)言叉袍。
比如同一件物品始锚,iPhone12ProMax512G。在購(gòu)買的上下文中是商品喳逛,但是在配送上下文中是貨物瞧捌。在不同的上下文中,關(guān)注的屬性也是不一樣的润文。
戰(zhàn)略設(shè)計(jì)
戰(zhàn)略設(shè)計(jì)側(cè)重于高層次姐呐、宏觀上去劃分和集成限界上下文。消費(fèi)金融目前主要支持了白條和金條業(yè)務(wù)線典蝌。在我們進(jìn)行業(yè)務(wù)體系化的建設(shè)中曙砂,把消金的信貸領(lǐng)域劃分成了授信域、用戶域骏掀、用戶域鸠澈、賬戶域、交易域截驮、營(yíng)銷域笑陈、觸達(dá)域等一級(jí)業(yè)務(wù)域,每個(gè)一級(jí)業(yè)務(wù)域下再根據(jù)具體分析拆分二級(jí)業(yè)務(wù)域葵袭。
領(lǐng)域劃分
限界上下文劃分
上下文映射
如何識(shí)別限界上下文
可以從兩個(gè)方向識(shí)別限界上下文:
縱向:識(shí)別用例或者事件涵妥,倘若相鄰兩個(gè)事件之間的關(guān)系較弱,或者體現(xiàn)了兩個(gè)非常明顯的階段坡锡,就可以對(duì)其進(jìn)行分割蓬网。
橫向:梳理所有的用例,根據(jù)組成用例的名詞和動(dòng)詞去發(fā)現(xiàn)用例之間的相關(guān)性(相同鹉勒、相似的名稱)帆锋,然后去提煉一個(gè)整體的概念。
識(shí)別限界上下文遵循的原則
單一抽象層次原則:每個(gè)限界上下文從概念上應(yīng)盡量處于同一個(gè)抽象的層次贸弥,不能嵌套窟坐。
正交原則:限界上下文之間不能互相影響,互相包含。
戰(zhàn)術(shù)設(shè)計(jì)
戰(zhàn)術(shù)設(shè)計(jì)主要是圍繞著領(lǐng)域模型為主哲鸳。通用業(yè)務(wù)用例或者故事點(diǎn)分析梳理總結(jié)出實(shí)體和值對(duì)象臣疑,然后通過分析它他們之間相關(guān)梳理出聚合來。
領(lǐng)域?qū)ο髣澐?/p>
無狀態(tài)和有狀態(tài)
對(duì)象是有狀態(tài)的徙菠,服務(wù)是無狀態(tài)的讯沈。由于Spring以及失血模型的流行,我們大量的業(yè)務(wù)邏輯都是在無狀態(tài)服務(wù)中的婿奔。
落地實(shí)踐
事件風(fēng)暴
事件風(fēng)暴是一種快速探索復(fù)雜業(yè)務(wù)領(lǐng)域和對(duì)領(lǐng)域建模的實(shí)踐缺狠。事件風(fēng)暴從領(lǐng)域中關(guān)注的業(yè)務(wù)事件出發(fā),在此過程中團(tuán)隊(duì)經(jīng)過充分討論萍摊,統(tǒng)一語(yǔ)言挤茄,最后找到領(lǐng)域模型。
事件風(fēng)暴是一項(xiàng)團(tuán)隊(duì)活動(dòng)冰木,領(lǐng)域?qū)<遗c項(xiàng)目團(tuán)隊(duì)通過頭腦風(fēng)暴的形式穷劈,羅列出領(lǐng)域中所有的領(lǐng)域事件,整合之后形成最終的領(lǐng)域事件集合踊沸,然后對(duì)每一個(gè)事件歇终,標(biāo)注出導(dǎo)致該事件的命令,再為每一個(gè)事件標(biāo)注出命令發(fā)起方的角色逼龟。
命令可以是用戶發(fā)起评凝,也可以是第三方系統(tǒng)調(diào)用或者定時(shí)器觸發(fā)等,最后對(duì)事件進(jìn)行分類腺律,整理出實(shí)體奕短、聚合、聚合根以及限界上下文疾渣。
核心概念
事件(Event):事件風(fēng)暴的核心概念篡诽,事件是過去發(fā)生的與業(yè)務(wù)有關(guān)的事實(shí)崖飘。一般使用賓語(yǔ)+動(dòng)詞的過去式榴捡,例如:申請(qǐng)單被風(fēng)控審批通過,訂單被支付成功朱浴。
命令(Command):命令即動(dòng)作吊圾,命令會(huì)改變對(duì)象的狀態(tài),并產(chǎn)生相應(yīng)的事件翰蠢。比如:成功支付訂單项乒、取消訂單。
用戶(User或者Actor):命令是由對(duì)象執(zhí)行的梁沧,這稱之為用戶檀何。用戶可以是自然人,也可以是系統(tǒng),這里一般指自然人频鉴。
規(guī)則(Policy):當(dāng)產(chǎn)生事件或者執(zhí)行命令時(shí)栓辜,需要進(jìn)行某些業(yè)務(wù)相關(guān)的規(guī)則校驗(yàn)。比如用戶參加拼團(tuán)活動(dòng)垛孔,需要校驗(yàn)活動(dòng)是否有效等藕甩。
執(zhí)行模型
用戶執(zhí)行了命令,命令生成了事件周荐,事件觸發(fā)了規(guī)則校驗(yàn)狭莱。
如何利用事件風(fēng)暴構(gòu)建領(lǐng)域模型
事件風(fēng)暴的參與者
組織者:組織者應(yīng)當(dāng)熟悉事件風(fēng)暴的整個(gè)流程,能夠組織大家順利完成事件風(fēng)暴概作;
領(lǐng)域?qū)<遥侯I(lǐng)域?qū)<覒?yīng)該是精通業(yè)務(wù)的人腋妙,在事件風(fēng)暴過程中,要負(fù)責(zé)澄清一些業(yè)務(wù)上的概念讯榕,思考業(yè)務(wù)上有沒有遺漏的事件辉阶;
項(xiàng)目成員:負(fù)責(zé)開發(fā)這個(gè)項(xiàng)目的成員,所有角色都可參加瘩扼,比如BA谆甜、QA、UX集绰、DEV规辱。因?yàn)槭录L(fēng)暴可以快速讓整個(gè)團(tuán)隊(duì)了解整個(gè)項(xiàng)目的業(yè)務(wù)流程
尋找領(lǐng)域事件
由尋找領(lǐng)域事件開始。領(lǐng)域事件一般用橘色的便利貼表示栽燕,書寫領(lǐng)域?qū)嵺`的規(guī)則是使用被動(dòng)語(yǔ)態(tài)罕袋,并按照時(shí)間順序貼在白紙上。
最開始可能很多成員都不知道該怎么寫碍岔,或者不知道該怎么尋找領(lǐng)域事件浴讯。可以由組織者寫下領(lǐng)域中發(fā)生的第一個(gè)事件蔼啦。其它參與者會(huì)迅速的開始模仿榆纽,這時(shí)我們可以讓大家快速的進(jìn)入狀態(tài)。
在遇到有疑惑的事件時(shí)捏肢,不必長(zhǎng)時(shí)間阻塞在那里討論奈籽,把它作為標(biāo)記記下來即可,后續(xù)再進(jìn)行重點(diǎn)優(yōu)化鸵赫∫缕粒可以貼一個(gè)比較醒目的便簽紙(比如紫色)在事件旁邊。
隨著我們對(duì)業(yè)務(wù)認(rèn)識(shí)的不斷加深辩棒,可以隨時(shí)回顧和總結(jié)之前添加的內(nèi)容狼忱,對(duì)于有問題的描述進(jìn)行更正膨疏,對(duì)于表述不清楚的內(nèi)容可以進(jìn)行重寫。
事件是有相對(duì)順序的钻弄〕芍猓可以把一系列有相對(duì)順序關(guān)系的事件放在一行上,從左到右排好斧蜕。這樣有助于梳理領(lǐng)域事件双霍,查看是否有遺漏。
尋找命令和角色
在收集完領(lǐng)域事件后批销,我們可以在此基礎(chǔ)上進(jìn)一步探索系統(tǒng)核心事件的運(yùn)行機(jī)制洒闸。這里我們?cè)谥暗念I(lǐng)域事件的基礎(chǔ)上加入指令和角色的概念。
指令代表系統(tǒng)中用戶的意圖均芽、動(dòng)作和決定丘逸,一般用藍(lán)色的便利貼表示;角色表一類特定用戶掀宋,一般用黃色便利貼表示深纲。它們之間的關(guān)系是“角色”發(fā)送“指令”產(chǎn)生了“領(lǐng)域事件”(指令也可由外部系統(tǒng)觸發(fā),外部系統(tǒng)通常用粉色的便利貼表示)劲妙。
在尋找命令和角色的過程中湃鹊,你可能會(huì)遇到某些命令會(huì)在“特定的條件下”觸發(fā)。比如:“當(dāng)用戶通過新的設(shè)備登入時(shí)镣奋,系統(tǒng)會(huì)發(fā)送提醒通知”币呵。通常,我們將這種系統(tǒng)的行為邏輯稱為策略侨颈,通常記錄在紫丁香色的便利貼上余赢,放在命令旁邊。
尋找領(lǐng)域模型和聚合
當(dāng)我們做完了上一個(gè)環(huán)節(jié)哈垢,就可以開始尋找系統(tǒng)中的領(lǐng)域模型和聚合了妻柒。我們把跟一個(gè)概念相同的指令和事件集合到一起,并用黃色的較大的便利貼表示領(lǐng)域模型耘分。
把跟這個(gè)領(lǐng)域模型相關(guān)的命令放到左邊举塔,事件放到右邊。需要注意的是陶贼,這個(gè)時(shí)候會(huì)去掉“事件的相對(duì)順序”這個(gè)概念啤贩,因?yàn)槲覀円呀?jīng)不需要了。
可能有些領(lǐng)域模型不能作為一個(gè)獨(dú)立存在的對(duì)象拜秧。它應(yīng)該被另一個(gè)領(lǐng)域模型持有和使用。那這時(shí)候章郁,可以考慮把兩個(gè)模型合起來枉氮,形成一個(gè)聚合志衍。在最上面的模型就是這個(gè)聚合的聚合根,其之下的模型都是它的實(shí)體或值對(duì)象聊替。
劃分領(lǐng)域和限界上下文
找到領(lǐng)域模型以后楼肪,我們應(yīng)當(dāng)就可以比較輕松地劃分子域和限界上下文了。
在劃分限界上下文的時(shí)候也可以反過來檢驗(yàn)領(lǐng)域模型和通用語(yǔ)言的正確性惹悄。如果發(fā)現(xiàn)一個(gè)模型有歧義春叫,那它就應(yīng)該是限界上下文邊界的地方,我們應(yīng)該重新思考這個(gè)模型泣港,必要時(shí)進(jìn)行拆分暂殖。
應(yīng)用落地
分層架構(gòu)
分層架構(gòu)發(fā)展
四層架構(gòu)
傳統(tǒng)的四層架構(gòu)都是限定型松散分層架構(gòu),即Infrastructure層的任意上層都可以訪問該層(“L”型)当纱,而其它層遵守嚴(yán)格分層架構(gòu)呛每。
四層架構(gòu)
六邊形架構(gòu)
Alistair Cockburn在2005年提出,解決了傳統(tǒng)的分層架構(gòu)所帶來的問題坡氯,實(shí)際上它也是一種分層架構(gòu)晨横,只不過不是上下或左右,而是變成了內(nèi)部和外部箫柳。在領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)(DDD)和微服務(wù)架構(gòu)中都出現(xiàn)了六邊形架構(gòu)的身影手形,在《實(shí)現(xiàn)領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)》一書中,作者將六邊形架構(gòu)應(yīng)用到領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)的實(shí)現(xiàn)悯恍,六邊形的內(nèi)部代表了application和domain層叁幢,而在Chris Richardson對(duì)微服務(wù)架構(gòu)模式的定義中,每個(gè)微服務(wù)使用六邊形架構(gòu)設(shè)計(jì)坪稽,足見六邊形架構(gòu)的重要性曼玩。
六邊形架構(gòu)
洋蔥架構(gòu)
2008 年 Jeffrey Palermo 提出了洋蔥架構(gòu),它在端口和適配器架構(gòu)的基礎(chǔ)上貫徹了將領(lǐng)域放在應(yīng)用中心窒百,將傳達(dá)機(jī)制(UI)和系統(tǒng)使用的基礎(chǔ)設(shè)施(ORM黍判、搜索引擎、第三方 API...)放在外圍的思路篙梢。但是它前進(jìn)了一步顷帖,在其中加入了內(nèi)部層次。
端口和適配器架構(gòu)與洋蔥架構(gòu)有著相同的思路渤滞,它們都通過編寫適配器代碼將應(yīng)用核心從對(duì)基礎(chǔ)設(shè)施的關(guān)注中解放出來贬墩,避免基礎(chǔ)設(shè)施代碼滲透到應(yīng)用核心之中。這樣應(yīng)用使用的工具和傳達(dá)機(jī)制都可以輕松地替換妄呕,可以一定程度地避免技術(shù)陶舞、工具或者供應(yīng)商鎖定。
還有绪励,任何一個(gè)外部層次都可以直接調(diào)用任何一個(gè)內(nèi)部層次肿孵,這樣既不會(huì)破壞耦合的方向唠粥,也避免了僅僅為了追求分層模式而創(chuàng)建一些沒有任何業(yè)務(wù)邏輯的代理方法甚至代理類。這和 Martin Flowler 表達(dá)的偏好一致停做。
The Onion Architecture 原文地址
洋蔥架構(gòu)
干凈架構(gòu)
Robert C. Martin在2012年提出了干凈架構(gòu)(Clean Architecture)晤愧,這是六邊形架構(gòu)的一個(gè)變體,通過隔離變化和依賴倒置守護(hù)業(yè)務(wù)代碼蛉腌。
CleanArchitecture
清晰架構(gòu)
2017年又出現(xiàn)一個(gè)在Clean?Architecture基礎(chǔ)上增加CQRS的Explicit?Architecture官份。
?Explicit Architecture
雖然這些架構(gòu)在細(xì)節(jié)上都略有不同,但他們都非常相似烙丛。它們都具有相同的目標(biāo)舅巷,那就是分離關(guān)注。他們都通過軟件分層來實(shí)現(xiàn)這種分離蜀变。至少有一個(gè)層代表業(yè)務(wù)規(guī)則悄谐,而另一個(gè)層用于接口。
系統(tǒng)特點(diǎn):
獨(dú)立的框架库北,這樣的架構(gòu)并不依賴與應(yīng)用軟件的具體庫(kù)包爬舰,這樣可以將框架作為工具,而不必將你的系統(tǒng)都胡亂混合在一起
可測(cè)試寒瓦,業(yè)務(wù)規(guī)則能夠在沒有UI和數(shù)據(jù)庫(kù) 或Web服務(wù)器的情況下被測(cè)試
數(shù)據(jù)庫(kù)的獨(dú)立性情屹,你能夠在MySQL或SQL?Server、Mongo之間切換杂腰,你的業(yè)務(wù)規(guī)則不會(huì)和數(shù)據(jù)庫(kù)綁定
獨(dú)立的外部代理垃你,其實(shí)你的業(yè)務(wù)規(guī)則可以對(duì)其外面的技術(shù)世界毫無所知
依賴倒置原則
依賴倒置原則(Dependency Inversion Principle, DIP),它通過改變不同層之間的依賴關(guān)系達(dá)到改進(jìn)目的喂很。
依賴倒置原則由Robert C. Martin提出惜颇,正式定義為:
高層模塊不應(yīng)該依賴于底層模塊,兩者都應(yīng)該依賴于抽象少辣。
抽象不應(yīng)該依賴于細(xì)節(jié)凌摄,細(xì)節(jié)應(yīng)該依賴于抽象。
經(jīng)過我們多次的嘗試和探討后漓帅,最終我們選擇了干凈架構(gòu)作為落地的分層架構(gòu)锨亏,并且結(jié)合CQRS架構(gòu)。在此基礎(chǔ)上我們提供了一個(gè)Maven腳手架:http://coding.jd.com/com.jd.jr.cf/ddd-archetype/
依賴倒置
以上是我們工程的模塊的依賴圖忙干。在原干凈架構(gòu)的基礎(chǔ)上增加了API和Types器予、Query三個(gè)模塊。API主要是一些接口協(xié)議捐迫,Types封裝了領(lǐng)域內(nèi)一些特殊的值對(duì)象Domain?Primitives乾翔,而Query是基于CQRS的查詢端,可以建立獨(dú)立于Domain的查詢模型弓乙。
Domain
即上圖中的Entites層末融。這里面主要封裝了業(yè)務(wù)域的核心業(yè)務(wù)規(guī)則钧惧,包括了領(lǐng)域模型和領(lǐng)域服務(wù)暇韧。這一層封裝了這個(gè)領(lǐng)域最通用和最高層級(jí)的業(yè)務(wù)規(guī)則勾习,和業(yè)務(wù)規(guī)則不相關(guān)的改變都不應(yīng)該影響到這一層,這些業(yè)務(wù)規(guī)則不會(huì)輕易發(fā)生變化懈玻。這一層可以被其他的領(lǐng)域引用巧婶,這個(gè)可以理解為共享內(nèi)核。
Application
即上圖中的Use?Cases層涂乌。應(yīng)用層艺栈,主要封裝了業(yè)務(wù)用例(UseCase),為了和DomainService區(qū)分湾盒,在此ApplicationService使用UseCase的概念湿右,實(shí)際上二者等同。應(yīng)用層一般會(huì)比較薄罚勾,主要對(duì)Domain層進(jìn)行編排毅人。
Adapter
適配器層,Domain和Application為應(yīng)用核心尖殃,應(yīng)用核心與任何外部系統(tǒng)的交互都通過適配器層丈莺。適配器層實(shí)際上又分成兩大類,主動(dòng)適配和被動(dòng)適配送丰。主動(dòng)適配基本都是應(yīng)用的入口缔俄,比如JSF、MQ器躏、調(diào)度器等俐载,被動(dòng)適配基本都是應(yīng)用出口,比如訪問RPC接口登失、數(shù)據(jù)庫(kù)等遏佣。
主動(dòng)適配器是系統(tǒng)主動(dòng)適配一些組件
Boot
框架與驅(qū)動(dòng)層,主要包括了SpringBoot的啟動(dòng)類壁畸,AOP贼急、系統(tǒng)配置、Spring容器等捏萍。Domain和Application層不應(yīng)該直接依賴Spring容器太抓,這兩層的一些對(duì)象在Boot層注入Spring容器。
Query
CQRS的查詢層令杈,可以單獨(dú)定義不同于領(lǐng)域模型的查詢模型灾部。在CQRS中巩检,查詢的數(shù)據(jù)庫(kù)模型可以與命令中的數(shù)據(jù)庫(kù)模型不一樣,查詢的模型是針對(duì)查詢優(yōu)化的澄暮。這一層不是必須的,也可以獨(dú)立部署折剃。Query可以直接引入DO,將DO轉(zhuǎn)換成DTO對(duì)外輸出。
Api
外對(duì)暴露的接口的協(xié)議僧须。
Types
Types模塊是保存無狀態(tài)的邏輯的Domain?Primitives的地方。
模塊和包說明
|--- adapter???????????????????? -- 適配器層 應(yīng)用與外部應(yīng)用交互適配
|????? |--- controller?????????? -- 控制器層项炼,API中的接口的實(shí)現(xiàn)
|????? |?????? |--- assembler??? -- 裝配器担平,DTO和領(lǐng)域模型的轉(zhuǎn)換
|????? |?????? |--- impl???????? -- 協(xié)議層中接口的實(shí)現(xiàn)
|????? |--- repository?????????? -- 倉(cāng)儲(chǔ)層
|????? |?????? |--- assembler??? -- 裝配器,PO和領(lǐng)域模型的轉(zhuǎn)換
|????? |?????? |--- impl???????? -- 領(lǐng)域?qū)又袀}(cāng)儲(chǔ)接口的實(shí)現(xiàn)
|????? |--- rpc????????????????? -- RPC層,Domain層中port中依賴的外部的接口實(shí)現(xiàn)锭部,調(diào)用遠(yuǎn)程RPC接口
|????? |--- task???????????????? -- 任務(wù)暂论,主要是調(diào)度任務(wù)的適配器
|--- api???????????????????????? -- 應(yīng)用協(xié)議層 應(yīng)用對(duì)外暴露的api接口
|--- boot??????????????????????? -- 啟動(dòng)層 應(yīng)用框架、驅(qū)動(dòng)等
|????? |--- aop????????????????? -- 切面
|????? |--- config?????????????? -- 配置
|????? |--- Application????????? -- 啟動(dòng)類
|--- app???????????????????????? -- 應(yīng)用層
|????? |--- cases??????????????? -- 應(yīng)用服務(wù)
|--- domain????????????????????? -- 領(lǐng)域?qū)?/p>
|????? |--- model??????????????? -- 領(lǐng)域?qū)ο?/p>
|????? |?????? |--- aggregate??? -- 聚合
|????? |?????? |--- entities???? -- 實(shí)休
|????? |?????? |--- vo?????????? -- 值對(duì)象
|????? |--- service????????????? -- 域服務(wù)
|????? |--- factory????????????? -- 工廠拌禾,針對(duì)一些復(fù)雜的Object可以通過工廠來構(gòu)建
|????? |--- port???????????????? -- 端口取胎,即接口
|????? |--- event??????????????? -- 領(lǐng)域事件
|????? |--- exception??????????? -- 異常封裝
|????? |--- ability????????????? -- 領(lǐng)域能力
|????? |--- extension??????????? -- 擴(kuò)展點(diǎn)
|????? |?????? |--- impl??????? -- 擴(kuò)展點(diǎn)實(shí)現(xiàn)
|--- query?????????????????????? -- 查詢層,封裝讀服務(wù)
|????? |--- model??????????????? -- 查詢模型
|????? |--- service????????????? -- 查詢服務(wù)
|--- types?????????????????????? -- 定義Domain Primitive
在落地中遇到的問題
關(guān)于服務(wù)
到這大家已經(jīng)發(fā)現(xiàn)了應(yīng)用層也有Service湃窍,Domain層也有Service闻蛀。應(yīng)用服務(wù)和領(lǐng)域服務(wù)的劃分是一個(gè)難題,在干凈架構(gòu)中將應(yīng)用服務(wù)叫做Use?Case坝咐。
單從字面理解循榆,不管是領(lǐng)域服務(wù)還是應(yīng)用服務(wù),都是服務(wù)墨坚。而什么是服務(wù)秧饮?從SOA到微服務(wù),它們所描述的服務(wù)都是一個(gè)寬泛的概念泽篮,我們可以理解為服務(wù)是行為的抽象盗尸。從前綴來看,它們隸屬于不同的層帽撑,應(yīng)用服務(wù)屬于應(yīng)用層泼各,領(lǐng)域服務(wù)屬于領(lǐng)域?qū)印?/p>
應(yīng)用服務(wù)
應(yīng)用服務(wù)是用來表達(dá)用例(Use?Case)和用戶故事(User Story)的主要手段。應(yīng)用服務(wù)負(fù)責(zé)編排和轉(zhuǎn)發(fā)亏拉,它將要實(shí)現(xiàn)的功能委托給一個(gè)或多個(gè)領(lǐng)域?qū)ο髞韺?shí)現(xiàn)扣蜻,它本身只負(fù)責(zé)處理業(yè)務(wù)用例的執(zhí)行順序以及結(jié)果的拼裝。通過這樣一種方式及塘,它隱藏了領(lǐng)域?qū)拥膹?fù)雜性及其內(nèi)部實(shí)現(xiàn)機(jī)制莽使。
領(lǐng)域服務(wù)
當(dāng)領(lǐng)域中的某個(gè)操作過程或轉(zhuǎn)換過程不是實(shí)體或值對(duì)象的職責(zé)時(shí),我們便應(yīng)該將該操作放在一個(gè)單獨(dú)的接口中笙僚,即領(lǐng)域服務(wù)芳肌。請(qǐng)確保該服務(wù)和通用語(yǔ)言時(shí)是一致的,并且保證它是無狀態(tài)的。
兩個(gè)凡事
上面的定義比較抽象亿笤。那哪些邏輯應(yīng)該放在Use?Case翎迁,哪些該放在Domain?Service中呢?在此我們引用了兩個(gè)凡事的原則净薛。
1.凡是可以移動(dòng)到領(lǐng)域模型中的邏輯都不應(yīng)該出現(xiàn)在領(lǐng)域服務(wù)中
2.凡是可以移動(dòng)到領(lǐng)域?qū)又械倪壿嫸疾粦?yīng)該出現(xiàn)在應(yīng)用服務(wù)中
另外DomainService也不是必須的汪榔,一些簡(jiǎn)單的UseCase是可以直接構(gòu)造領(lǐng)域模型然后調(diào)用其方法。避免出現(xiàn)無業(yè)務(wù)邏輯的DomainService
關(guān)于事務(wù)
事務(wù)不是業(yè)務(wù)邏輯罕拂,就像存儲(chǔ)不是業(yè)務(wù)邏輯一樣揍异。事務(wù)是一個(gè)技術(shù)實(shí)現(xiàn)或者細(xì)節(jié)全陨,所以應(yīng)該隱藏在Adapter的Repository中爆班。但是有時(shí)候是無法在Repository中實(shí)現(xiàn),可以在DomainService中實(shí)現(xiàn)辱姨。
聚合內(nèi)事務(wù)
聚合是一個(gè)一致性的事務(wù)單元柿菩,所以對(duì)于一個(gè)聚合內(nèi)的事務(wù)應(yīng)該在對(duì)應(yīng)的Aggregate的Repository中。
跨聚合事務(wù)
如果出現(xiàn)跨Aggregate的事務(wù)應(yīng)該在ApplicationService層中雨涛,ApplicationService來編排這兩個(gè)Aggregate的Repository來保證事務(wù)一致性枢舶。
跨域事務(wù)
如果出現(xiàn)跨域的,也就是分布式事務(wù)替久,優(yōu)先考慮領(lǐng)域事件的方式凉泄,在另外一個(gè)域監(jiān)聽領(lǐng)域事件處理。
關(guān)于模型分類
開始我們并沒有對(duì)模型進(jìn)行分類規(guī)范蚯根,發(fā)現(xiàn)落地的過程中后众,有不少同事對(duì)這些模型并沒有清晰的認(rèn)識(shí),造成了模型的濫用颅拦,超出了它們的邊界蒂誉。因此我們強(qiáng)調(diào)了模型的分類,在這里我們將整個(gè)應(yīng)用中涉及的模型分成了三種距帅,分別為DTO右锨、DO和PO。
DTO:Data?Transfer?Object碌秸,數(shù)據(jù)傳輸對(duì)象绍移,在API模塊中定義,在Adapter中使用蹂窖,不能在Application和Domain中使用
DO:Domain?Object允趟,領(lǐng)域?qū)ο蠹搭I(lǐng)域模型(Domain?Model),在Domain中定義,在Adapter中和DTO涣楷、PO進(jìn)行轉(zhuǎn)換狮斗,一般在Application中構(gòu)造生成(如果有DomainService绽乔,則在DomainService中生成)。DO主要有三類碳褒,聚合(Aggregate)沙峻、實(shí)體(Entity)摔寨、值對(duì)象(ValueObject),另外Domain?Service也屬于領(lǐng)域模型
PO:Persistent?Object删顶,持久化對(duì)象即數(shù)據(jù)庫(kù)模型逗余,在Adapter中定義和使用季惩。(P.S.在阿里的規(guī)范中持久化對(duì)象又叫Data?Object蜀备,簡(jiǎn)稱DO碾阁,實(shí)際上就是PO脂凶,二者等值的宪睹,此處為了和領(lǐng)域?qū)ο髤^(qū)分亭病,使用了PO的定義)
模型分類
模型的轉(zhuǎn)換
DTO和DO嘶居、PO的轉(zhuǎn)換,可以通過一些通用工具解決菠齿。
如果是字段相同绳匀,可以使用CGLIB BeanCopier疾棵,Spring帶的BeanUtil實(shí)際上就是使用CGLIB BeanCopier的痹仙。
如果有嵌套的字段映射蝶溶,可以使用MapStruct抖所,非常方便的進(jìn)行類型轉(zhuǎn)換田轧。
<dependency>
????????<groupId>org.mapstruct</groupId>
????????<artifactId>mapstruct</artifactId>
????????<version>${org.mapstruct.version}</version>
????</dependency>
關(guān)于實(shí)體和值對(duì)象
在授信域下的準(zhǔn)入域落地的過程中傻粘。通過對(duì)用戶準(zhǔn)入的用例的分析弦悉,我們將用例中的一些名詞列出來分析后稽莉。將用戶污秆、業(yè)務(wù)身份昧甘、校驗(yàn)結(jié)果充边、準(zhǔn)入申請(qǐng)記錄列為了領(lǐng)域?qū)ο蟆F渲杏脩暨@個(gè)對(duì)象的爭(zhēng)議比較大刮吧,因?yàn)橛械耐抡J(rèn)識(shí)用戶是一個(gè)實(shí)體杀捻,因?yàn)樗形ㄒ粯?biāo)識(shí)致讥,用戶PIN垢袱。在討論這個(gè)問題之前请契,我們先回顧一下實(shí)體和值對(duì)象的定義爽锥。
實(shí)體
實(shí)體(Entity)畔柔, 主要由標(biāo)識(shí)定義的對(duì)象靶擦。它可以是任何事物玄捕,只要滿足兩個(gè)條件即可枚粘,一是它在整個(gè)生命周期中具有連續(xù)性赌结;二是它的區(qū)別并不是由那些對(duì)用戶非常重要的屬性決定的柬姚。
其實(shí)這兩點(diǎn)要簡(jiǎn)化為具有唯一標(biāo)識(shí)和生命周期量承。唯一標(biāo)識(shí)可以進(jìn)一步理解為業(yè)務(wù)編號(hào),比如訂單實(shí)體中的訂單號(hào)泣洞。生命周期則一般可以理解為持久化球凰,實(shí)際上生命周期是標(biāo)識(shí)在實(shí)體生命周期內(nèi)體現(xiàn)出連續(xù)性呕诉。
值對(duì)象
值對(duì)象(Value Object)吃度,用于描述領(lǐng)域的某個(gè)方面而本身沒有概念的對(duì)象稱為值對(duì)象椿每,值對(duì)象被實(shí)例化之后用來表示一些設(shè)計(jì)元素间护,對(duì)于這些設(shè)計(jì)元素兑牡,我們只關(guān)心它們是什么均函,不關(guān)心它是誰(shuí)苞也。
另外同一個(gè)事物(對(duì)象)在不同的上下文中可以是實(shí)體或者值對(duì)象如迟,但是在同一上下文中是確定的殷勘。值對(duì)象最大的特點(diǎn)是不可變的玲销。
度量或描述領(lǐng)域中的一件東西
可以作為不變對(duì)象
將不同的相關(guān)屬性組合成一個(gè)概念整體
當(dāng)度量或描述改變時(shí)贤斜,可以使用另一個(gè)值對(duì)象予以替換
可以與其他值對(duì)象進(jìn)行相等性比較
不會(huì)對(duì)協(xié)作對(duì)象造成負(fù)面影響
那么用戶為什么在準(zhǔn)入域中是一個(gè)值對(duì)象而不是一個(gè)實(shí)體呢瘩绒?用戶是具有唯一標(biāo)識(shí)Pin的锁荔,但是我們無需對(duì)這個(gè)用戶的狀態(tài)進(jìn)行維護(hù)阳堕,也就是在準(zhǔn)入上下文中是不需要維護(hù)用戶的生命周期的嘱丢。區(qū)別值對(duì)象的一個(gè)更簡(jiǎn)單的方法是越驻,這個(gè)對(duì)象是不是當(dāng)前這個(gè)業(yè)務(wù)域存儲(chǔ)的缀旁,從外部獲取的對(duì)象基本可以確定為值對(duì)象(在共享內(nèi)核的情況下可能會(huì)有不一樣的判定)并巍。在準(zhǔn)入上下文中實(shí)際上我們是只是關(guān)注了用戶的部分屬性懊渡。顯而易見用戶在用戶域是一個(gè)實(shí)體剃执。為了更好的區(qū)分實(shí)體和值對(duì)象肾档,這里可以引入一個(gè)概念怒见,最小化集成原則遣耍。
最小化集成原則
在 DDD 項(xiàng)目中通常存在多個(gè)限界上下文配阵,意味著我們需要找到合適的方法對(duì)這些上下文進(jìn)行集成,當(dāng)模型概念從上游上下文流入下游上下文中時(shí)难审,盡量使用值對(duì)象來表示這些概念告喊,這樣的好處是可以達(dá)到最小化集成黔姜,既可以最小化下游模型中的屬性數(shù)目秆吵,又可以使用不變的值對(duì)象減少職責(zé)假設(shè)纳寂。
研發(fā)是喜歡“偷懶”的。所以在落地討論時(shí)腋粥,一些同事也提出了我們?cè)跍?zhǔn)入域?yàn)槭裁床荒苤苯右糜脩粲虻挠脩魧?shí)體隘冲,這樣就可以不需要再在準(zhǔn)入域建一個(gè)用戶對(duì)象了对嚼。按照最小化集成原則,我們應(yīng)該在準(zhǔn)入域建一個(gè)用戶值對(duì)象杏愤。用戶域的用戶模型是很復(fù)雜的珊楼,但是在準(zhǔn)入域我們只關(guān)心用戶極少的屬性厕宗,甚至都不關(guān)心用戶本身具備的行為能力已慢。如果直接引入用戶域的用戶實(shí)體佑惠,也意味著用戶域的用戶實(shí)體的修改時(shí)有可能影響到我們的業(yè)務(wù)旭咽。使用值對(duì)象的概念也能將準(zhǔn)入域和用戶域進(jìn)行很好的隔離穷绵,起到防腐層的作用仲墨。所以兩個(gè)域之間建議通過DTO進(jìn)行傳輸宗收,避免直接引入其他域的領(lǐng)域模型混稽。
我們應(yīng)該盡量使用值對(duì)象建模而不是實(shí)體對(duì)象匈勋,因?yàn)槲覀兛梢苑浅H菀椎貙?duì)值對(duì)象進(jìn)行創(chuàng)建洽洁、測(cè)試、使用昭雌、優(yōu)化和維護(hù)
關(guān)于值對(duì)象的不可變
可以這么說吧烛卧,不可變是值對(duì)象最顯著的特征总放。
關(guān)于值對(duì)象的不可變局雄,也讓一些同事感到困惑哎榴。他們認(rèn)為值對(duì)象還是變的尚蝌,比如顏色可以改變衣形。其實(shí)顏色可以改變描述并不準(zhǔn)確谆吴,得引入語(yǔ)境句狼。比如衣服的顏色可以改變腻菇,是衣服這個(gè)實(shí)體的顏色的屬性改變了,并不是顏色這個(gè)值對(duì)象本身變了丘薛。這改變衣服顏色這個(gè)用例中洋侨,衣服是實(shí)體凰兑,顏色是值對(duì)象。有位同事就舉了在商城下單時(shí)的問題锅知,他說在下單時(shí)地址是可以改變的售睹。這個(gè)問題是復(fù)雜的昌妹,我們還是從用例開始解析飞崖,首先要準(zhǔn)確的描述這個(gè)用例固歪,特別是準(zhǔn)確描述其語(yǔ)境(即上下文)牢裳。在下單時(shí)修改地址蒲讯,實(shí)際是由兩個(gè)用例組成的,用戶在地址管理頁(yè)修改地址脊另,在下單頁(yè)從地址列表中選擇一個(gè)地址偎痛。這兩個(gè)用例實(shí)際上是兩個(gè)不同的上下文。用戶在地址管理頁(yè)修改地址谓谦,在這個(gè)語(yǔ)境下反粥,地址就是實(shí)體才顿,所以是修改了常用地址的這個(gè)實(shí)體上的地址(省市區(qū))幅垮。在下單頁(yè)用戶修改地址忙芒,實(shí)際上是從常用地址中選擇一個(gè)新的地址呵萨,這個(gè)修改地址實(shí)際上是將地址這個(gè)值對(duì)象用另外一個(gè)值對(duì)象替換了,常用地址本身并沒有修改跑杭。說到這里,其實(shí)訂單的收貨地址修改和衣服的顏色改變是相似的咆耿。
Domain?Primitive
Domain?Primitive是一種特殊的值對(duì)象德谅,用來描述標(biāo)準(zhǔn)模型,標(biāo)準(zhǔn)模型是用于表示事物類型的描述性對(duì)象萨螺。
Primitive的定義是:不人任何其他事物發(fā)展而來窄做,初級(jí)的形成或者生長(zhǎng)的早期階段。
這么說吧椭盏,標(biāo)準(zhǔn)模型就是“放之四海而皆準(zhǔn)”的模型,這個(gè)模型在大部分上下文中都是一致的吻商。比如Money這個(gè)對(duì)象掏颊,Money是由面值和貨幣類型組成的,在絕大部分語(yǔ)境中我們也只關(guān)心這兩個(gè)屬性(在造幣廠就需要關(guān)心更多的屬性了)艾帐。
常見的 DP 的使用場(chǎng)景包括:
有格式限制的 String:比如Name乌叶,PhoneNumber,OrderNumber柒爸,ZipCode准浴,Address等
有限制的Integer:比如OrderId(>0),Percentage(0-100%)捎稚,Quantity(>=0)等
可枚舉的 int :比如 Status(一般不用Enum因?yàn)榉葱蛄谢瘑栴})
Double 或 BigDecimal:一般用到的 Double 或 BigDecimal 都是有業(yè)務(wù)含義的乐横,比如 Temperature求橄、Money、Amount晰奖、ExchangeRate谈撒、Rating 等
復(fù)雜的數(shù)據(jù)結(jié)構(gòu):比如 Map> 等,盡量能把 Map 的所有操作包裝掉匾南,僅暴露必要行為
事例:
@Value
public?class?ExchangeRate {
????private?BigDecimal rate;
????private?Currency from;
????private?Currency to;
????public?ExchangeRate(BigDecimal rate, Currency from, Currency to) {
????????this.rate = rate;
????????this.from = from;
????????this.to = to;
????}
????public?Money exchange(Money fromMoney) {
????????notNull(fromMoney);
????????isTrue(this.from.equals(fromMoney.getCurrency()));
????????BigDecimal targetAmount = fromMoney.getAmount().multiply(rate);
????????return?new?Money(targetAmount, to);
????}
}
關(guān)于Domian?Primitive的詳細(xì)可以參考?阿里技術(shù)專家詳解 DDD 系列- Domain Primitive
關(guān)于領(lǐng)域模型的加載性能
?這個(gè)主要是針對(duì)聚合的。比如商戶聚合蛔外,商戶聚合由商戶實(shí)體和門店實(shí)體聚合而成蛆楞,如下
@Getter
public?class?MerchantAggregate {
????private?MerchantEntity merchantEntity;
????private?List<StoreEntity> storeEntities;
}
每次重建時(shí)都會(huì)一次從數(shù)據(jù)庫(kù)中將商戶信息和其下的門店信息。當(dāng)門店很少的時(shí)候并不存在什么問題夹厌,但是當(dāng)一個(gè)商戶有5000家門店時(shí)豹爹。本來我們只是操作一下商戶實(shí)體的一些基本信息,但卻將這5000個(gè)門店實(shí)體加載到了內(nèi)存中矛纹。這個(gè)對(duì)性能影響比較大臂聋。這個(gè)怎么處理呢。接下來我們將對(duì)DDD里面的模型做一個(gè)總結(jié)或南,在合適的情況選擇合適的模型孩等。
失血模型
失血模型中,domain object只有屬性的get set方法的純數(shù)據(jù)類采够,所有的業(yè)務(wù)邏輯完全由Service層來完成的肄方。
service:? 腫脹的服務(wù)邏輯
model:只包含get set方法
顯然失血模型service層負(fù)擔(dān)太重,在DDD中一般不會(huì)有這種設(shè)計(jì)蹬癌。
貧血模型
貧血模型中权她,domain ojbect包含了不依賴于持久化的原子領(lǐng)域邏輯,而組合邏輯在Service層逝薪。
service :組合服務(wù)隅要,也叫事務(wù)服務(wù)
model:除包含get set方法,還包含原子服務(wù)
貧血模型比較常見董济,其問題在于原子服務(wù)往往不能直接拿到關(guān)聯(lián)model步清,因此可以把這個(gè)原子服務(wù)變成直接用關(guān)聯(lián)modelRepo拿到關(guān)聯(lián)model,這就是充血模型感局。
充血模型
充血模型中尼啡,絕大多業(yè)務(wù)邏輯都應(yīng)該被放在domain object里面,包括持久化邏輯询微,而Service層是很薄的一層崖瞭,僅僅封裝事務(wù)和少量邏輯,不和DAO層打交道撑毛。
service :組合服務(wù) 也叫事務(wù)服務(wù)
model:除包含get set方法书聚,還包含原子服務(wù)和數(shù)據(jù)持久化的邏輯
充血模型的問題也很明顯唧领,當(dāng)model中包含了數(shù)據(jù)持久化的邏輯,實(shí)例化的時(shí)候可能會(huì)有很大麻煩雌续,拿到了太多不一定需要的關(guān)聯(lián)model斩个。
脹血模型
脹血模型取消了Service層,在domain object的domain logic上面封裝事務(wù)驯杜。
在這個(gè)商戶聚合中受啥,我們可以采用充血模型,也就是在商戶聚合中直接持有門店的Repositoy鸽心,這樣就可以實(shí)現(xiàn)懶加載滚局。只有使用門店的時(shí)候再通過門店Repository獲取門店顽频,這樣就避免了一次性加載過多的門店從而影響性能藤肢。
@Getter
public?class?MerchantAggregate {
????private?final?MerchantEntity merchantEntity;
????private?final?StoreRepository storeRepository;
????private?List<StoreEntity> storeEntities;
????public?MerchantAggregate(MerchantEntity merchantEntity, StoreRepository storeRepository) {
????????this.merchantEntity = merchantEntity;
????????this.storeRepository = storeRepository;
????}
????public?List<StoreEntity> getStoreEntities(){
????????this.storeEntities = storeRepository.getAll(merchantEntity.getMerchantId());
????????return?storeEntities;
????}
????private?StoreEntity getOne(String storeId){
????????return?storeRepository.getOne(storeId);
????}
}
這個(gè)StoreRepository也可以提供根據(jù)門店ID獲取門店的方法糯景,這樣在商戶聚合中就可以使用嘁圈,盡量避免全部加載門店蟀淮。其實(shí)不推薦使用這種模型最住,這種模型只有在特定的場(chǎng)景才應(yīng)該使用。
關(guān)于模型共享
模型共享的問題温学,其實(shí)上面已經(jīng)提到了部分。為什么單獨(dú)再?gòu)?qiáng)調(diào)一遍呢甚疟?因?yàn)楹芏嗳硕紝?duì)這塊提出了疑問。為什么不能直接引用用戶域的用戶實(shí)體
康威定律
在講這個(gè)模型共享前我們先了解一下康威定律
Conway’s law: Organizations which design systems are constrained to produce designs which are copies of the communication structures of these organizations. - Melvin Conway(1967)
設(shè)計(jì)系統(tǒng)的組織其產(chǎn)生的設(shè)計(jì)等價(jià)于組織間的溝通結(jié)構(gòu)览妖。
在限界上下文(Bounded?Context)
任何大型項(xiàng)目都會(huì)存在多個(gè)模型 轧拄。而當(dāng)基于不同模型的代碼被組合到一起后,軟件就會(huì)出現(xiàn)bug檩电,變得不可靠和難以理解。團(tuán)隊(duì)成員之間的溝通變得混亂俐末。人們往往弄不清楚?一個(gè)模型不應(yīng)該在哪個(gè)上下文中被使用。
明確地定義模型所應(yīng)用的上下文奄侠。根據(jù)團(tuán)隊(duì)的組織,軟件系統(tǒng)的各個(gè)部分的用法?以及物理表現(xiàn)(代碼和數(shù)據(jù)庫(kù)模式等)來設(shè)置模型的邊界垄潮。在這些邊界中嚴(yán)格保持模型的一致性闷盔,而不要受到邊界之外問題的干擾和混淆旅急。
ANTICORRUPTION?LAYER(防腐層)
以下是完全基于防腐層設(shè)計(jì)的服務(wù)間通訊逢勾。上游系統(tǒng)通過開放公共主機(jī)的方式提供服務(wù)(主動(dòng)適配器)藐吮,下游通過被動(dòng)適配器來隔離上游系統(tǒng)的服務(wù)溺拱。
Shared?Kernel通常是Core?Domain谣辞,或者是一組Generic?SubDomain,也可能二者兼有潦闲,它可以是兩個(gè)團(tuán)隊(duì)都需要的任何一部分模型迫皱。不僅僅可以共享Domain歉闰,也可以共享相關(guān)的Repository卓起。使用共享內(nèi)核的目的是減少重復(fù)(并不是消除重復(fù),因?yàn)橹挥性谝粋€(gè)Bounded?Context中才能消除重復(fù))戏阅,并兩個(gè)系統(tǒng)之間的集成變得相對(duì)容易一些昼弟。
在消金領(lǐng)域內(nèi)奕筐,我們把賬單域又分成了核心賬單域(主要處理用戶賬單的資金科目的變動(dòng))、貸款域离赫、還款域、逾期域渊胸、退款域,貸款(這里指正向交易)翎猛、還款胖翰、逾期、退款的上下文中都會(huì)對(duì)用戶的核心賬單進(jìn)行修改切厘。如果使用ACL的模式萨咳,集成起來也比較費(fèi)勁迂卢,并且也會(huì)增加RPC或者分布式事務(wù)的問題桐汤。這個(gè)時(shí)候就可以把核心賬單域作為一個(gè)共享內(nèi)核提供給其他四個(gè)子域,這個(gè)情況下核心賬單域也不需要單獨(dú)部署一個(gè)應(yīng)用怔毛,只需要集成到其他四個(gè)子域中即可。
關(guān)于對(duì)象的創(chuàng)建和管理
在干凈架構(gòu)中拣度,內(nèi)圈是不依賴Spring框架的。很多同事反饋沒有Spring怎么管理這些對(duì)象呢螃壤,甚至有的人開玩笑說沒有了Spring都不會(huì)編程了。所以在此我們對(duì)這個(gè)作了說明奸晴。
對(duì)于有狀態(tài)的對(duì)象可以通過new的方式創(chuàng)建,如果這個(gè)對(duì)象是全局唯一的共享的寄啼,可以設(shè)置成單例,系統(tǒng)初始化時(shí)創(chuàng)建墩划。
對(duì)于無狀態(tài)的對(duì)象可以交由Spring管理。Domian和Application層的對(duì)象(比如應(yīng)用服務(wù)和領(lǐng)域服務(wù))同樣可以交由Spring容器管理乙帮,因?yàn)檫@兩層不依賴Spring,可以通過構(gòu)造方法的方式傳入依賴的其他的Spring管理的Bean察净,在最外層的Boot層由Spring創(chuàng)建管理。
關(guān)于構(gòu)造方法的說明塞绿,由于是通過構(gòu)造方法的方式進(jìn)行初始化,可能會(huì)帶來一些壞味道异吻,比如長(zhǎng)參數(shù)列表。但是這個(gè)壞味道是可以接受的诀浪,因?yàn)闃?gòu)造方法的方式提供了一個(gè)很好的檢驗(yàn)機(jī)制棋返,避免產(chǎn)生必要參數(shù)沒有遺漏輸入的問題。另外當(dāng)構(gòu)造一個(gè)對(duì)象比較麻煩時(shí)雷猪,可以引入Factory,通過Factory構(gòu)建對(duì)象求摇。
如上所述我們希望在應(yīng)用和領(lǐng)域?qū)颖M量不依賴框架殊者,比如Spring验夯,在最外層的Boot層由Spring創(chuàng)建并管理猖吴。這個(gè)時(shí)候就需要在外層寫大量的@bean代碼
package?com.jd.jr.cf.dawn.example.config;
import?com.jd.jr.cf.dawn.example.service.UserServiceImpl;
import?com.jd.jr.cf.dawn.example.service.PersonService;
import?com.jd.jr.cf.dawn.example.service.PersonServiceImpl;
import?org.springframework.context.annotation.Bean;
import?org.springframework.context.annotation.Configuration;
@Configuration
public?class?ExampleConfigDawnLoader {
????@Bean
????public?UserServiceImpl userService(PersonService personService) {
????????return?new?UserServiceImpl(personService);
????}
????@Bean
????public?PersonServiceImpl personService() {
????????return?new?PersonServiceImpl();
????}
}
為了我們提供了一個(gè)組件,可以自動(dòng)生成上述的樣版代碼海蔽,只需要聲明一下該類即可,組件是在編譯時(shí)生成的代碼党窜,會(huì)自動(dòng)根據(jù)構(gòu)造方法生成相對(duì)應(yīng)的@Bean代碼。
package?com.jd.jr.cf.dawn.example.config;
import?com.jd.jr.cf.dawn.annotation.DawnLoader;
import?com.jd.jr.cf.dawn.example.service.PersonServiceImpl;
import?com.jd.jr.cf.dawn.example.service.UserServiceImpl;
@DawnLoader
public?class?ExampleConfig {
????private?UserServiceImpl userService;
????private?PersonServiceImpl personService;
}
關(guān)于并發(fā)安全
其實(shí)DDD的落地也是有一個(gè)套路的幌衣,在DomainService中一般都是這樣的“DDD八股文”壤玫。首先從Factory中新增領(lǐng)域?qū)ο蠡蛘邚腞epository加載領(lǐng)域?qū)ο笃寐樱缓笳{(diào)用領(lǐng)域?qū)ο蟮姆椒严福詈笳{(diào)用Repository進(jìn)行store對(duì)象挡逼。簡(jiǎn)單的處理模型就是括改,創(chuàng)建對(duì)象-》調(diào)用對(duì)象的方法-》保存對(duì)象家坎。這樣就產(chǎn)生了一個(gè)問題了,并發(fā)安全問題虱疏。讀取對(duì)象惹骂、處理對(duì)象和保存對(duì)象不是一個(gè)原子操作做瞪,
比如扣減額度的處理模型对粪,從數(shù)據(jù)庫(kù)中加載賬戶實(shí)體装蓬,調(diào)用實(shí)體的扣減額度的方法,持久化賬戶實(shí)體牍帚。我們這邊傳統(tǒng)的做法是,使用SQL語(yǔ)句來操作額度并避免其扣減為負(fù)數(shù)暗赶。
UPDATE?cf_userbalance
SET
limitBalance = limitBalance - #{userBalance.limitBalance,jdbcType=DECIMAL}
WHERE?limitBalance - #{userBalance.limitBalance,jdbcType=DECIMAL} >= 0;
但是在DDD中肃叶,所有的業(yè)務(wù)邏輯都應(yīng)該是內(nèi)聚內(nèi)賬戶實(shí)體中的,也就是在賬戶實(shí)體中進(jìn)行操作額度因惭。那怎么保證這個(gè)并發(fā)安全呢?在此我們引入了樂觀鎖的機(jī)制筛欢。其實(shí)每次重載對(duì)象的時(shí)候都帶上了版本號(hào),處理完后持久化時(shí)版姑,在條件語(yǔ)句中帶個(gè)版本號(hào)。
這個(gè)樂觀鎖在很多ORM框架中都已經(jīng)支持迟郎,使用起來特別方便剥险,只需要增加一個(gè)@Version的注解即可宪肖,比如Spring?Data?Jpa和Mybatis?Plus。其實(shí)JPA很適合DDD控乾,比較推薦使用Spring?Data?Jpa,可以極大的簡(jiǎn)化代碼蜕衡。
關(guān)于讀寫分離
我們的領(lǐng)域?qū)ο缶褪腔跇I(yè)務(wù)進(jìn)行建模的,特別是我們?cè)诮r(shí)也不怎么關(guān)心持久化慨仿。
但是作為一個(gè)業(yè)務(wù)系統(tǒng),「查詢」的相關(guān)功能也是不可或缺的镰吆。在實(shí)現(xiàn)各式各樣的查詢功能時(shí),往往會(huì)發(fā)現(xiàn)很難用領(lǐng)域模型來實(shí)現(xiàn)万皿。假設(shè)在用戶需要一個(gè)訂單相關(guān)信息的查詢功能,展現(xiàn)的是查詢結(jié)果的列表牢硅。列表中的數(shù)據(jù)來自于「訂單」,「商品」唤衫,「品類」,「送貨地址」等多個(gè)領(lǐng)域?qū)ο笾械哪硯讉€(gè)字段。這樣的場(chǎng)景如果還是通過領(lǐng)域?qū)ο髞矸庋b就顯的很麻煩蛆挫,其次與領(lǐng)域知識(shí)也沒有太緊密的關(guān)系。
此時(shí) CQRS 作為一種模式可以很好的解決以上的問題悴侵。實(shí)際上在消金我們已經(jīng)在很多場(chǎng)景使用了讀寫分離的了拭嫁,比如大量使用了預(yù)熱可免,賬戶預(yù)熱做粤、訂單預(yù)熱。大量的讀服務(wù)都是通過預(yù)熱服務(wù)怕品。這個(gè)已經(jīng)算是CQRS的雛形了。
CQRS
CQRS — Command Query Responsibility Segregation肉康,故名思義是將 command 與 query 分離的一種模式。
CQRS 將系統(tǒng)中的操作分為兩類吼和,即「命令」(Command) 與「查詢」(Query)涨薪。命令則是對(duì)會(huì)引起數(shù)據(jù)發(fā)生變化操作的總稱炫乓,即我們常說的新增,更新厢岂,刪除這些操作阳距,都是命令塔粒。而查詢則和字面意思一樣筐摘,即不會(huì)對(duì)數(shù)據(jù)產(chǎn)生變化的操作,只是按照某些條件查找數(shù)據(jù)咖熟。
CQRS 的核心思想是將這兩類不同的操作進(jìn)行分離,然后在兩個(gè)獨(dú)立的「服務(wù)」中實(shí)現(xiàn)馍管。這里的「服務(wù)」一般是指兩個(gè)獨(dú)立部署的應(yīng)用。在某些特殊情況下确沸,也可以部署在同一個(gè)應(yīng)用內(nèi)的不同接口上俘陷。
Command 與 Query 對(duì)應(yīng)的數(shù)據(jù)源也應(yīng)該是互相獨(dú)立的观谦,即更新操作在一個(gè)數(shù)據(jù)源拉盾,而查詢操作在另一個(gè)數(shù)據(jù)源上豁状。
————————————————
版權(quán)聲明:本文為CSDN博主「vow_」的原創(chuàng)文章,遵循CC 4.0 BY-SA版權(quán)協(xié)議泻红,轉(zhuǎn)載請(qǐng)附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/qq_30757161/article/details/116485388