前言
領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)是一項(xiàng)艱巨的技術(shù)挑戰(zhàn),但它也會(huì)帶來(lái)豐厚的回報(bào),當(dāng)大多數(shù)軟件項(xiàng)目開(kāi)始僵化而成為遺留系統(tǒng)時(shí),它卻為你敞開(kāi)了機(jī)會(huì)的大門(mén)。
現(xiàn)在面臨的問(wèn)題
過(guò)度耦合
過(guò)度耦合有兩方面,一方面是領(lǐng)域之間沒(méi)有拆分,由于業(yè)務(wù)初期,我們的功能大都非常簡(jiǎn)單,普通的CRUD就能滿足,此時(shí)系統(tǒng)是清晰的。隨著迭代的不斷演化,業(yè)務(wù)邏輯變得越來(lái)越復(fù)雜,我們的系統(tǒng)也越來(lái)越冗雜。模塊彼此關(guān)聯(lián),誰(shuí)都很難說(shuō)清模塊的具體功能意圖是啥。修改一個(gè)功能時(shí),往往光回溯該功能需要的修改點(diǎn)就需要很長(zhǎng)時(shí)間,更別提修改帶來(lái)的不可預(yù)知的影響面。
另一方面是業(yè)務(wù)邏輯和一些膠水適配的邏輯耦合。有時(shí)候我們的代碼寫(xiě)得不好,往往是在我們代碼中依賴(lài)了大量的外部服務(wù),而這些服務(wù)又往往不是為我們定制的,我們需要寫(xiě)大量的適配。結(jié)果就是我們的核心業(yè)務(wù)邏輯被那些非核心業(yè)務(wù)邏輯所淹沒(méi)。比如我們有一個(gè)給用戶(hù)發(fā)逾期短信的用例,本來(lái)是很簡(jiǎn)單的一個(gè)業(yè)務(wù)邏輯,跑出所有的逾期分期單并發(fā)出逾期事件,監(jiān)聽(tīng)這些逾期事件去取手機(jī)手機(jī),然后調(diào)用短信平臺(tái)的接口發(fā)送短信。但是歷史原因我們的手機(jī)號(hào)碼保存在不同的地方,我們先取金融手機(jī)號(hào)碼,如果不存在金融手機(jī)號(hào)碼則查詢(xún)用戶(hù)開(kāi)白條時(shí)實(shí)名手機(jī)號(hào)碼,如果不存在實(shí)名手機(jī)號(hào)碼則獲取申請(qǐng)分期貸款時(shí)填寫(xiě)的手機(jī)號(hào)碼。結(jié)果就是我們的發(fā)逾期短信的業(yè)務(wù)邏輯中大部分都是取獲取手機(jī)號(hào)碼的邏輯。
貧血癥和失憶癥
在我們習(xí)慣了J2EE的開(kāi)發(fā)模式后,Action/Service/DAO這種分層模式,會(huì)很自然地寫(xiě)出過(guò)程式代碼,而學(xué)到的很多關(guān)于OO理論的也毫無(wú)用武之地。使用這種開(kāi)發(fā)方式,對(duì)象只是數(shù)據(jù)的載體,沒(méi)有行為。以數(shù)據(jù)為中心,以數(shù)據(jù)庫(kù)ER設(shè)計(jì)作驅(qū)動(dòng)。分層架構(gòu)在這種開(kāi)發(fā)模式下,可以理解為是對(duì)數(shù)據(jù)移動(dòng)、處理和實(shí)現(xiàn)的過(guò)程。
當(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)采用這種貧血模型和過(guò)程化設(shè)計(jì)是沒(méi)有問(wèn)題的,但在業(yè)務(wù)邏輯復(fù)雜了,業(yè)務(wù)邏輯、狀態(tài)會(huì)散落到在大量方法中,原本的代碼意圖會(huì)漸漸不明確,我們將這種情況稱(chēng)為由貧血癥引起的失憶癥。
業(yè)務(wù)規(guī)則泄露
在我們開(kāi)始接觸軟件開(kāi)發(fā)時(shí)就被告知,代碼復(fù)用可以極大的提高我們的工作效率。所以大部分后端研發(fā)提供的API接口或者方法都是極其開(kāi)放的,而且我們是直接使用數(shù)據(jù)庫(kù)模型,往往一個(gè)更新接口可以更新這個(gè)模型的所有字段,這樣幾乎可以適用于所有的更新場(chǎng)景。結(jié)果就是所有的調(diào)用方都可以修改這個(gè)模型的幾乎所有屬性。我甚至看到過(guò)可以修改數(shù)據(jù)庫(kù)自增ID的接口。這個(gè)是一個(gè)極端的例子。
回到業(yè)務(wù)上來(lái)。我們有一個(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)的,這種不確定性增加了出問(wèn)題的機(jī)率。
軟件核心復(fù)雜性應(yīng)對(duì)之道
統(tǒng)一語(yǔ)言
同一對(duì)象在不同上下文中的概念可能是不同的。
領(lǐng)域太復(fù)雜,只有在分割的上下文內(nèi)才可能形成統(tǒng)一語(yǔ)言。
比如同一件物品,iPhone12ProMax512G。在購(gòu)買(mǎi)的上下文中是商品,但是在配送上下文中是貨物。在不同的上下文中,關(guān)注的屬性也是不一樣的。
戰(zhàn)略設(shè)計(jì)
戰(zhàn)略設(shè)計(jì)側(cè)重于高層次、宏觀上去劃分和集成限界上下文。消費(fèi)金融目前主要支持了白條和金條業(yè)務(wù)線。在我們進(jìn)行業(yè)務(wù)體系化的建設(shè)中,把消金的信貸領(lǐng)域劃分成了授信域、用戶(hù)域、用戶(hù)域、賬戶(hù)域、交易域、營(yíng)銷(xiāo)域、觸達(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)性(相同、相似的名稱(chēng)),然后去提煉一個(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ò)分析它他們之間相關(guān)梳理出聚合來(lái)。
領(lǐng)域?qū)ο髣澐?/p>
無(wú)狀態(tài)和有狀態(tài)
對(duì)象是有狀態(tài)的,服務(wù)是無(wú)狀態(tài)的。由于Spring以及失血模型的流行,我們大量的業(yè)務(wù)邏輯都是在無(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ā),在此過(guò)程中團(tuán)隊(duì)經(jīng)過(guò)充分討論,統(tǒng)一語(yǔ)言,最后找到領(lǐng)域模型。
事件風(fēng)暴是一項(xiàng)團(tuán)隊(duì)活動(dòng),領(lǐng)域?qū)<遗c項(xiàng)目團(tuán)隊(duì)通過(guò)頭腦風(fēng)暴的形式,羅列出領(lǐng)域中所有的領(lǐng)域事件,整合之后形成最終的領(lǐng)域事件集合,然后對(duì)每一個(gè)事件,標(biāo)注出導(dǎo)致該事件的命令,再為每一個(gè)事件標(biāo)注出命令發(fā)起方的角色。
命令可以是用戶(hù)發(fā)起,也可以是第三方系統(tǒng)調(diào)用或者定時(shí)器觸發(fā)等,最后對(duì)事件進(jìn)行分類(lèi),整理出實(shí)體、聚合、聚合根以及限界上下文。
核心概念
事件(Event):事件風(fēng)暴的核心概念,事件是過(guò)去發(fā)生的與業(yè)務(wù)有關(guān)的事實(shí)。一般使用賓語(yǔ)+動(dòng)詞的過(guò)去式,例如:申請(qǐng)單被風(fēng)控審批通過(guò),訂單被支付成功。
命令(Command):命令即動(dòng)作,命令會(huì)改變對(duì)象的狀態(tài),并產(chǎn)生相應(yīng)的事件。比如:成功支付訂單、取消訂單。
用戶(hù)(User或者Actor):命令是由對(duì)象執(zhí)行的,這稱(chēng)之為用戶(hù)。用戶(hù)可以是自然人,也可以是系統(tǒng),這里一般指自然人。
規(guī)則(Policy):當(dāng)產(chǎn)生事件或者執(zhí)行命令時(shí),需要進(jìn)行某些業(yè)務(wù)相關(guān)的規(guī)則校驗(yàn)。比如用戶(hù)參加拼團(tuán)活動(dòng),需要校驗(yàn)活動(dòng)是否有效等。
執(zhí)行模型
用戶(hù)執(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)暴過(guò)程中,要負(fù)責(zé)澄清一些業(yè)務(wù)上的概念,思考業(yè)務(wù)上有沒(méi)有遺漏的事件;
項(xiàng)目成員:負(fù)責(zé)開(kāi)發(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)域事件開(kāi)始。領(lǐng)域事件一般用橘色的便利貼表示,書(shū)寫(xiě)領(lǐng)域?qū)嵺`的規(guī)則是使用被動(dòng)語(yǔ)態(tài),并按照時(shí)間順序貼在白紙上。
最開(kāi)始可能很多成員都不知道該怎么寫(xiě),或者不知道該怎么尋找領(lǐng)域事件。可以由組織者寫(xiě)下領(lǐng)域中發(fā)生的第一個(gè)事件。其它參與者會(huì)迅速的開(kāi)始模仿,這時(shí)我們可以讓大家快速的進(jìn)入狀態(tài)。
在遇到有疑惑的事件時(shí),不必長(zhǎng)時(shí)間阻塞在那里討論,把它作為標(biāo)記記下來(lái)即可,后續(xù)再進(jìn)行重點(diǎn)優(yōu)化??梢再N一個(gè)比較醒目的便簽紙(比如紫色)在事件旁邊。
隨著我們對(duì)業(yè)務(wù)認(rèn)識(shí)的不斷加深,可以隨時(shí)回顧和總結(jié)之前添加的內(nèi)容,對(duì)于有問(wèn)題的描述進(jìn)行更正,對(duì)于表述不清楚的內(nèi)容可以進(jìn)行重寫(xiě)。
事件是有相對(duì)順序的??梢园岩幌盗杏邢鄬?duì)順序關(guān)系的事件放在一行上,從左到右排好。這樣有助于梳理領(lǐng)域事件,查看是否有遺漏。
尋找命令和角色
在收集完領(lǐng)域事件后,我們可以在此基礎(chǔ)上進(jìn)一步探索系統(tǒng)核心事件的運(yùn)行機(jī)制。這里我們?cè)谥暗念I(lǐng)域事件的基礎(chǔ)上加入指令和角色的概念。
指令代表系統(tǒng)中用戶(hù)的意圖、動(dòng)作和決定,一般用藍(lán)色的便利貼表示;角色表一類(lèi)特定用戶(hù),一般用黃色便利貼表示。它們之間的關(guān)系是“角色”發(fā)送“指令”產(chǎn)生了“領(lǐng)域事件”(指令也可由外部系統(tǒng)觸發(fā),外部系統(tǒng)通常用粉色的便利貼表示)。
在尋找命令和角色的過(guò)程中,你可能會(huì)遇到某些命令會(huì)在“特定的條件下”觸發(fā)。比如:“當(dāng)用戶(hù)通過(guò)新的設(shè)備登入時(shí),系統(tǒng)會(huì)發(fā)送提醒通知”。通常,我們將這種系統(tǒng)的行為邏輯稱(chēng)為策略,通常記錄在紫丁香色的便利貼上,放在命令旁邊。
尋找領(lǐng)域模型和聚合
當(dāng)我們做完了上一個(gè)環(huán)節(jié),就可以開(kāi)始尋找系統(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è)模型合起來(lái),形成一個(gè)聚合。在最上面的模型就是這個(gè)聚合的聚合根,其之下的模型都是它的實(shí)體或值對(duì)象。
劃分領(lǐng)域和限界上下文
找到領(lǐng)域模型以后,我們應(yīng)當(dāng)就可以比較輕松地劃分子域和限界上下文了。
在劃分限界上下文的時(shí)候也可以反過(guò)來(lái)檢驗(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層的任意上層都可以訪問(wèn)該層(“L”型),而其它層遵守嚴(yán)格分層架構(gòu)。
四層架構(gòu)
六邊形架構(gòu)
Alistair Cockburn在2005年提出,解決了傳統(tǒng)的分層架構(gòu)所帶來(lái)的問(wèn)題,實(shí)際上它也是一種分層架構(gòu),只不過(guò)不是上下或左右,而是變成了內(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ì)》一書(shū)中,作者將六邊形架構(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ì),足見(jiàn)六邊形架構(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)有著相同的思路,它們都通過(guò)編寫(xiě)適配器代碼將應(yīng)用核心從對(duì)基礎(chǔ)設(shè)施的關(guān)注中解放出來(lái),避免基礎(chǔ)設(shè)施代碼滲透到應(yīng)用核心之中。這樣應(yīng)用使用的工具和傳達(dá)機(jī)制都可以輕松地替換,可以一定程度地避免技術(shù)、工具或者供應(yīng)商鎖定。
還有,任何一個(gè)外部層次都可以直接調(diào)用任何一個(gè)內(nèi)部層次,這樣既不會(huì)破壞耦合的方向,也避免了僅僅為了追求分層模式而創(chuàng)建一些沒(méi)有任何業(yè)務(wù)邏輯的代理方法甚至代理類(lèi)。這和 Martin Flowler 表達(dá)的偏好一致。
The Onion Architecture 原文地址
洋蔥架構(gòu)
干凈架構(gòu)
Robert C. Martin在2012年提出了干凈架構(gòu)(Clean Architecture),這是六邊形架構(gòu)的一個(gè)變體,通過(guò)隔離變化和依賴(lài)倒置守護(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)注。他們都通過(guò)軟件分層來(lái)實(shí)現(xiàn)這種分離。至少有一個(gè)層代表業(yè)務(wù)規(guī)則,而另一個(gè)層用于接口。
系統(tǒng)特點(diǎn):
獨(dú)立的框架,這樣的架構(gòu)并不依賴(lài)與應(yīng)用軟件的具體庫(kù)包,這樣可以將框架作為工具,而不必將你的系統(tǒng)都胡亂混合在一起
可測(cè)試,業(yè)務(wù)規(guī)則能夠在沒(méi)有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ù)世界毫無(wú)所知
依賴(lài)倒置原則
依賴(lài)倒置原則(Dependency Inversion Principle, DIP),它通過(guò)改變不同層之間的依賴(lài)關(guān)系達(dá)到改進(jìn)目的。
依賴(lài)倒置原則由Robert C. Martin提出,正式定義為:
高層模塊不應(yīng)該依賴(lài)于底層模塊,兩者都應(yīng)該依賴(lài)于抽象。
抽象不應(yīng)該依賴(lài)于細(xì)節(jié),細(xì)節(jié)應(yīng)該依賴(lài)于抽象。
經(jīng)過(guò)我們多次的嘗試和探討后,最終我們選擇了干凈架構(gòu)作為落地的分層架構(gòu),并且結(jié)合CQRS架構(gòu)。在此基礎(chǔ)上我們提供了一個(gè)Maven腳手架:http://coding.jd.com/com.jd.jr.cf/ddd-archetype/
依賴(lài)倒置
以上是我們工程的模塊的依賴(lài)圖。在原干凈架構(gòu)的基礎(chǔ)上增加了API和Types、Query三個(gè)模塊。API主要是一些接口協(xié)議,Types封裝了領(lǐng)域內(nèi)一些特殊的值對(duì)象Domain?Primitives,而Query是基于CQRS的查詢(xún)端,可以建立獨(dú)立于Domain的查詢(xún)模型。
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)的交互都通過(guò)適配器層。適配器層實(shí)際上又分成兩大類(lèi),主動(dòng)適配和被動(dòng)適配。主動(dòng)適配基本都是應(yīng)用的入口,比如JSF、MQ、調(diào)度器等,被動(dòng)適配基本都是應(yīng)用出口,比如訪問(wèn)RPC接口、數(shù)據(jù)庫(kù)等。
主動(dòng)適配器是系統(tǒng)主動(dòng)適配一些組件
Boot
框架與驅(qū)動(dòng)層,主要包括了SpringBoot的啟動(dòng)類(lèi),AOP、系統(tǒng)配置、Spring容器等。Domain和Application層不應(yīng)該直接依賴(lài)Spring容器,這兩層的一些對(duì)象在Boot層注入Spring容器。
Query
CQRS的查詢(xún)層,可以單獨(dú)定義不同于領(lǐng)域模型的查詢(xún)模型。在CQRS中,查詢(xún)的數(shù)據(jù)庫(kù)模型可以與命令中的數(shù)據(jù)庫(kù)模型不一樣,查詢(xún)的模型是針對(duì)查詢(xún)優(yōu)化的。這一層不是必須的,也可以獨(dú)立部署。Query可以直接引入DO,將DO轉(zhuǎn)換成DTO對(duì)外輸出。
Api
外對(duì)暴露的接口的協(xié)議。
Types
Types模塊是保存無(wú)狀態(tài)的邏輯的Domain?Primitives的地方。
模塊和包說(shuō)明
|--- 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中依賴(lài)的外部的接口實(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)類(lèi)
|--- 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可以通過(guò)工廠來(lái)構(gòu)建
|????? |--- port???????????????? -- 端口,即接口
|????? |--- event??????????????? -- 領(lǐng)域事件
|????? |--- exception??????????? -- 異常封裝
|????? |--- ability????????????? -- 領(lǐng)域能力
|????? |--- extension??????????? -- 擴(kuò)展點(diǎn)
|????? |?????? |--- impl??????? -- 擴(kuò)展點(diǎn)實(shí)現(xiàn)
|--- query?????????????????????? -- 查詢(xún)層,封裝讀服務(wù)
|????? |--- model??????????????? -- 查詢(xún)模型
|????? |--- service????????????? -- 查詢(xún)服務(wù)
|--- types?????????????????????? -- 定義Domain Primitive
在落地中遇到的問(wèn)題
關(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ù)是行為的抽象。從前綴來(lái)看,它們隸屬于不同的層,應(yīng)用服務(wù)屬于應(yīng)用層,領(lǐng)域服務(wù)屬于領(lǐng)域?qū)印?/p>
應(yīng)用服務(wù)
應(yīng)用服務(wù)是用來(lái)表達(dá)用例(Use?Case)和用戶(hù)故事(User Story)的主要手段。應(yīng)用服務(wù)負(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ī)制。
領(lǐng)域服務(wù)
當(dāng)領(lǐng)域中的某個(gè)操作過(guò)程或轉(zhuǎn)換過(guò)程不是實(shí)體或值對(duì)象的職責(zé)時(shí),我們便應(yīng)該將該操作放在一個(gè)單獨(dú)的接口中,即領(lǐng)域服務(wù)。請(qǐng)確保該服務(wù)和通用語(yǔ)言時(shí)是一致的,并且保證它是無(wú)狀態(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)無(wú)業(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í)候是無(wú)法在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來(lái)編排這兩個(gè)Aggregate的Repository來(lái)保證事務(wù)一致性。
跨域事務(wù)
如果出現(xiàn)跨域的,也就是分布式事務(wù),優(yōu)先考慮領(lǐng)域事件的方式,在另外一個(gè)域監(jiān)聽(tīng)領(lǐng)域事件處理。
關(guān)于模型分類(lèi)
開(kāi)始我們并沒(méi)有對(duì)模型進(jìn)行分類(lèi)規(guī)范,發(fā)現(xiàn)落地的過(guò)程中,有不少同事對(duì)這些模型并沒(méi)有清晰的認(rèn)識(shí),造成了模型的濫用,超出了它們的邊界。因此我們強(qiáng)調(diào)了模型的分類(lèi),在這里我們將整個(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主要有三類(lèi),聚合(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)稱(chēng)DO,實(shí)際上就是PO,二者等值的,此處為了和領(lǐng)域?qū)ο髤^(qū)分,使用了PO的定義)
模型分類(lèi)
模型的轉(zhuǎn)換
DTO和DO、PO的轉(zhuǎn)換,可以通過(guò)一些通用工具解決。
如果是字段相同,可以使用CGLIB BeanCopier,Spring帶的BeanUtil實(shí)際上就是使用CGLIB BeanCopier的。
如果有嵌套的字段映射,可以使用MapStruct,非常方便的進(jìn)行類(lèi)型轉(zhuǎn)換。
<dependency>
????????<groupId>org.mapstruct</groupId>
????????<artifactId>mapstruct</artifactId>
????????<version>${org.mapstruct.version}</version>
????</dependency>
關(guān)于實(shí)體和值對(duì)象
在授信域下的準(zhǔn)入域落地的過(guò)程中。通過(guò)對(duì)用戶(hù)準(zhǔn)入的用例的分析,我們將用例中的一些名詞列出來(lái)分析后。將用戶(hù)、業(yè)務(wù)身份、校驗(yàn)結(jié)果、準(zhǔn)入申請(qǐng)記錄列為了領(lǐng)域?qū)ο蟆F渲杏脩?hù)這個(gè)對(duì)象的爭(zhēng)議比較大,因?yàn)橛械耐抡J(rèn)識(shí)用戶(hù)是一個(gè)實(shí)體,因?yàn)樗形ㄒ粯?biāo)識(shí),用戶(hù)PIN。在討論這個(gè)問(wèn)題之前,我們先回顧一下實(shí)體和值對(duì)象的定義。
實(shí)體
實(shí)體(Entity), 主要由標(biāo)識(shí)定義的對(duì)象。它可以是任何事物,只要滿足兩個(gè)條件即可,一是它在整個(gè)生命周期中具有連續(xù)性;二是它的區(qū)別并不是由那些對(duì)用戶(hù)非常重要的屬性決定的。
其實(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è)方面而本身沒(méi)有概念的對(duì)象稱(chēng)為值對(duì)象,值對(duì)象被實(shí)例化之后用來(lái)表示一些設(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ù)面影響
那么用戶(hù)為什么在準(zhǔn)入域中是一個(gè)值對(duì)象而不是一個(gè)實(shí)體呢?用戶(hù)是具有唯一標(biāo)識(shí)Pin的,但是我們無(wú)需對(duì)這個(gè)用戶(hù)的狀態(tài)進(jìn)行維護(hù),也就是在準(zhǔn)入上下文中是不需要維護(hù)用戶(hù)的生命周期的。區(qū)別值對(duì)象的一個(gè)更簡(jiǎn)單的方法是,這個(gè)對(duì)象是不是當(dāng)前這個(gè)業(yè)務(wù)域存儲(chǔ)的,從外部獲取的對(duì)象基本可以確定為值對(duì)象(在共享內(nèi)核的情況下可能會(huì)有不一樣的判定)。在準(zhǔn)入上下文中實(shí)際上我們是只是關(guān)注了用戶(hù)的部分屬性。顯而易見(jiàn)用戶(hù)在用戶(hù)域是一個(gè)實(shí)體。為了更好的區(qū)分實(shí)體和值對(duì)象,這里可以引入一個(gè)概念,最小化集成原則。
最小化集成原則
在 DDD 項(xiàng)目中通常存在多個(gè)限界上下文,意味著我們需要找到合適的方法對(duì)這些上下文進(jìn)行集成,當(dāng)模型概念從上游上下文流入下游上下文中時(shí),盡量使用值對(duì)象來(lái)表示這些概念,這樣的好處是可以達(dá)到最小化集成,既可以最小化下游模型中的屬性數(shù)目,又可以使用不變的值對(duì)象減少職責(zé)假設(shè)。
研發(fā)是喜歡“偷懶”的。所以在落地討論時(shí),一些同事也提出了我們?cè)跍?zhǔn)入域?yàn)槭裁床荒苤苯右糜脩?hù)域的用戶(hù)實(shí)體,這樣就可以不需要再在準(zhǔn)入域建一個(gè)用戶(hù)對(duì)象了。按照最小化集成原則,我們應(yīng)該在準(zhǔn)入域建一個(gè)用戶(hù)值對(duì)象。用戶(hù)域的用戶(hù)模型是很復(fù)雜的,但是在準(zhǔn)入域我們只關(guān)心用戶(hù)極少的屬性,甚至都不關(guān)心用戶(hù)本身具備的行為能力。如果直接引入用戶(hù)域的用戶(hù)實(shí)體,也意味著用戶(hù)域的用戶(hù)實(shí)體的修改時(shí)有可能影響到我們的業(yè)務(wù)。使用值對(duì)象的概念也能將準(zhǔn)入域和用戶(hù)域進(jìn)行很好的隔離,起到防腐層的作用。所以?xún)蓚€(gè)域之間建議通過(guò)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ì)象的不可變
可以這么說(shuō)吧,不可變是值對(duì)象最顯著的特征。
關(guān)于值對(duì)象的不可變,也讓一些同事感到困惑。他們認(rèn)為值對(duì)象還是變的,比如顏色可以改變。其實(shí)顏色可以改變描述并不準(zhǔn)確,得引入語(yǔ)境。比如衣服的顏色可以改變,是衣服這個(gè)實(shí)體的顏色的屬性改變了,并不是顏色這個(gè)值對(duì)象本身變了。這改變衣服顏色這個(gè)用例中,衣服是實(shí)體,顏色是值對(duì)象。有位同事就舉了在商城下單時(shí)的問(wèn)題,他說(shuō)在下單時(shí)地址是可以改變的。這個(gè)問(wèn)題是復(fù)雜的,我們還是從用例開(kāi)始解析,首先要準(zhǔn)確的描述這個(gè)用例,特別是準(zhǔn)確描述其語(yǔ)境(即上下文)。在下單時(shí)修改地址,實(shí)際是由兩個(gè)用例組成的,用戶(hù)在地址管理頁(yè)修改地址,在下單頁(yè)從地址列表中選擇一個(gè)地址。這兩個(gè)用例實(shí)際上是兩個(gè)不同的上下文。用戶(hù)在地址管理頁(yè)修改地址,在這個(gè)語(yǔ)境下,地址就是實(shí)體,所以是修改了常用地址的這個(gè)實(shí)體上的地址(省市區(qū))。在下單頁(yè)用戶(hù)修改地址,實(shí)際上是從常用地址中選擇一個(gè)新的地址,這個(gè)修改地址實(shí)際上是將地址這個(gè)值對(duì)象用另外一個(gè)值對(duì)象替換了,常用地址本身并沒(méi)有修改。說(shuō)到這里,其實(shí)訂單的收貨地址修改和衣服的顏色改變是相似的。
Domain?Primitive
Domain?Primitive是一種特殊的值對(duì)象,用來(lái)描述標(biāo)準(zhǔn)模型,標(biāo)準(zhǔn)模型是用于表示事物類(lèi)型的描述性對(duì)象。
Primitive的定義是:不人任何其他事物發(fā)展而來(lái),初級(jí)的形成或者生長(zhǎng)的早期階段。
這么說(shuō)吧,標(biāo)準(zhǔn)模型就是“放之四海而皆準(zhǔn)”的模型,這個(gè)模型在大部分上下文中都是一致的。比如Money這個(gè)對(duì)象,Money是由面值和貨幣類(lèi)型組成的,在絕大部分語(yǔ)境中我們也只關(guān)心這兩個(gè)屬性(在造幣廠就需要關(guān)心更多的屬性了)。
常見(jiàn)的 DP 的使用場(chǎng)景包括:
有格式限制的 String:比如Name,PhoneNumber,OrderNumber,ZipCode,Address等
有限制的Integer:比如OrderId(>0),Percentage(0-100%),Quantity(>=0)等
可枚舉的 int :比如 Status(一般不用Enum因?yàn)榉葱蛄谢瘑?wè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ù)專(zhuān)家詳解 DDD 系列- Domain Primitive
關(guān)于領(lǐng)域模型的加載性能
?這個(gè)主要是針對(duì)聚合的。比如商戶(hù)聚合,商戶(hù)聚合由商戶(hù)實(shí)體和門(mén)店實(shí)體聚合而成,如下
@Getter
public?class?MerchantAggregate {
????private?MerchantEntity merchantEntity;
????private?List<StoreEntity> storeEntities;
}
每次重建時(shí)都會(huì)一次從數(shù)據(jù)庫(kù)中將商戶(hù)信息和其下的門(mén)店信息。當(dāng)門(mén)店很少的時(shí)候并不存在什么問(wèn)題,但是當(dāng)一個(gè)商戶(hù)有5000家門(mén)店時(shí)。本來(lái)我們只是操作一下商戶(hù)實(shí)體的一些基本信息,但卻將這5000個(gè)門(mén)店實(shí)體加載到了內(nèi)存中。這個(gè)對(duì)性能影響比較大。這個(gè)怎么處理呢。接下來(lái)我們將對(duì)DDD里面的模型做一個(gè)總結(jié),在合適的情況選擇合適的模型。
失血模型
失血模型中,domain object只有屬性的get set方法的純數(shù)據(jù)類(lèi),所有的業(yè)務(wù)邏輯完全由Service層來(lái)完成的。
service:? 腫脹的服務(wù)邏輯
model:只包含get set方法
顯然失血模型service層負(fù)擔(dān)太重,在DDD中一般不會(huì)有這種設(shè)計(jì)。
貧血模型
貧血模型中,domain ojbect包含了不依賴(lài)于持久化的原子領(lǐng)域邏輯,而組合邏輯在Service層。
service :組合服務(wù),也叫事務(wù)服務(wù)
model:除包含get set方法,還包含原子服務(wù)
貧血模型比較常見(jiàn),其問(wèn)題在于原子服務(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ù)持久化的邏輯
充血模型的問(wèn)題也很明顯,當(dāng)model中包含了數(shù)據(jù)持久化的邏輯,實(shí)例化的時(shí)候可能會(huì)有很大麻煩,拿到了太多不一定需要的關(guān)聯(lián)model。
脹血模型
脹血模型取消了Service層,在domain object的domain logic上面封裝事務(wù)。
在這個(gè)商戶(hù)聚合中,我們可以采用充血模型,也就是在商戶(hù)聚合中直接持有門(mén)店的Repositoy,這樣就可以實(shí)現(xiàn)懶加載。只有使用門(mén)店的時(shí)候再通過(guò)門(mén)店Repository獲取門(mén)店,這樣就避免了一次性加載過(guò)多的門(mén)店從而影響性能。
@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ù)門(mén)店ID獲取門(mén)店的方法,這樣在商戶(hù)聚合中就可以使用,盡量避免全部加載門(mén)店。其實(shí)不推薦使用這種模型,這種模型只有在特定的場(chǎng)景才應(yīng)該使用。
關(guān)于模型共享
模型共享的問(wèn)題,其實(shí)上面已經(jīng)提到了部分。為什么單獨(dú)再?gòu)?qiáng)調(diào)一遍呢?因?yàn)楹芏嗳硕紝?duì)這塊提出了疑問(wèn)。為什么不能直接引用用戶(hù)域的用戶(hù)實(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ù)模式等)來(lái)設(shè)置模型的邊界。在這些邊界中嚴(yán)格保持模型的一致性,而不要受到邊界之外問(wèn)題的干擾和混淆。
ANTICORRUPTION?LAYER(防腐層)
以下是完全基于防腐層設(shè)計(jì)的服務(wù)間通訊。上游系統(tǒng)通過(guò)開(kāi)放公共主機(jī)的方式提供服務(wù)(主動(dòng)適配器),下游通過(guò)被動(dòng)適配器來(lái)隔離上游系統(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),我們把賬單域又分成了核心賬單域(主要處理用戶(hù)賬單的資金科目的變動(dòng))、貸款域、還款域、逾期域、退款域,貸款(這里指正向交易)、還款、逾期、退款的上下文中都會(huì)對(duì)用戶(hù)的核心賬單進(jìn)行修改。如果使用ACL的模式,集成起來(lái)也比較費(fèi)勁,并且也會(huì)增加RPC或者分布式事務(wù)的問(wèn)題。這個(gè)時(shí)候就可以把核心賬單域作為一個(gè)共享內(nèi)核提供給其他四個(gè)子域,這個(gè)情況下核心賬單域也不需要單獨(dú)部署一個(gè)應(yīng)用,只需要集成到其他四個(gè)子域中即可。
關(guān)于對(duì)象的創(chuàng)建和管理
在干凈架構(gòu)中,內(nèi)圈是不依賴(lài)Spring框架的。很多同事反饋沒(méi)有Spring怎么管理這些對(duì)象呢,甚至有的人開(kāi)玩笑說(shuō)沒(méi)有了Spring都不會(huì)編程了。所以在此我們對(duì)這個(gè)作了說(shuō)明。
對(duì)于有狀態(tài)的對(duì)象可以通過(guò)new的方式創(chuàng)建,如果這個(gè)對(duì)象是全局唯一的共享的,可以設(shè)置成單例,系統(tǒng)初始化時(shí)創(chuàng)建。
對(duì)于無(wú)狀態(tài)的對(duì)象可以交由Spring管理。Domian和Application層的對(duì)象(比如應(yīng)用服務(wù)和領(lǐng)域服務(wù))同樣可以交由Spring容器管理,因?yàn)檫@兩層不依賴(lài)Spring,可以通過(guò)構(gòu)造方法的方式傳入依賴(lài)的其他的Spring管理的Bean,在最外層的Boot層由Spring創(chuàng)建管理。
關(guān)于構(gòu)造方法的說(shuō)明,由于是通過(guò)構(gòu)造方法的方式進(jìn)行初始化,可能會(huì)帶來(lái)一些壞味道,比如長(zhǎng)參數(shù)列表。但是這個(gè)壞味道是可以接受的,因?yàn)闃?gòu)造方法的方式提供了一個(gè)很好的檢驗(yàn)機(jī)制,避免產(chǎn)生必要參數(shù)沒(méi)有遺漏輸入的問(wèn)題。另外當(dāng)構(gòu)造一個(gè)對(duì)象比較麻煩時(shí),可以引入Factory,通過(guò)Factory構(gòu)建對(duì)象。
如上所述我們希望在應(yīng)用和領(lǐng)域?qū)颖M量不依賴(lài)框架,比如Spring,在最外層的Boot層由Spring創(chuàng)建并管理。這個(gè)時(shí)候就需要在外層寫(xiě)大量的@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)生成上述的樣版代碼,只需要聲明一下該類(lèi)即可,組件是在編譯時(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è)問(wèn)題了,并發(fā)安全問(wèn)題。讀取對(duì)象、處理對(duì)象和保存對(duì)象不是一個(gè)原子操作,
比如扣減額度的處理模型,從數(shù)據(jù)庫(kù)中加載賬戶(hù)實(shí)體,調(diào)用實(shí)體的扣減額度的方法,持久化賬戶(hù)實(shí)體。我們這邊傳統(tǒng)的做法是,使用SQL語(yǔ)句來(lái)操作額度并避免其扣減為負(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)賬戶(hù)實(shí)體中的,也就是在賬戶(hù)實(shí)體中進(jìn)行操作額度。那怎么保證這個(gè)并發(fā)安全呢?在此我們引入了樂(lè)觀鎖的機(jī)制。其實(shí)每次重載對(duì)象的時(shí)候都帶上了版本號(hào),處理完后持久化時(shí),在條件語(yǔ)句中帶個(gè)版本號(hào)。
這個(gè)樂(lè)觀鎖在很多ORM框架中都已經(jīng)支持,使用起來(lái)特別方便,只需要增加一個(gè)@Version的注解即可,比如Spring?Data?Jpa和Mybatis?Plus。其實(shí)JPA很適合DDD,比較推薦使用Spring?Data?Jpa,可以極大的簡(jiǎn)化代碼。
關(guān)于讀寫(xiě)分離
我們的領(lǐng)域?qū)ο缶褪腔跇I(yè)務(wù)進(jìn)行建模的,特別是我們?cè)诮r(shí)也不怎么關(guān)心持久化。
但是作為一個(gè)業(yè)務(wù)系統(tǒng),「查詢(xún)」的相關(guān)功能也是不可或缺的。在實(shí)現(xiàn)各式各樣的查詢(xún)功能時(shí),往往會(huì)發(fā)現(xiàn)很難用領(lǐng)域模型來(lái)實(shí)現(xiàn)。假設(shè)在用戶(hù)需要一個(gè)訂單相關(guān)信息的查詢(xún)功能,展現(xiàn)的是查詢(xún)結(jié)果的列表。列表中的數(shù)據(jù)來(lái)自于「訂單」,「商品」,「品類(lèi)」,「送貨地址」等多個(gè)領(lǐng)域?qū)ο笾械哪硯讉€(gè)字段。這樣的場(chǎng)景如果還是通過(guò)領(lǐng)域?qū)ο髞?lái)封裝就顯的很麻煩,其次與領(lǐng)域知識(shí)也沒(méi)有太緊密的關(guān)系。
此時(shí) CQRS 作為一種模式可以很好的解決以上的問(wèn)題。實(shí)際上在消金我們已經(jīng)在很多場(chǎng)景使用了讀寫(xiě)分離的了,比如大量使用了預(yù)熱,賬戶(hù)預(yù)熱、訂單預(yù)熱。大量的讀服務(wù)都是通過(guò)預(yù)熱服務(wù)。這個(gè)已經(jīng)算是CQRS的雛形了。
CQRS
CQRS — Command Query Responsibility Segregation,故名思義是將 command 與 query 分離的一種模式。
CQRS 將系統(tǒng)中的操作分為兩類(lèi),即「命令」(Command) 與「查詢(xún)」(Query)。命令則是對(duì)會(huì)引起數(shù)據(jù)發(fā)生變化操作的總稱(chēng),即我們常說(shuō)的新增,更新,刪除這些操作,都是命令。而查詢(xún)則和字面意思一樣,即不會(huì)對(duì)數(shù)據(jù)產(chǎn)生變化的操作,只是按照某些條件查找數(shù)據(jù)。
CQRS 的核心思想是將這兩類(lèi)不同的操作進(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ù)源,而查詢(xún)操作在另一個(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