領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)(DDD)以及落地實(shí)踐

前言

領(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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容