推薦好文: 2.5萬字詳解23種設(shè)計模式
微服務(wù)springcloud環(huán)境下基于Netty搭建websocket集群實現(xiàn)服務(wù)器消息推送----netty是yyds
代碼中如何干掉太多的if else即if else的多種替代方案以提高代碼質(zhì)量通過公司代碼審查
1、背景
1.1. 前言
不想看以下抽象概念的讀者們,DDD有關(guān)demo的完整代碼已經(jīng)給大家整理好了,可以關(guān)注【微信公眾號】微信搜索【老板來一杯java】,然后【加群】直接獲取【DDD源碼】,并贈送【DDD領(lǐng)域驅(qū)動設(shè)計實戰(zhàn)落地解惑】PDF一份!在自己項目中引入即用!
小編先問大家一個問題,也算是高級工程師面試中常問的問題,怎么樣才能設(shè)計出一個好的軟件系統(tǒng),或者說一個高質(zhì)量的大型軟件系統(tǒng)應(yīng)該具有哪些特點? DDD也是設(shè)計一個高質(zhì)量軟件系統(tǒng)中的一種解決方案。了解DDD之前,小編建議讀者還是需要具備一定的設(shè)計模式的思想,不太了解設(shè)計模式的可以先參考小編的文章: 2.5萬字詳解23種設(shè)計模式
DDD這個思想呢,最早是Eric Evans(埃里克·埃文斯)在2003年《Domain-Driven Design –Tackling Complexity in the Heart of Software》書中提出的一個概念,該書翻譯過來就是領(lǐng)域驅(qū)動設(shè)計—軟件核心復(fù)雜性應(yīng)對之道,但是提出的時候微服務(wù)當(dāng)時并沒有流行,所以一直沒有火起來,DDD最近開始流行的原因,主要是借著微服務(wù)的東風(fēng)。
1.2. MVC模式 VS DDD模式
MVC三層開發(fā)模式大家應(yīng)該都非常熟悉,現(xiàn)在公司開發(fā)基本都是這種模式。
MVC開發(fā)流程:
- 用戶需求轉(zhuǎn)化為產(chǎn)品需求
- 需求評審會pm講解需求轉(zhuǎn)化為研發(fā)需求
- 研發(fā)人員根據(jù)需求進(jìn)行設(shè)計庫表結(jié)構(gòu)
- 編寫dao層代碼
- 編寫service代碼
- 編寫controller代碼
這一步步下來,是不是感覺非常的絲滑。比如產(chǎn)品提了一個需求,首先我們一般會想考慮設(shè)計幾張表,怎么存儲數(shù)據(jù),然后建立dao層,service層,controller層來實現(xiàn)這個功能。但是嚴(yán)格來講,mvc本質(zhì)上是一種面向數(shù)據(jù)的設(shè)計,主要關(guān)注數(shù)據(jù),自低向上的思想。雖然在開發(fā)速度上有一定優(yōu)勢,如果只追求開發(fā)速度,面向數(shù)據(jù)模型編程在短期之內(nèi)可以搞定需求,但一味追求速度,如果你系統(tǒng)的業(yè)務(wù)變化快速,從長遠(yuǎn)來看隨著時間的增長,系統(tǒng)堆了雜七雜八以后,MVC的短板就會日益明顯。
1.2.1. MVC存在的問題:
1.新需求的開發(fā)會越來越難。 2.代碼維護(hù)越來越難,一個類代碼太多,這怎么看對吧,就是一堆屎山。 3.技術(shù)創(chuàng)新越來越難,代碼沒時間重構(gòu),越拖越爛。 4.測試越來越難,沒辦法單元測試,一個小需求又要回歸測試,太累。
1.2.2. 使用DDD的意義:
單體架構(gòu)局部業(yè)務(wù)膨脹可以拆分成微服務(wù),微服務(wù)架構(gòu)局部業(yè)務(wù)膨脹,又拆成什么呢? DDD就是為了解決這些問題的存在,從一個軟件系統(tǒng)的長期價值來看,就需要用DDD,雖然一開始從設(shè)計到開發(fā)需要成本,但是隨著時間的增長,N年以后代碼依然很整潔,利于擴展和維護(hù),高度自治,高度內(nèi)聚,邊界領(lǐng)域劃分的很清楚。當(dāng)然了,針對于簡單的系統(tǒng)用DDD反而用復(fù)雜了,殺雞焉用宰牛刀! MVC的開發(fā)模式:是數(shù)據(jù)驅(qū)動,自低向上的思想,關(guān)注數(shù)據(jù)。 DDD的開發(fā)模式:是領(lǐng)域驅(qū)動,自頂向下,關(guān)注業(yè)務(wù)活動。
1.3. 總結(jié):
DDD 分層架構(gòu)中的要素其實和三層架構(gòu)類似,只是在 DDD 分層架構(gòu)中,這些要素被重新歸類,重新劃分了層,確定了層與層之間的交互規(guī)則和職責(zé)邊界。 MVC是一個短暫的快樂但不足以支撐漫長的生活,DDD是一個不要短暫的溫存而是一世的陪伴,如果是你來抉擇你會選擇哪一個?
2、DDD領(lǐng)域驅(qū)動模型
2.1. 概念
不想看以下抽象概念的讀者們,DDD有關(guān)demo的完整代碼已經(jīng)給大家整理好了,可以關(guān)注【微信公眾號】微信搜索【老板來一杯java】,然后【加群】直接獲取【DDD源碼】,在自己項目中引入即用!
1. DDD:
DDD(Domain Driven Design)領(lǐng)域驅(qū)動模型,是一種處理高度復(fù)雜領(lǐng)域的設(shè)計思想,不是一種架構(gòu),而是一種架構(gòu)設(shè)計方法論,是一種設(shè)計模式。說白了就是把一個復(fù)雜的軟件應(yīng)用系統(tǒng)的其中各個部分進(jìn)行一個很好的拆解和封裝,以達(dá)到高內(nèi)聚低耦合的這樣一個效果。
說白了就是,DDD就是以高內(nèi)聚低耦合為目的,對軟件系統(tǒng)進(jìn)行模塊化的一種思想。
2. 戰(zhàn)略設(shè)計:
指的是領(lǐng)域名詞、動詞分析、提取領(lǐng)域模型。官方解釋,在某個領(lǐng)域,核心圍繞上下文的設(shè)計,主要關(guān)注上下文的劃分、上下文映射的設(shè)計,通用語言的設(shè)計。
說白了就是,在某個系統(tǒng),核心圍繞子系統(tǒng)的設(shè)計;主要關(guān)注,這些子系統(tǒng)的劃分,子系統(tǒng)的交互方式,還有子系統(tǒng)的核心術(shù)語的定義。
3. 戰(zhàn)術(shù)設(shè)計:
用領(lǐng)域模型指導(dǎo)設(shè)計及編碼的實現(xiàn)。官方解釋,核心關(guān)注上下文中的實體建模,定義值對象,實體等,更偏向開發(fā)細(xì)節(jié)。
說白了就是,上下文對應(yīng)的就是某一個子系統(tǒng),子系統(tǒng)里代碼實現(xiàn)怎么設(shè)計,就是戰(zhàn)術(shù)設(shè)計要解決的問題。核心關(guān)注某個子系統(tǒng)的代碼實現(xiàn),以面向?qū)ο蟮乃季S設(shè)計類的屬性和方法,和設(shè)計類圖沒有什么區(qū)別,只是有一些規(guī)則而已。就是 指導(dǎo)我們劃分類。
4. 問題空間:
問題空間屬于需求分析階段,重點是明確這個系統(tǒng)要解決什么問題,能夠提供什么價值,也就是關(guān)注系統(tǒng)的What與Why。 問題空間將問題域提煉成更多可管理的子域,是真對于問題域而言的。問題空間 在研究和解決業(yè)務(wù)問題時,DDD 會按照一定的規(guī)則將業(yè)務(wù)領(lǐng)域進(jìn)行細(xì)分,當(dāng)領(lǐng)域細(xì)分到一定的程度后,DDD 會將問題范圍限定在特定的邊界內(nèi),在這個邊界內(nèi)建立領(lǐng)域模型,進(jìn)而用代碼實現(xiàn)該領(lǐng)域模型,解決相應(yīng)的業(yè)務(wù)問題。簡言之,DDD 的領(lǐng)域就是這個邊界內(nèi)要解決的業(yè)務(wù)問題域。
領(lǐng)域可以進(jìn)一步劃分為子領(lǐng)域。我們把劃分出來的多個子領(lǐng)域稱為子域,每個子域?qū)?yīng)一個更小的問題域或更小的業(yè)務(wù)范圍,每個子域又包含了核心子域,支撐子域,通用子域。
領(lǐng)域的核心思想就是將問題域逐級細(xì)分,來降低業(yè)務(wù)理解和系統(tǒng)實現(xiàn)的復(fù)雜度。通過領(lǐng)域細(xì)分,逐步縮小服務(wù)需要解決的問題域,構(gòu)建合適的領(lǐng)域模型。
說白了就是,就是系統(tǒng)下面有多個子系統(tǒng),就是分了一些類型,比如電商系統(tǒng),訂單就是核心領(lǐng)域,支付調(diào)用銀行,支付寶什么的就是支撐子域,相當(dāng)于我們俗稱的下游,通用子域,就是一些鑒權(quán),用戶中心,每個系統(tǒng)都會用到,就設(shè)計成通用子域,關(guān)鍵就是討論過程如何得出這些問題域,是戰(zhàn)略設(shè)計要解決的。
5. 解決空間:
解決方案域?qū)儆谙到y(tǒng)設(shè)計階段,針對識別出來的問題域,尋求合理的解決方案,也就是關(guān)注系統(tǒng)的How。在領(lǐng)域驅(qū)動設(shè)計中,核心領(lǐng)域(Core Domain)與子領(lǐng)域(Sub Domain)屬于問題域的范疇,限界上下文(Bounded Context)則屬于解決方案域的范疇。 說白了就是,得出這些問題域之后,就基于這些問題域來求解,屬于解決空間。相當(dāng)于,知道了y=2x,知道了x是多少,然后求y的值。解決空間就是指,領(lǐng)域之間的關(guān)系是什么樣子,每個領(lǐng)域中通用的術(shù)語 ,具體在領(lǐng)域內(nèi)怎么實現(xiàn)代碼,進(jìn)行領(lǐng)域建模就可以了。 從問題域到解決方案域,實際上就是從需求分析到設(shè)計的過程,也是我們逐步識別限界上下文的過程。
6. 事件風(fēng)暴:
事件風(fēng)暴的基本思想,就是將軟件開發(fā)人員和領(lǐng)域?qū)<揖奂谝黄?,完成領(lǐng)域模型設(shè)計(領(lǐng)域分析和領(lǐng)域建模)。劃分出微服務(wù)邏輯邊界和物理邊界,定義領(lǐng)域模型中的領(lǐng)域?qū)ο螅笇?dǎo)微服務(wù)設(shè)計和開發(fā)。
領(lǐng)域分析,是根據(jù)需求劃分出初步的領(lǐng)域和限界上下文,以及上下文之間的關(guān)系;然后分析每個上下文內(nèi)部,抽取每個子域的領(lǐng)域概念,識別出哪些是實體,哪些是值對象;
領(lǐng)域建模,就是對實體、值對象進(jìn)行關(guān)聯(lián)和聚合,劃分出聚合的范疇和聚合根;
DDD需要進(jìn)行領(lǐng)域分析和領(lǐng)域建模,除了事件風(fēng)暴之外實現(xiàn)的方法有,領(lǐng)域故事講述,四色建模法,用例法等。
事件風(fēng)暴是建立領(lǐng)域模型的主要方法,但是在 DDD 領(lǐng)域建模和系統(tǒng)建設(shè)過程中,有很多的參與者,包括領(lǐng)域?qū)<?、產(chǎn)品經(jīng)理、項目經(jīng)理、架構(gòu)師、開發(fā)經(jīng)理和測試經(jīng)理等。對同樣的領(lǐng)域知識,不同的參與角色可能會有不同的理解,那大家交流起來就會有障礙,怎么辦呢?因此,在 DDD 中就出現(xiàn)了“通用語言”和“限界上下文”這兩個重要的概念。
7. 通用語言:
DDD的主要參與者:領(lǐng)域?qū)<?開發(fā)人員。領(lǐng)域?qū)<疑瞄L某個領(lǐng)域的知識,專注于交付的業(yè)務(wù)價值。而開發(fā)人員則注重于技術(shù)實現(xiàn),總是想著類、接口、方法、設(shè)計模式、架構(gòu)等。這也就導(dǎo)致了團(tuán)隊交流的困難性。因此找到雙方的通用語言是解決該問題的有效途徑。
通用語言定義上下文含義。在事件風(fēng)暴過程中,通過團(tuán)隊交流達(dá)成共識的,能夠簡單、清晰、準(zhǔn)確描述業(yè)務(wù)涵義和規(guī)則的語言就是通用語言。 通用語言包含術(shù)語和用例場景,并且能夠直接反映在代碼中。通用語言中的名詞可以給領(lǐng)域?qū)ο竺缟唐?、訂單等,對?yīng)實體對象;而動詞則表示一個動作或事件,如商品已下單、訂單已付款等,對應(yīng)領(lǐng)域事件或者命令。
通用語言說白了就是,使用團(tuán)隊(業(yè)務(wù)人員、產(chǎn)品經(jīng)理、UI設(shè)計、前端設(shè)計、后端技術(shù)等)中大家都懂的概念,統(tǒng)一語言用可以用于需求文檔、PRD文檔、代碼以及日常的溝通,可以解決交流障礙的問題,使領(lǐng)域?qū)<液烷_發(fā)人員能夠協(xié)同合作,從而能夠確保業(yè)務(wù)需求的正確表達(dá)。
8. 限界上下文:
官方解釋:限界上下文主要用來封裝通用語言和領(lǐng)域?qū)ο蟆?br> 限界上下文可以拆分為兩個詞,限界和上下文。
限界:適用的對象一般是抽象事物,指不同事物的分界,指定某些事物的范圍。
上下文:個人理解就是語境。語言都有它的語義環(huán)境,同樣,通用語言也有它的上下文環(huán)境。為了避免同樣的概念或語義在不同的上下文環(huán)境中產(chǎn)生歧義,DDD 在戰(zhàn)略設(shè)計上提出了“限界上下文”這個概念,用來確定語義所在的領(lǐng)域邊界。限界上下文就是用來定義領(lǐng)域邊界,以確保每個上下文含義在它特定的邊界內(nèi)都具有唯一的含義,領(lǐng)域模型則存在于這個邊界之內(nèi)。這個邊界定義了模型的適用范圍,使團(tuán)隊所有成員能夠明確地知道什么應(yīng)該在模型中實現(xiàn),什么不應(yīng)該在模型中實現(xiàn)。 ? 比如說,商品在不同的階段有不同的術(shù)語,在銷售階段是商品,而在運輸階段則變成了貨物。同樣的一個東西,由于業(yè)務(wù)領(lǐng)域的不同,賦予了這些術(shù)語不同的涵義和職責(zé)邊界,這個邊界就可能會成為未來微服務(wù)設(shè)計的邊界。那么,領(lǐng)域邊界就是通過限界上下文來定義的。
限界上下文是微服務(wù)設(shè)計和拆分的主要依據(jù)。在領(lǐng)域模型中,如果不考慮技術(shù)異構(gòu)、團(tuán)隊溝通等其它外部因素,一個限界上下文理論上就可以設(shè)計為一個微服務(wù)。
限界上下文是業(yè)務(wù)概念的邊界,是業(yè)務(wù)問題最小粒度的劃分。在某個業(yè)務(wù)領(lǐng)域中會包含多個限界上下文,我們通過找出這些確定的限界上下文對系統(tǒng)進(jìn)行解耦,要求每一個限界上下文其內(nèi)部必須是緊密組織的、職責(zé)明確的、具有較高的內(nèi)聚性。說白了就是,上下文對應(yīng)的就是某一個子系統(tǒng),系統(tǒng)之間要劃分好邊界。
9. 上下文映射:
上下文之間交互方式就是上下文映射,相對于系統(tǒng)里面這就是RPC,http等交互方式。
10. 領(lǐng)域:
從廣義上講,領(lǐng)域具體指一種特定的范圍或區(qū)域。在DDD中上下文的劃分完的東西叫作領(lǐng)域,領(lǐng)域下面又劃分了,核心領(lǐng)域,支撐子域,通用子域。 子域:在領(lǐng)域不斷劃分的過程中,領(lǐng)域會細(xì)分為不同的子域,子域可以根據(jù)自身重要性和功能屬性劃分為三類子域,它們分別是:核心域、通用域和支撐域。
核心域:它是業(yè)務(wù)成功的主要因素和公司的核心競爭力 通用域:沒有太多個性化的訴求,同時被多個子域使用的通用功能子域是通用域 支撐域:有一種功能子域是必需的,但既不包含決定產(chǎn)品和公司核心競爭力的功能,也不包含通用功能的子域,就是支撐域。
說白了就是,系統(tǒng)下面有多個子系統(tǒng),就是分了一些類型,比如電商系統(tǒng),訂單就是核心領(lǐng)域,支付調(diào)用銀行,支付寶什么的就是支撐子域,相當(dāng)于我們俗稱的下游,通用子域,就是一些鑒權(quán),用戶中心,每個系統(tǒng)都會用到,就設(shè)計成通用子域,關(guān)鍵就是討論過程如何得出這些域,是戰(zhàn)略設(shè)計要解決的。
11. 領(lǐng)域模型:
領(lǐng)域模型是對領(lǐng)域內(nèi)的概念類或現(xiàn)實世界中對象的可視化表示。它專注于分析問題領(lǐng)域本身,發(fā)掘重要的業(yè)務(wù)領(lǐng)域概念,并建立業(yè)務(wù)領(lǐng)域概念之間的關(guān)系。 是描述業(yè)務(wù)用例實現(xiàn)的對象模型。它是對業(yè)務(wù)角色和業(yè)務(wù)實體之間應(yīng)該如何聯(lián)系和協(xié)作以執(zhí)行業(yè)務(wù)的一種抽象。 領(lǐng)域模型分為領(lǐng)域?qū)ο蠛皖I(lǐng)域服務(wù)兩大類,領(lǐng)域?qū)ο笥糜诖鎯顟B(tài),領(lǐng)域服務(wù)用于改變領(lǐng)域?qū)ο蟮臓顟B(tài)。
特點:
- 領(lǐng)域模型是對具有某個邊界的領(lǐng)域的一個抽象,反映了領(lǐng)域內(nèi)用戶業(yè)務(wù)需求的本質(zhì);領(lǐng)域模型是有邊界的,只反應(yīng)了我們在領(lǐng)域內(nèi)所關(guān)注的部分;
- 領(lǐng)域模型只反映業(yè)務(wù),和任何技術(shù)實現(xiàn)無關(guān);領(lǐng)域模型不僅能反映領(lǐng)域中的一些實體概念,如貨物,書本,應(yīng)聘記錄,地址,等;還能反映領(lǐng)域中的一些過程概念,如資金轉(zhuǎn)賬,等;
- 領(lǐng)域模型確保了我們的軟件的業(yè)務(wù)邏輯都在一個模型中,都在一個地方;這樣對提高軟件的可維護(hù)性,業(yè)務(wù)可理解性以及可重用性方面都有很好的幫助;
- 領(lǐng)域模型能夠幫助開發(fā)人員相對平滑地將領(lǐng)域知識轉(zhuǎn)化為軟件構(gòu)造;
- 領(lǐng)域模型貫穿軟件分析、設(shè)計,以及開發(fā)的整個過程;領(lǐng)域?qū)<?、設(shè)計人員、開發(fā)人員通過領(lǐng)域模型進(jìn)行交流,彼此共享知識與信息;因為大家面向的都是同一個模型,所以可以防止需求走樣,可以讓軟件設(shè)計開發(fā)人員做出來的軟件真正滿足需求;
- 要建立正確的領(lǐng)域模型并不簡單,需要領(lǐng)域?qū)<摇⒃O(shè)計、開發(fā)人員積極溝通共同努力,然后才能使大家對領(lǐng)域的認(rèn)識不斷深入,從而不斷細(xì)化和完善領(lǐng)域模型;
- 為了讓領(lǐng)域模型看的見,我們需要用一些方法來表示它;圖是表達(dá)領(lǐng)域模型最常用的方式,但不是唯一的表達(dá)方式,代碼或文字描述也能表達(dá)領(lǐng)域模型;
- 領(lǐng)域模型是整個軟件的核心,是軟件中最有價值和最具競爭力的部分;設(shè)計足夠精良且符合業(yè)務(wù)需求的領(lǐng)域模型能夠更快速的響應(yīng)需求變化;
12. 領(lǐng)域事件:
聚合之間產(chǎn)生的業(yè)務(wù)協(xié)同使用領(lǐng)域事件的方式來完成,領(lǐng)域事件就是將上游聚合處理完成這個動作通過事件的方式進(jìn)行抽象。
在DDD中有一個原則,一個業(yè)務(wù)用例對應(yīng)一個事務(wù),一個事務(wù)對應(yīng)一個聚合根,也就是在一次事務(wù)中只能對一個聚合根操作。但在實際應(yīng)用中,一個業(yè)務(wù)用例往往需要修改多個聚合根,而不同的聚合根可能在不同的限界上下文中,引入領(lǐng)域事件即不破壞DDD的一個事務(wù)只修改一個聚合根的原則,也能實現(xiàn)限界上下文之間的解耦。對于領(lǐng)域事件發(fā)布,在領(lǐng)域服務(wù)發(fā)布,在不使用領(lǐng)域服務(wù)的情況下,則由應(yīng)用層在調(diào)用資源庫持久化聚合根之后再發(fā)布領(lǐng)域事件。
一個事件可能當(dāng)前限界上下文內(nèi)也需要消費,即可能有多個限界上下文需要消費,一個事件對應(yīng)多個消費者。
一個完整的領(lǐng)域事件 = 事件發(fā)布 + 事件存儲 + 事件分發(fā) + 事件處理。 事件發(fā)布:構(gòu)建一個事件,需要唯一標(biāo)識,然后發(fā)布; 事件存儲:發(fā)布事件前需要存儲,因為接收后的事建也會存儲,可用于重試或?qū)~等;就是每次執(zhí)行一次具體的操作時,把行為記錄下來,執(zhí)行持久化。 事件分發(fā):服務(wù)內(nèi)的應(yīng)用服務(wù)或者領(lǐng)域服務(wù)直接發(fā)布給訂閱者,服務(wù)外需要借助消息中間件,比如Kafka,RabbitMQ等,支持同步或者異步。 事件處理:先將事件存儲,然后再處理。 當(dāng)然了,實際開發(fā)中事件存儲和事件處理不是必須的。
因此實現(xiàn)方案:發(fā)布訂閱模式,分為跨上下文(kafka,RocketMq)和上下文內(nèi)(spring事件,Guava Event Bus)的領(lǐng)域事件。
e.g. 用戶注冊后,發(fā)送短信和郵件,使用spring事件實現(xiàn)領(lǐng)域事件代碼如下:
1.創(chuàng)建用戶注冊事件
/**
* 用戶注冊事件
* @Author WDYin
**/
public class UserRegisterEvent extends ApplicationEvent {
public UserRegisterEvent(Object source) {
super(source);
}
}
2.用戶監(jiān)聽事件
/**
* 用戶監(jiān)聽事件
* @Author WDYin
**/
@Component
public class UserListener {
@EventListener(UserRegisterEvent.class)
public void userRegister(UserRegisterEvent event) {
User user = (User) event.getSource();
System.out.println("用戶注冊。。。發(fā)送短信。。。" + user);
System.out.println("用戶注冊。。。發(fā)送郵件。。。" + user);
}
@EventListener(UserCancelEvent.class)
public void userCancelEvent(UserCancelEvent event) {
User user = (User) event.getSource();
System.out.println("用戶注銷。。。" + user);
}
}
3.發(fā)布用戶注冊事件
/**
* 發(fā)布用戶注冊事件
* @Author : WDYin
*/
@RunWith(value = SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = DemoApplication.class)
public class MyClient {
@Autowired
private ApplicationContext applicationContext;
@Test
public void test() {
User user = new User();
//發(fā)布事件
applicationContext.publishEvent(new UserRegisterEvent(user));
}
}
13. 實體:
官方解釋,實體和值對象會形成聚合,每個聚合一般是在一個事務(wù)中操作,一般都有持久性操作。聚合中,跟實體的生命周期決定了聚合整體的生命周期。說白了,就是對象之間的關(guān)聯(lián),只是規(guī)定了關(guān)聯(lián)對象規(guī)則(必須是由實體和值對象組成的),操作聚合時類似hibernate的One-Many對象的操作,一起操作,不能單獨操作。
e.g. 權(quán)限管理系統(tǒng)——用戶實體,代碼如下:
@NoArgsConstructor
@Getter
public class User extends Aggregate<Long, User> {
/**
* 用戶id-聚合根唯一標(biāo)識
*/
private UserId userId;
/**
* 用戶名
*/
private String userName;
/**
* 姓名
*/
private String realName;
/**
* 手機號
*/
private String phone;
/**
* 密碼
*/
private String password;
/**
* 鎖定結(jié)束時間
*/
private Date lockEndTime;
/**
* 登錄失敗次數(shù)
*/
private Integer failNumber;
/**
* 用戶角色
*/
private List<Role> roles;
/**
* 部門
*/
private Department department;
/**
* 領(lǐng)導(dǎo)
*/
private User leader;
/**
* 下屬
*/
private List<User> subordinationList = new ArrayList<>();
/**
* 用戶狀態(tài)
*/
private UserStatus userStatus;
/**
* 用戶地址
*/
private Address address;
public User(String userName, String phone, String password) {
saveUserName(userName);
savePhone(phone);
savePassword(password);
}
/**
* 保存用戶名
* @param userName
*/
private void saveUserName(String userName) {
if (StringUtils.isBlank(userName)){
Assert.throwException("用戶名不能為空!");
}
this.userName = userName;
}
/**
* 保存電話
* @param phone
*/
private void savePhone(String phone) {
if (StringUtils.isBlank(phone)){
Assert.throwException("電話不能為空!");
}
this.phone = phone;
}
/**
* 保存密碼
* @param password
*/
private void savePassword(String password) {
if (StringUtils.isBlank(password)){
Assert.throwException("密碼不能為空!");
}
this.password = password;
}
/**
* 保存用戶地址
* @param province
* @param city
* @param region
*/
public void saveAddress(String province,String city,String region){
this.address = new Address(province,city,region);
}
/**
* 保存用戶角色
* @param roleList
*/
public void saveRole(List<Role> roleList) {
if (CollectionUtils.isEmpty(roles)){
Assert.throwException("角色不能為空!");
}
this.roles = roleList;
}
/**
* 保存領(lǐng)導(dǎo)
* @param leader
*/
public void saveLeader(User leader) {
if (Objects.isNull(leader)){
Assert.throwException("leader不能為空!");
}
this.leader = leader;
}
/**
* 增加下屬
* @param user
*/
public void increaseSubordination(User user) {
if (null == user){
Assert.throwException("leader不能為空!");
}
this.subordinationList.add(user);
}
}
###14. 值對象: 官方解釋,描述了領(lǐng)域中的一件東西,將不同的相關(guān)屬性組合成了一個概念整體,當(dāng)度量和描述改變時,可以用另外一個值對象予以替換,屬性判等,固定不變。 說白了就是,不關(guān)心唯一值,具有校驗邏輯,等值判斷邏輯,只關(guān)心值的類。只有數(shù)據(jù)初始化操作和有限的不涉及修改數(shù)據(jù)的行為,基本不包含業(yè)務(wù)邏輯。比如下單的地址。
當(dāng)你決定一個領(lǐng)域概念是否是一個值對象時,需考慮它是否擁有以下特征:
- 度量或者描述了領(lǐng)域中的一件東西
- 可作為不變量
- 將不同的相關(guān)的屬性組合成一個概念整體(Conceptual Whole)
- 當(dāng)度量和描述改變時,可以用另一個值對象予以替換
- 可以和其他值對象進(jìn)行相等性比較
- 不會對協(xié)作對象造成副作用
- 當(dāng)你只關(guān)心某個對象的屬性時,該對象便可作為一個值對象。為其添加有意義的屬性,并賦予它相應(yīng)的行為。需要將值對象看成不變對象,不要給它任何身份標(biāo)識, 還應(yīng)盡量避免像實體對象一樣的復(fù)雜性。
值對象本質(zhì)上就是一個集。該集合有若干用于描述目的、具有整體概念和不可修改的屬性。該集合存在的意義是在領(lǐng)域建模的過程中,值對象可保證屬性歸類的清晰和概念的完整性,避免屬性零碎。
代碼如下:
/**
* 地址數(shù)據(jù)
*
* @Author WDYin
* @Date 2022/5/24
*/
@Getter
public class Address extends ValueObject {
/**
* 省
*/
private String province;
/**
* 市
*/
private String city;
/**
* 區(qū)
*/
private String region;
public Address(String province, String city, String region) {
if (StringUtils.isBlank(province)){
Assert.throwException("province不能為空!");
}
if (StringUtils.isBlank(city)){
Assert.throwException("city不能為空!");
}
if (StringUtils.isBlank(region)){
Assert.throwException("region不能為空!");
}
this.province = province;
this.city = city;
this.region = region;
}
}
14. 聚合:
官方解釋,實體和值對象會形成聚合,每個聚合一般是在一個事務(wù)中操作,一般都有持久性操作。聚合中,跟實體的生命周期決定了聚合整體的生命周期。說白了,就是對象之間的關(guān)聯(lián),只是規(guī)定了關(guān)聯(lián)對象規(guī)則(必須是由實體和值對象組成的),操作聚合時類似hibernate的One-Many對象的操作,一起操作,不能單獨操作。
聚合的規(guī)范:
- 我們把一些關(guān)聯(lián)性極強、生命周期一致的實體、值對象放到一個聚合里。
- 聚合是領(lǐng)域?qū)ο蟮娘@式分組,旨在支持領(lǐng)域模型的行為和不變性,同時充當(dāng)一致性和事務(wù)性邊界。
- 聚合在 DDD 分層架構(gòu)里屬于領(lǐng)域?qū)樱I(lǐng)域?qū)影硕鄠€聚合,共同實現(xiàn)核心業(yè)務(wù)邏輯??缍鄠€實體的業(yè)務(wù)邏輯通過領(lǐng)域服務(wù)來實現(xiàn),跨多個聚合的業(yè)務(wù)邏輯通過應(yīng)用服務(wù)來實現(xiàn)。
- 比如有的業(yè)務(wù)場景需要同一個聚合的 A 和 B 兩個實體來共同完成,我們就可以將這段業(yè)務(wù)邏輯用領(lǐng)域服務(wù)來實現(xiàn);而有的業(yè)務(wù)邏輯需要聚合 C 和聚合 D 中的兩個服務(wù)共同完成,這時你就可以用應(yīng)用服務(wù)來組合這兩個服務(wù)。
在DDD中,聚合也可以用來表示整體與部分的關(guān)系,但不再強調(diào)部分與整體的獨立性。聚合是將相關(guān)聯(lián)的領(lǐng)域?qū)ο筮M(jìn)行顯示分組,來表達(dá)整體的概念(也可以是單一的領(lǐng)域?qū)ο螅?。比如將表示訂單與訂單項的領(lǐng)域?qū)ο筮M(jìn)行組合,來表達(dá)領(lǐng)域中訂單這個整體概念。
15. 聚合根:
聚合根(Aggreate Root, AR)就是軟件模型中那些最重要的以名詞形式存在的領(lǐng)域?qū)ο?。聚合根是主要的業(yè)務(wù)邏輯載體,DDD中所有的戰(zhàn)術(shù)實現(xiàn)都圍繞著聚合根展開。70%的場景下,一個聚合內(nèi)都只有一個實體,那就是聚合根。 說白了就是:聚合的根實體,最具代表性的實體。比如訂單和訂單項聚合之后的聚合根就是訂單。
聚合根的特征:
- 它作為實體本身,擁有實體的屬性和業(yè)務(wù)行為,實現(xiàn)自身的業(yè)務(wù)邏輯。
- 它作為聚合的管理者,在聚合內(nèi)部負(fù)責(zé)協(xié)調(diào)實體和值對象按照固定的業(yè)務(wù)規(guī)則協(xié)同完成共同的業(yè)務(wù)邏輯。
- 聚合根之間的引用通過ID完成。在聚合之間,它還是聚合對外的接口人,以聚合根 ID 關(guān)聯(lián)的方式接受外部任務(wù)和請求,在上下文內(nèi)實現(xiàn)聚合之間的業(yè)務(wù)協(xié)同。也就是說,聚合之間通過聚合根 ID 關(guān)聯(lián)引用,如果需要訪問其它聚合的實體,就要先訪問聚合根,再導(dǎo)航到聚合內(nèi)部實體,外部對象不能直接訪問聚合內(nèi)實體。
簡單概括一下:
通過事件風(fēng)暴(我理解就是頭腦風(fēng)暴,不過我們一般都是先通過個人理解,然后再和相關(guān)核心同學(xué)進(jìn)行溝通),得到實體和值對象; 將這些實體和值對象聚合為“投保聚合”和“客戶聚合”,其中“投保單”和“客戶”是兩者的聚合根; 找出與聚合根“投保單”和“客戶”關(guān)聯(lián)的所有緊密依賴的實體和值對象; 在聚合內(nèi)根據(jù)聚合根、實體和值對象的依賴關(guān)系,畫出對象的引用和依賴模型。
16. 貧血模型:
貧血模型具有一堆屬性和set get方法,存在的問題就是通過pojo這個對象上看不出業(yè)務(wù)有哪些邏輯,一個pojo可能被多個模塊調(diào)用,只能去上層各種各樣的service來調(diào)用,這樣以后當(dāng)梳理這個實體有什么業(yè)務(wù),只能一層一層去搜service,也就是貧血失憶癥,不夠面向?qū)ο蟆?/p>
代碼如下:
public class User {
private Long id;
private String userName;//用戶名
private String password;//密碼
private String gesture; //手勢密碼
private String phone; //手機號碼
private String email;
private int status; //賬戶狀態(tài)
private Date lockEndTime; //鎖定結(jié)束時間
private int failNumber; //登錄失敗次數(shù)
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getGesture() {
return gesture;
}
public void setGesture(String gesture) {
this.gesture = gesture;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public Date getLockEndTime() {
return lockEndTime;
}
public void setLockEndTime(Date lockEndTime) {
this.lockEndTime = lockEndTime;
}
public int getFailNumber() {
return failNumber;
}
public void setFailNumber(int failNumber) {
this.failNumber = failNumber;
}
}
17. 充血模型:
比如如下user用戶有改密碼,改手機號,修改登錄失敗次數(shù)等操作,都內(nèi)聚在這個user實體中,每個實體的業(yè)務(wù)都是清晰的,就是充血模型,充血模型的內(nèi)存計算會多一些,內(nèi)聚核心業(yè)務(wù)邏輯處理。
說白了就是,不只是有貧血模型中setter getter方法,還有其他的一些業(yè)務(wù)方法,這才是面向?qū)ο蟮谋举|(zhì),通過user實體就能看出有哪些業(yè)務(wù)存在。
代碼如下:
@NoArgsConstructor
@Getter
public class User extends Aggregate<Long, User> {
/**
* 用戶名
*/
private String userName;
/**
* 姓名
*/
private String realName;
/**
* 手機號
*/
private String phone;
/**
* 密碼
*/
private String password;
/**
* 鎖定結(jié)束時間
*/
private Date lockEndTime;
/**
* 登錄失敗次數(shù)
*/
private Integer failNumber;
/**
* 用戶角色
*/
private List<Role> roles;
/**
* 部門
*/
private Department department;
/**
* 用戶狀態(tài)
*/
private UserStatus userStatus;
/**
* 用戶地址
*/
private Address address;
public User(String userName, String phone, String password) {
saveUserName(userName);
savePhone(phone);
savePassword(password);
}
/**
* 保存用戶名
* @param userName
*/
private void saveUserName(String userName) {
if (StringUtils.isBlank(userName)){
Assert.throwException("用戶名不能為空!");
}
this.userName = userName;
}
/**
* 保存電話
* @param phone
*/
private void savePhone(String phone) {
if (StringUtils.isBlank(phone)){
Assert.throwException("電話不能為空!");
}
this.phone = phone;
}
/**
* 保存密碼
* @param password
*/
private void savePassword(String password) {
if (StringUtils.isBlank(password)){
Assert.throwException("密碼不能為空!");
}
this.password = password;
}
/**
* 保存用戶地址
* @param province
* @param city
* @param region
*/
public void saveAddress(String province,String city,String region){
this.address = new Address(province,city,region);
}
/**
* 保存用戶角色
* @param roleList
*/
public void saveRole(List<Role> roleList) {
if (CollectionUtils.isEmpty(roles)){
Assert.throwException("角色不能為空!");
}
this.roles = roleList;
}
}
18. 領(lǐng)域服務(wù):
聚合根與領(lǐng)域服務(wù)負(fù)責(zé)封裝實現(xiàn)業(yè)務(wù)邏輯。領(lǐng)域服務(wù)負(fù)責(zé)對聚合根進(jìn)行調(diào)度和封裝,同時可以對外提供各種形式的服務(wù),對于不能直接通過聚合根完成的業(yè)務(wù)操作就需要通過領(lǐng)域服務(wù)。
說白了就是,聚合根本身無法完全處理這個邏輯,例如支付這個步驟,訂單聚合不可能支付,所以在訂單聚合上架一層領(lǐng)域服務(wù),在領(lǐng)域服務(wù)中實現(xiàn)支付邏輯,然后應(yīng)用服務(wù)調(diào)用領(lǐng)域服務(wù)。
在以下幾種情況時,我們可以使用領(lǐng)域服務(wù):
- 對于不能直接通過聚合根完成的業(yè)務(wù)操作就需要通過領(lǐng)域服務(wù)。
- 在DDD中,每個實體只能操作自己實體的變化,不能改另一個實體的狀態(tài)??鐚嶓w的狀態(tài)變化需要抽象出一個領(lǐng)域服務(wù),不能直接修改實體的狀態(tài),只能調(diào)用實體的業(yè)務(wù)方法。
- 以多個領(lǐng)域?qū)ο笞鳛檩斎雲(yún)?shù)進(jìn)行計算,結(jié)果產(chǎn)生一個值對象。
- 執(zhí)行一個顯著的業(yè)務(wù)操作
- 對領(lǐng)域?qū)ο筮M(jìn)行轉(zhuǎn)換
遵守以下規(guī)范:
- 同限界上下文內(nèi)的聚合之間的領(lǐng)域服務(wù)可直接調(diào)用
- 兩個限界上下文的交互必須通過應(yīng)用服務(wù)層抽離接口->適配層適配。
e.g. 用戶升職,上級領(lǐng)導(dǎo)要變,上級領(lǐng)導(dǎo)的下屬要變
代碼如下:
/**
* @Author WDYin
* @Date 2022/5/15
**/
@Service
public class UserDomainServiceImpl implements UserDomainService {
@Override
public void promote(User user, User leader) {
//保存領(lǐng)導(dǎo)
user.saveLeader(leader);
//領(lǐng)導(dǎo)增加下屬
leader.increaseSubordination(user);
}
}
19. 應(yīng)用服務(wù):
應(yīng)用服務(wù)作為總體協(xié)調(diào)者,先通過資源庫獲取到聚合根,然后調(diào)用聚合根或者領(lǐng)域服務(wù)中的業(yè)務(wù)方法,最后再次調(diào)用資源庫保存聚合根。
作用:
- 除了同步方法調(diào)用外,還可以發(fā)布或者訂閱領(lǐng)域事件,權(quán)限校驗、事務(wù)控制,一個事務(wù)對應(yīng)一個聚合根。
- 應(yīng)用層方法主要執(zhí)行服務(wù)編排等輕量級邏輯,尤其針對跨多個領(lǐng)域的業(yè)務(wù)場景,效果明顯。
- 參數(shù)校驗,簡單的crud,可直接調(diào)用倉庫接口
/**
* @Author WDYin
**/
@Service
public class UserApplicationServiceImpl implements UserApplicationService {
@Resource
private DomainEventPublisher domainEventPublisher;
@Resource
private UserRepository userRepository;
@Resource
private UserAssembler userAssembler;
@Resource
private UserDomainService userDomainService;
/**
* 用戶注冊
* @param userAddCommand 注冊信息
*/
@Override
public void register(UserAddCommand userAddCommand) {
//業(yè)務(wù)檢查
userDomainService.check(userAddCommand);
//組裝user領(lǐng)域模型
User user = userAssembler.commandToDo(userAddCommand);
//落庫
userRepository.add(user);
//發(fā)送用戶注冊事件
domainEventPublisher.publish(new UserRegisterEvent(user, UserEventTypeEnum.REGISTER));
}
/**
* 用戶修改
* @param userUpdateCommand 修改信息
*/
@Override
public void update(UserUpdateCommand userUpdateCommand) {
User user = userRepository.getById(userUpdateCommand.getId());
//發(fā)送用戶注冊事件
domainEventPublisher.publish(new UserRegisterEvent(user, UserEventTypeEnum.CREATED));
}
/**
* 用戶升職
* @param userId 用戶id
* @param leaderId 領(lǐng)導(dǎo)的id
*/
@Override
public void promote(UserId userId, UserId leaderId) {
//獲取用戶領(lǐng)域模型
User user = userRepository.getById(userId.getUserId());
//獲取用戶的新領(lǐng)導(dǎo)
User leader = userRepository.getById(leaderId.getUserId());
//領(lǐng)域服務(wù)升職方法
userDomainService.promote(user,leader);
}
}
20. 倉庫:
負(fù)責(zé)提供聚合根或者持久化聚合根。倉庫幫助我們持久化整個聚合的,存一個對象會把相關(guān)對象都存下來。從技術(shù)上講,Repository和DAO所扮演的角色相似,不過DAO的設(shè)計初衷只是對數(shù)據(jù)庫的一層很薄的封裝,而Repository是更偏向于領(lǐng)域模型。另外,在所有的領(lǐng)域?qū)ο笾?,只有聚合根才“配得上”擁有Repository,而DAO沒有這種約束。
代碼如下:
/**
* @Author WDYin
* @Date 2022/5/15
**/
@Repository
public class UserRepositoryImpl implements UserRepository {
@Resource
private UserPersistence userPersistence;
@Resource
private UserConverter userConverter;
@Override
public void add(User user) {
UserPO userPo = userConverter.doToPo(user);
userPersistence.insert(userPo);
user.setId(userPo.getId());
}
@Override
public User getById(Long id) {
UserPO userPO = userPersistence.selectByPrimaryKey(id);
return userConverter.poToDo(userPO);
}
}
21. 工廠:
比如說創(chuàng)建一個實體,里面有五個值對象組成,每次創(chuàng)建的時候都得new一次,這里用工廠簡化,工廠幫助我們創(chuàng)建聚合。這一方面可以享受到工廠模式本身的好處,另一方面,DDD中的Factory還具有將“聚合根的創(chuàng)建邏輯”顯現(xiàn)出來的效果。Factory有兩種實現(xiàn)方式: 1)直接在聚合根中實現(xiàn)Factory方法,常用于簡單的創(chuàng)建過程 2)獨立的Factory類,用于有一定復(fù)雜度的創(chuàng)建過程,或者創(chuàng)建邏輯不適合放在聚合根上 工廠也可以使用converter來代替 代碼如下: 屬性的拷貝使用BeanUtils或者mapstruct都可以。
@Component
public class UserConverter {
public User poToDo(UserPO userPO) {
User user = new User();
BeanUtils.copyProperties(userPO,user);
return user;
}
public UserPO doToPo(User user) {
UserPo userPO = new UserPo();
BeanUtils.copyProperties(userPO,user);
return userPO;
}
}
22. 防腐層:
當(dāng)某個功能模塊需要依賴第三方系統(tǒng)提供的數(shù)據(jù)或者功能時,我們常用的策略就是直接使用外部系統(tǒng)的API、數(shù)據(jù)結(jié)構(gòu)。這樣存在的問題就是,因使用外部系統(tǒng),而被外部系統(tǒng)的質(zhì)量問題影響,從而“腐化”本身設(shè)計的問題。
因此我們的解決方案就是在兩個系統(tǒng)之間加入一個中間層,隔離第三方系統(tǒng)的依賴,對第三方系統(tǒng)進(jìn)行通訊轉(zhuǎn)換和語義隔離,這個中間層,我們叫它防腐層。
說白了就是,兩個系統(tǒng)之間加了中間層,中間層類似適配器模式,解決接口差異的對接,接口轉(zhuǎn)換是單向的(即從調(diào)用方向被調(diào)用方進(jìn)行接口轉(zhuǎn)換);防腐層強調(diào)兩個子系統(tǒng)語義解耦,接口轉(zhuǎn)換是雙向的。
防腐層作用:
- 使兩方的系統(tǒng)解耦,隔離雙方變更的影響,允許雙方獨立演進(jìn);
- 防腐層允許其它的外部系統(tǒng)能夠在不改變現(xiàn)有系統(tǒng)的領(lǐng)域?qū)拥那疤嵯?,與該系統(tǒng)實現(xiàn)無縫集成,從而降低系統(tǒng)集成的開發(fā)工作量。
2.4. 落地方式
DDD分為戰(zhàn)略設(shè)計和戰(zhàn)術(shù)設(shè)計。
2.4.1. 戰(zhàn)略設(shè)計:
參與人員:業(yè)務(wù)專家(領(lǐng)域?qū)<遥?,產(chǎn)品經(jīng)理,技術(shù)專家(研發(fā)人員) 戰(zhàn)略設(shè)計更偏向于軟件架構(gòu)的層面,官方解釋,在某個領(lǐng)域,核心圍繞上下文的設(shè)計。講求的是領(lǐng)域和限界上下文(Bounded Context,BC)的劃分,以及各個限界上下文之間的上下游關(guān)系,還有通用語言的設(shè)計。也就是從業(yè)務(wù)視角出發(fā),歸好類,把邊界劃分好,明確界限上下文,可以用事件風(fēng)暴來做。會得到通用語言,上下文,上下文之間的交互關(guān)系,邊界,不同的域。 說白了就是,在某個系統(tǒng),核心圍繞子系統(tǒng)的設(shè)計;主要關(guān)注,這些子系統(tǒng)的劃分,子系統(tǒng)的交互方式,還有子系統(tǒng)的核心術(shù)語的定義。當(dāng)前如此火熱的“在微服務(wù)中使用DDD”這個命題,究其最初的邏輯無外乎是“DDD中的限界上下文可以用于指導(dǎo)微服務(wù)中的服務(wù)劃分”,
事實上,限界上下文依然是軟件模塊化的一種體現(xiàn)。
三步走: 第一步:需求分析,根據(jù)需求劃分出初步的領(lǐng)域和限界上下文,以及上下文之間的關(guān)系; 第二步:領(lǐng)域分析,進(jìn)一步分析每個上下文內(nèi)部,抽取每個子域的領(lǐng)域概念,識別出哪些是實體,哪些是值對象; 第三步:領(lǐng)域建模,對實體、值對象進(jìn)行關(guān)聯(lián)和聚合,劃分出聚合的范疇和聚合根;
2.4.2. 戰(zhàn)術(shù)設(shè)計:
參與人員:技術(shù)專家(研發(fā)人員) 戰(zhàn)術(shù)設(shè)計便更偏向于編碼實現(xiàn),官方解釋,核心關(guān)注上下文中的實體建模,定義值對象,實體等,更偏向開發(fā)細(xì)節(jié)。用領(lǐng)域模型指導(dǎo)設(shè)計及編碼的實現(xiàn),以技術(shù)為主導(dǎo)。 說白了就是,上下文對應(yīng)的就是某一個子系統(tǒng),子系統(tǒng)里代碼實現(xiàn)怎么設(shè)計,就是戰(zhàn)術(shù)設(shè)計要解決的問題。核心關(guān)注某個子系統(tǒng)的代碼實現(xiàn),以面向?qū)ο蟮乃季S設(shè)計類的屬性和方法,和設(shè)計類圖沒有什么區(qū)別,只是有一些規(guī)則而已,就是指導(dǎo)我們劃分類。DDD戰(zhàn)術(shù)設(shè)計的目的是使得業(yè)務(wù)能夠從技術(shù)中分離并突顯出來,讓代碼直接表達(dá)業(yè)務(wù)的本身。 其中包含了實體,聚合,聚合根,值對象,聚合之間的關(guān)系,倉庫,工廠,防腐層,充血模型,領(lǐng)域服務(wù),領(lǐng)域事件等概念。 戰(zhàn)術(shù)層面可以說DDD是一種放大的設(shè)計模式。
三步走: 第一步:編寫核心業(yè)務(wù)邏輯,由領(lǐng)域模型驅(qū)動軟件設(shè)計,通過代碼來表現(xiàn)該領(lǐng)域模型,在實體和領(lǐng)域服務(wù)中實現(xiàn)核心業(yè)務(wù)邏輯; 第二步:為聚合根設(shè)計倉儲,并思考實體或值對象的創(chuàng)建方式; 第三步:在工程中實踐領(lǐng)域模型,并在實踐中檢驗?zāi)P偷暮侠硇?,倒推模型中不足的地方并重?gòu)。
3、DDD架構(gòu)
分層架構(gòu)的一個重要原則是每層只能與位于其下方的層發(fā)生耦合,較低層絕不能直接訪問較高層。分層架構(gòu)可以簡單分為兩種: 嚴(yán)格分層架構(gòu): 某層只能與位于其直接下方的層發(fā)生耦合 松散分層架構(gòu): 則允許某層與它的任意下方層發(fā)生耦合 我們在實際運用過程中多使用的是松散分層架構(gòu)。
3.1. 傳統(tǒng)四層架構(gòu):
將領(lǐng)域模型和業(yè)務(wù)邏輯分離出來,并減少對基礎(chǔ)設(shè)施、用戶界面甚至應(yīng)用層邏輯的依賴,因為它們不屬業(yè)務(wù)邏輯。將一個夏雜的系統(tǒng)分為不同的層,每層都應(yīng)該具有良好的內(nèi)聚性,并且只依賴于比其自身更低的層。
傳統(tǒng)分層架構(gòu)的 基礎(chǔ)設(shè)施層 位于底層,持久化和消息機制便位于該層。可將基礎(chǔ)設(shè)施層中所有組件看作應(yīng)用程序的低層服務(wù),較高層與該層發(fā)生耦合以復(fù)用技術(shù)基礎(chǔ)設(shè)施。即便如此,依然應(yīng)避免核心的領(lǐng)域模型對象與基礎(chǔ)設(shè)施層直接耦合。
3.2. 改良版四層架構(gòu)
傳統(tǒng)架構(gòu)的缺陷:就是將基礎(chǔ)設(shè)施層放在最底層存在缺點,比如此時領(lǐng)域?qū)又械囊恍┘夹g(shù)實現(xiàn)令人頭疼:違背分層架構(gòu)的基本原則,難以編寫測試用例等。
因此通過Java設(shè)計六大原則中的依賴倒置原則實現(xiàn)各層對基礎(chǔ)資源的解耦:也就是低層服務(wù)(如基礎(chǔ)設(shè)施層)應(yīng)依賴高層組件(比如用戶界面層、應(yīng)用層和領(lǐng)域?qū)樱┧峁┙涌?。高層定義好倉庫的接口,基礎(chǔ)設(shè)施層實現(xiàn)各層定義好的倉庫接口。 依賴倒置原則:具體依賴于抽象,而不是抽象依賴于具體。
(1)用戶接口層:
①一般包括用戶接口、Web 服務(wù)、rpc請求,mq消息等外部輸入均被視為外部輸入的請求。對外暴露API,具體形式不限于RPC、Rest API、消息等。 ②一般都很薄,提供必要的參數(shù)校驗和異常捕獲流程。 ③一般會提供VO或者DTO到Entity或者ValueObject的轉(zhuǎn)換,用于前后端調(diào)用的適配,當(dāng)然dto可以直接使用command和query,視情況而定。 ④用戶接口層很重要,在于前后端調(diào)用的適配。若你的微服務(wù)要面向很多應(yīng)用或渠道提供服務(wù),而每個渠道的入?yún)⒊鰠⒍疾灰粯樱悴惶赡荛_發(fā)出太多應(yīng)用服務(wù),這樣Facade接口就起很好的作用了,包括DO和DTO對象的組裝和轉(zhuǎn)換等。
(2)應(yīng)用層:
①應(yīng)用層方法提供用例級別的能力透出,不處理業(yè)務(wù)邏輯,而只是調(diào)用領(lǐng)域?qū)?,對領(lǐng)域服務(wù)/聚合根方法調(diào)用的封裝,負(fù)責(zé)領(lǐng)域的組合、編排、轉(zhuǎn)發(fā)、轉(zhuǎn)換和傳遞。 ②應(yīng)用服務(wù)作為總體協(xié)調(diào)者,先通過資源庫獲取到聚合根,然后調(diào)用聚合根中的業(yè)務(wù)方法,最后再次調(diào)用資源庫保存聚合根。 ③除了同步方法調(diào)用外,還可以發(fā)布或者訂閱領(lǐng)域事件,權(quán)限校驗、事務(wù)控制,一個事務(wù)對應(yīng)一個聚合根。 ④應(yīng)用層方法主要執(zhí)行服務(wù)編排等輕量級邏輯,尤其針對跨多個領(lǐng)域的業(yè)務(wù)場景,效果明顯。 ⑤參數(shù)校驗,簡單的crud,可直接調(diào)用倉庫接口 ⑥跨上下文(kafka,RocketMq)和上下文內(nèi)(spring事件,Guava Event Bus)的領(lǐng)域事件 ⑦倉儲層接口
(3)領(lǐng)域?qū)樱?/h4>
①包含了業(yè)務(wù)核心的領(lǐng)域模型:實體(聚合根+值對象),使用充血模型實現(xiàn)所有與之相關(guān)的業(yè)務(wù)功能,主要表達(dá)業(yè)務(wù)概念,業(yè)務(wù)狀態(tài)信息以及業(yè)務(wù)規(guī)則。 ②真正的業(yè)務(wù)邏輯都在領(lǐng)域?qū)泳帉懀酆细?fù)責(zé)封裝實現(xiàn)業(yè)務(wù)邏輯,對應(yīng)用層暴露領(lǐng)域級別的服務(wù)接口。 ③聚合根不能直接操作其它聚合根,聚合根與聚合根之間只能通過聚合根ID引用;同限界上下文內(nèi)的聚合之間的領(lǐng)域服務(wù)可直接調(diào)用;兩個限界上下文的交互必須通過應(yīng)用服務(wù)層抽離接口->適配層適配。 ④跨實體的狀態(tài)變化,使用領(lǐng)域服務(wù),領(lǐng)域服務(wù)不能直接修改實體的狀態(tài),只能調(diào)用實體的業(yè)務(wù)方法 ⑤在所有的領(lǐng)域?qū)ο笾校挥芯酆细艙碛蠷epository,因為Repository不同于DAO,它所扮演的角色只是向領(lǐng)域模型提供聚合根。 ⑥防腐層接口 ⑦倉儲層接口
(4)基礎(chǔ)設(shè)施層:
①為業(yè)務(wù)邏輯提供支撐能力,提供通用的技術(shù)能力,倉庫寫增刪改查類似DAO。 ② 防腐層實現(xiàn)(封裝變化)用于業(yè)務(wù)檢查和隔離第三方服務(wù),內(nèi)部try catch ③ 聚合根工廠負(fù)責(zé)創(chuàng)建聚合根,但并非必須的,可以將聚合根的創(chuàng)建寫到聚合根下并改為靜態(tài)方法。工廠組組裝復(fù)雜對象,可能會調(diào)用第三方服務(wù),倉庫集成工廠Facotry/build應(yīng)對復(fù)雜對象的封裝,也可以使用converter。 ④ 多于技術(shù)有關(guān),如:DB交互的接口、Cache相關(guān)、MQ、工具類等 ⑤抽象系統(tǒng)內(nèi)第三方組件的交互,為上層提供技術(shù)層面的支持,與業(yè)務(wù)細(xì)節(jié)無關(guān)。
3.3. 整潔架構(gòu)(洋蔥架構(gòu))
在整潔架構(gòu)里,同心圓代表應(yīng)用軟件的不同部分,從里到外依次是領(lǐng)域模型、領(lǐng)域服務(wù)、應(yīng)用服務(wù)和最外圍的容易變化的內(nèi)容,比如用戶界面和基礎(chǔ)設(shè)施。 整潔架構(gòu)最主要的原則是依賴原則,它定義了各層的依賴關(guān)系,越往里依賴越低,代碼級別越高,越是核心能力。外圓代碼依賴只能指向內(nèi)圓,內(nèi)圓不需要知道外圓的任何情況。 在洋蔥架構(gòu)中,各層的職能劃分:
領(lǐng)域模型實現(xiàn)領(lǐng)域內(nèi)核心業(yè)務(wù)邏輯,它封裝了企業(yè)級的業(yè)務(wù)規(guī)則。領(lǐng)域模型的主體是實體,一個實體可以是一個帶方法的對象,也可以是一個數(shù)據(jù)結(jié)構(gòu)和方法集合。 領(lǐng)域服務(wù)實現(xiàn)涉及多個實體的復(fù)雜業(yè)務(wù)邏輯。應(yīng)用服務(wù)實現(xiàn)與用戶操作相關(guān)的服務(wù)組合與編排,它包含了應(yīng)用特有的業(yè)務(wù)流程規(guī)則,封裝和實現(xiàn)了系統(tǒng)所有用例。 最外層主要提供適配的能力,適配能力分為主動適配和被動適配。主動適配主要實現(xiàn)外部用戶、網(wǎng)頁、批處理和自動化測試等對內(nèi)層業(yè)務(wù)邏輯訪問適配。被動適配主要是實現(xiàn)核心業(yè)務(wù)邏輯對基礎(chǔ)資源訪問的適配,比如數(shù)據(jù)庫、緩存、文件系統(tǒng)和消息中間件等。 紅圈內(nèi)的領(lǐng)域模型、領(lǐng)域服務(wù)和應(yīng)用服務(wù)一起組成軟件核心業(yè)務(wù)能力。
3.4. CQRS架構(gòu)(命令查詢隔離架構(gòu))
CQRS — Command Query Responsibility Segregation,故名思義是讀寫分離,就是將 command 與 query 分離的一種模式。 Command :命令則是對會引起數(shù)據(jù)發(fā)生變化操作的總稱,即我們常說的新增,更新,刪除這些操作,都是命令。 Query:查詢則和字面意思一樣,即不會對數(shù)據(jù)產(chǎn)生變化的操作,只是按照某些條件查找數(shù)據(jù)。 Command 與 Query 對應(yīng)的數(shù)據(jù)源可以公用一種數(shù)據(jù)源,也可以是互相獨立的,即更新操作在一個數(shù)據(jù)源,而查詢操作在另一個數(shù)據(jù)源上。
CQRS三種模式 (1)共享模型/共享存儲:讀寫公用一種領(lǐng)域模型,讀寫模型公用一種。 (2)分離模型/共享存儲:讀寫分別用不同的領(lǐng)域模型,讀操作使用讀領(lǐng)域模型,寫操作使用寫領(lǐng)域模型。 (3)分離模式/分離存儲:也叫做事件源 (Event source) CQRS,使用領(lǐng)域事件保證讀寫數(shù)據(jù)的一致性。也就是當(dāng) command 系統(tǒng)完成數(shù)據(jù)更新的操作后,會通過領(lǐng)域事件的方式通知 query 系統(tǒng)。query 系統(tǒng)在接受到事件之后更新自己的數(shù)據(jù)源。 CQRS(讀寫操作分別使用不同的數(shù)據(jù)庫)
軟件中的讀模型和寫模型是很不一樣的,我們通常所講的業(yè)務(wù)邏輯更多的時候是在寫操作過程中需要關(guān)注的東西,而讀操作更多關(guān)注的是如何向客戶方返回恰當(dāng)?shù)臄?shù)據(jù)展現(xiàn)。
因此在DDD的寫操作中,我們需要嚴(yán)格地按照“應(yīng)用服務(wù) -> 聚合根 -> 資源庫”的結(jié)構(gòu)進(jìn)行編碼,而在讀操作中,采用與寫操作相同的結(jié)構(gòu)有時不但得不到好處,反而使整個過程變得冗繁,還多了模型轉(zhuǎn)換,影響效率。本來讀操作就需要速度快,性能高。 因此本文CQRS實戰(zhàn)中的讀操作是基于數(shù)據(jù)模型,應(yīng)用層提供一個單獨的用于讀的倉庫,然后繞過聚合根和資源庫,也就是繞過領(lǐng)域?qū)樱趹?yīng)用層直接返回數(shù)據(jù)。而寫操作是基于領(lǐng)域模型,通過應(yīng)用服務(wù)->聚合根/領(lǐng)域服務(wù)->資源庫的代碼結(jié)構(gòu)進(jìn)行編碼。
3.5. 六邊形架構(gòu)(端口適配器架構(gòu))
六邊形架構(gòu)的核心理念是:應(yīng)用是通過端口與外部進(jìn)行交互的
下圖的紅圈內(nèi)的核心業(yè)務(wù)邏輯(應(yīng)用程序和領(lǐng)域模型)與外部資源(包括 APP、Web 應(yīng)用以及數(shù)據(jù)庫資源等)完全隔離,僅通過適配器進(jìn)行交互。它解決了業(yè)務(wù)邏輯與用戶界面的代碼交錯問題,很好地實現(xiàn)了前后端分離。六邊形架構(gòu)各層的依賴關(guān)系與整潔架構(gòu)一樣,都是由外向內(nèi)依賴。
六邊形架構(gòu)將系統(tǒng)分為內(nèi)六邊形和外六邊形兩層,這兩層的職能劃分如下:紅圈內(nèi)的六邊形實現(xiàn)應(yīng)用的核心業(yè)務(wù)邏輯;外六邊形完成外部應(yīng)用、驅(qū)動和基礎(chǔ)資源等的交互和訪問,對前端應(yīng)用以 API 主動適配的方式提供服務(wù),對基礎(chǔ)資源以依賴倒置被動適配的方式實現(xiàn)資源訪問。六邊形架構(gòu)的一個端口可能對應(yīng)多個外部系統(tǒng),不同的外部系統(tǒng)也可能會使用不同的適配器,由適配器負(fù)責(zé)協(xié)議轉(zhuǎn)換。這就使得應(yīng)用程序能夠以一致的方式被用戶、程序、自動化測試和批處理腳本使用。
3.6. 總結(jié)
這三種架構(gòu)模型的設(shè)計思想微服務(wù)架構(gòu)高內(nèi)聚低耦合原則的完美體現(xiàn),而它們身上閃耀的正是以領(lǐng)域模型為中心的設(shè)計思想,將核心業(yè)務(wù)邏輯與外部應(yīng)用、基礎(chǔ)資源進(jìn)行隔離。
紅色框內(nèi)部主要實現(xiàn)核心業(yè)務(wù)邏輯,但核心業(yè)務(wù)邏輯也是有差異的,有的業(yè)務(wù)邏輯屬于領(lǐng)域模型的能力,有的則屬于面向用戶的用例和流程編排能力。按照這種功能的差異,我們在這三種架構(gòu)中劃分了應(yīng)用層和領(lǐng)域?qū)樱瑏沓袚?dān)不同的業(yè)務(wù)邏輯。 領(lǐng)域?qū)訉崿F(xiàn)面向領(lǐng)域模型,實現(xiàn)領(lǐng)域模型的核心業(yè)務(wù)邏輯,屬于原子模型,它需要保持領(lǐng)域模型和業(yè)務(wù)邏輯的穩(wěn)定,對外提供穩(wěn)定的細(xì)粒度的領(lǐng)域服務(wù),所以它處于架構(gòu)的核心位置。 應(yīng)用層實現(xiàn)面向用戶操作相關(guān)的用例和流程,對外提供粗粒度的 API 服務(wù)。它就像一個齒輪一樣進(jìn)行前臺應(yīng)用和領(lǐng)域?qū)拥倪m配,接收前臺需求,隨時做出響應(yīng)和調(diào)整,盡量避免將前臺需求傳導(dǎo)到領(lǐng)域?qū)印?yīng)用層作為配速齒輪則位于前臺應(yīng)用和領(lǐng)域?qū)又g。
4、 CQRS實戰(zhàn)
4.1. 概念
CQRS(Command Query Responsibility Segregation)是將Command(命令)與Query(查詢)分離的一種模式。其基本思想在于:任何一個方法都可以拆分為命令和查詢兩部分:
Command:不返回任何結(jié)果(void),但會改變對象的狀態(tài)。Command是引起數(shù)據(jù)變化操作的總稱,一般會執(zhí)行某個動作,如:新增,更新,刪除等操作。操作都封裝在Command中,用戶提交Commond到CommandBus,然后分發(fā)到對應(yīng)的CommandHandler中執(zhí)行。Command執(zhí)行后通過Repository將數(shù)據(jù)持久化。事件源(Event source)CQRS,Command將特定的Event發(fā)送到EventBus,然后由特定的EventHandler處理。
Query:返回查詢結(jié)果,不會對數(shù)據(jù)產(chǎn)生變化的操作,只是按照某些條件查找數(shù)據(jù)?;赒uery條件,返回查詢結(jié)果;為不同的場景定制不同的Facade。
4.2. 架構(gòu)圖
基于四層的CQRS架構(gòu)圖:
4.3. 代碼布局
第一種是
- 用戶界面層調(diào)用應(yīng)用服務(wù)
- 應(yīng)用服務(wù)調(diào)用領(lǐng)域服務(wù)
- 在領(lǐng)域服務(wù)中
①通過倉庫獲取聚合根 ②通過資源庫持久化聚合根 ③調(diào)用聚合根的業(yè)務(wù)方法 ④發(fā)布領(lǐng)域事件
第二種是:
- 用戶界面層調(diào)用應(yīng)用服務(wù)
- 應(yīng)用服務(wù)
①通過資源庫獲取聚合根 ②調(diào)用聚合根的業(yè)務(wù)方法或者領(lǐng)域服務(wù)的方法 ③通過資源庫持久化聚合根 ④發(fā)布領(lǐng)域事件
4.4. 數(shù)據(jù)模型轉(zhuǎn)換
每一層都有自己特定的數(shù)據(jù),可以做如下區(qū)分:
- VO(View Object):視圖對象,主要對應(yīng)界面顯示的數(shù)據(jù)對象。對于一個WEB頁面,小程序,微信公眾號等前端需要的數(shù)據(jù)對象。也有團(tuán)隊用VO表示領(lǐng)域?qū)又械腣alue Object值對象,這個要根據(jù)團(tuán)隊的規(guī)范來定義。
- DTO(Data Transfer Object):數(shù)據(jù)傳輸對象,主要用于遠(yuǎn)程調(diào)用之間傳輸?shù)膶ο蟮牡胤?。比如我們一張表?100 個字段,那么對應(yīng)的 PO 就有 100 個屬性。但是客戶端只需要 10 個字段,沒有必要把整個 PO 對象傳遞到客戶端,這時我們就可以用只有這 10 個屬性的 DTO 來傳遞結(jié)果到客戶端,這樣也不會暴露服務(wù)端表結(jié)構(gòu)。到達(dá)客戶端以后,如果用這個對象來對應(yīng)界面顯示,那此時它的身份就轉(zhuǎn)為 VO。DTO泛指用于展示層與服務(wù)層之間的數(shù)據(jù)傳輸對象,當(dāng)然VO也相當(dāng)于數(shù)據(jù)DTO的一種。
- DO(Domain Object):領(lǐng)域?qū)ο螅褪菑默F(xiàn)實世界中抽象出來的有形或無形的業(yè)務(wù)實體,使用的是充血模型設(shè)計的對象。也有團(tuán)隊使用用 BO(Business Objects)表示業(yè)務(wù)對象的概念。
- PO(Persistent Object):持久化對象,它跟持久層(通常是關(guān)系型數(shù)據(jù)庫)的數(shù)據(jù)結(jié)構(gòu)形成一一對應(yīng)的映射關(guān)系,如果持久層是關(guān)系型數(shù)據(jù)庫,那么,數(shù)據(jù)表中的每個字段(或若干個)就對應(yīng) PO 的一個(或若干個)屬性。最形象的理解就是一個 PO 就是數(shù)據(jù)庫中的一條記錄,好處是可以把一條記錄作為一個對象處理,可以方便的轉(zhuǎn)為其它對象。也有團(tuán)隊使用DO(Data Object)表示數(shù)據(jù)對象
- POJO(Plain Ordinary Java Object):簡單對象,是只具有setter getter方法對象的統(tǒng)稱。但是不要把對象名命名成 xxxPOJO!
模型轉(zhuǎn)換架構(gòu)圖:
(1)從應(yīng)用層->基礎(chǔ)設(shè)施層的過程:
(2)從基礎(chǔ)設(shè)施層->應(yīng)用層的過程:
4.5. demo目錄結(jié)構(gòu)以及源碼
DDD有關(guān)demo的完整代碼已經(jīng)給大家整理好了,可以關(guān)注【微信公眾號】微信搜索【老板來一杯java】,然后【加群】直接獲取【DDD源碼】,并贈送【DDD領(lǐng)域驅(qū)動設(shè)計實戰(zhàn)落地解惑】PDF一份!在自己項目中引入即用!
代碼目錄結(jié)構(gòu)如下:
│
│ ├─interface 用戶接口層
│ │ └─controller 控制器,對外提供(Restful)接口
│ │ └─facade 外觀模式,對外提供本地接口和dubbo接口
│ │ └─mq mq消息,消費者消費外部mq消息
│ │
│ ├─application 應(yīng)用層
│ │ ├─assembler 裝配器
│ │ ├─dto 數(shù)據(jù)傳輸對象,xxxCommand/xxxQuery/xxxVo
│ │ │ ├─command 接受增刪改的參數(shù)
│ │ │ ├─query 接受查詢的參數(shù)
│ │ │ ├─vo 返回給前端的vo對象
│ │ ├─service 應(yīng)用服務(wù),負(fù)責(zé)領(lǐng)域的組合、編排、轉(zhuǎn)發(fā)、轉(zhuǎn)換和傳遞
│ │ ├─repository 查詢數(shù)據(jù)的倉庫接口
│ │ ├─listener 事件監(jiān)聽定義
│ │
│ ├─domain 領(lǐng)域?qū)?│ │ ├─entity 領(lǐng)域?qū)嶓w
│ │ ├─valueobject 領(lǐng)域值對象
│ │ ├─service 領(lǐng)域服務(wù)
│ │ ├─repository 倉庫接口,增刪改的接口
│ │ ├─acl 防腐層接口
│ │ ├─event 領(lǐng)域事件
│ │
│ ├─infrastructure 基礎(chǔ)設(shè)施層
│ │ ├─converter 實體轉(zhuǎn)換器
│ │ ├─repository 倉庫
│ │ │ ├─impl 倉庫實現(xiàn)
│ │ │ ├─mapper mybatis mapper接口
│ │ │ ├─po 數(shù)據(jù)庫orm數(shù)據(jù)對象
│ │ ├─ack 實體轉(zhuǎn)換器
│ │ ├─mq mq消息
│ │ ├─cache 緩存
│ │ ├─util 工具類
│ │
│
5、總結(jié)
- MVC是一個短暫的快樂但不足以支撐漫長的生活,而DDD是一個不要短暫的溫存而是一世的陪伴,如果是你來抉擇你會選擇哪一個?
- MVC的開發(fā)模式:是數(shù)據(jù)驅(qū)動,自低向上的思想,關(guān)注數(shù)據(jù)。DDD的開發(fā)模式:是領(lǐng)域驅(qū)動,自頂向下,關(guān)注業(yè)務(wù)活動。為了應(yīng)對業(yè)務(wù)快速變化的軟件系統(tǒng),DDD是面向?qū)ο蟮淖罱K體現(xiàn),大家一起用起來吧!
- DDD是一套方法論,一千個讀者一千個哈姆雷特。
- 下一期出DDD改造MVC架構(gòu)設(shè)計的代碼攻略。
- DDD改造MVC架構(gòu)設(shè)計的代碼,添加微信群即可獲取完整demo!
如果看到這里,說明你喜歡這篇文章,請轉(zhuǎn)發(fā),點贊。關(guān)注【微信公眾號】微信搜索【老板來一杯java】 回復(fù)【進(jìn)群】即可獲取【DDD源碼】,并贈送【DDD領(lǐng)域驅(qū)動設(shè)計實戰(zhàn)落地解惑】PDF一份! 回復(fù)【java】即可獲取【java基礎(chǔ)經(jīng)典面試】一份!
推薦好文: 使用高并發(fā)利器redis—解決淘寶/微博的【熱門搜索】和【最近搜索】的功能 SpringCloud核心組件概述(五大神獸) IntelliJ idea搭建微服務(wù)spring cloud框架(一) MySQL查詢語句執(zhí)行順序以及各關(guān)鍵字的詳解
|