DDD 必備架構(gòu)--六邊形架構(gòu)
架構(gòu)是研究“分”和“合”的藝術(shù),通過(guò)“分離關(guān)注點(diǎn)”將系統(tǒng)拆分為多個(gè)部分,然后在“原則和規(guī)則”的約束下對(duì)組件進(jìn)行裝配,形成高內(nèi)聚的構(gòu)件;再根據(jù)需求對(duì)多個(gè)構(gòu)件進(jìn)行關(guān)聯(lián),形成低耦合的連接,最終構(gòu)建“高內(nèi)聚低耦合”的軟件系統(tǒng)。
圖片
為了有效應(yīng)對(duì)軟件復(fù)雜性,通常會(huì)對(duì)其進(jìn)行分類,然后對(duì)癥下藥逐個(gè)擊破。
1. 軟件系統(tǒng)復(fù)雜性
面對(duì)一個(gè)軟件需求,我們經(jīng)常會(huì)將其分為兩類:
- 功能性需求。就是產(chǎn)品提出的眾多業(yè)務(wù)功能,例如:用戶登錄、查詢數(shù)據(jù)、添加訂單等;
- 非功能性需求。指系統(tǒng)在實(shí)現(xiàn)功能時(shí)必須滿足的技術(shù)指標(biāo),最常見(jiàn)的包括性能、可靠性、安全性、可維護(hù)性、易用性等,例如:系統(tǒng)的響應(yīng)時(shí)間、并發(fā)訪問(wèn)量、容錯(cuò)能力、數(shù)據(jù)安全性、可擴(kuò)展性等。
其實(shí),這兩類需求整好與軟件系統(tǒng)的兩類“復(fù)雜性”一一對(duì)應(yīng):
- 業(yè)務(wù)復(fù)雜性,指系統(tǒng)中業(yè)務(wù)邏輯和業(yè)務(wù)規(guī)則的復(fù)雜程度。業(yè)務(wù)復(fù)雜性主要來(lái)自于業(yè)務(wù)的規(guī)模、結(jié)構(gòu)、變化性等,這個(gè)與軟件所在領(lǐng)域有極大關(guān)系;
- 技術(shù)復(fù)雜性,指系統(tǒng)中所用技術(shù)的復(fù)雜程度。技術(shù)復(fù)雜性主要來(lái)自于所使用的數(shù)據(jù)庫(kù)、網(wǎng)絡(luò)協(xié)議、中間件、應(yīng)用框架等,這與軟件架構(gòu)和基礎(chǔ)設(shè)施關(guān)系巨大;
面對(duì)不同的需求和復(fù)雜性,其設(shè)計(jì)目標(biāo)和應(yīng)對(duì)方式也完全不同。
1.1. 業(yè)務(wù)復(fù)雜性
首先,看下業(yè)務(wù)復(fù)雜性:
圖片
對(duì)于業(yè)務(wù)需求,我們追求的是系統(tǒng)的復(fù)用性和擴(kuò)展性。
- 復(fù)用性。指組件或模塊能夠在不同的場(chǎng)景下被重復(fù)使用。高復(fù)用性設(shè)計(jì)能夠大幅度減少開(kāi)發(fā)和維護(hù)的成本,提高開(kāi)發(fā)效率。組件的粒度越小復(fù)用性越高,功能結(jié)構(gòu)越清晰,邏輯調(diào)整越便利,與之相反的是代碼重復(fù)率,重復(fù)代碼是系統(tǒng)腐化的重要標(biāo)志;
- 擴(kuò)展性。能夠在不改變現(xiàn)有系統(tǒng)結(jié)構(gòu)的情況下,方便地添加新的功能或修改現(xiàn)有功能。具有高擴(kuò)展性的系統(tǒng)能夠滿足未來(lái)需求的變化,降低系統(tǒng)的維護(hù)成本。
導(dǎo)致業(yè)務(wù)代碼變化的原因有很多,比如:
- 線上bug。線上bug很少但對(duì)業(yè)務(wù)系統(tǒng)的傷害巨大,發(fā)現(xiàn)bug后為了快速修復(fù)問(wèn)題,往往選擇短平快的方式而非最佳方案,這些“補(bǔ)丁”就像系統(tǒng)中的“飛線”在代碼中穿梭,成為超出三界的定時(shí)炸彈,一不小心就會(huì)給你意外的“驚喜”;
- 新功能需求。這是代碼膨脹的主要推動(dòng)力,開(kāi)發(fā)人員將產(chǎn)品提出的需求翻譯成代碼,不停的“塞入”到代碼倉(cāng)庫(kù),導(dǎo)致倉(cāng)庫(kù)快速膨脹,很快你將面對(duì)幾十萬(wàn)行代碼并在之上進(jìn)行新的開(kāi)發(fā);
- 創(chuàng)新性業(yè)務(wù)。意味著新建系統(tǒng)、新建倉(cāng)庫(kù)、新建服務(wù),看起來(lái)一切非常良好,可以一次性甩掉多年的歷史包袱,但公司整個(gè)系統(tǒng)變得越來(lái)越復(fù)雜,甚至沒(méi)人能說(shuō)出服務(wù)間的調(diào)用關(guān)系;
這些變更都會(huì)導(dǎo)致業(yè)務(wù)代碼越來(lái)越多、邏輯越來(lái)越復(fù)雜,最終變得難以維護(hù)。
為了更好的應(yīng)對(duì)這些變化,常見(jiàn)的手段包括:
- DDD, 構(gòu)建于“領(lǐng)域模型”基礎(chǔ)之上,使用面向?qū)ο蟮母鞣N語(yǔ)言特性,實(shí)現(xiàn)邏輯的封裝、復(fù)用;將業(yè)務(wù)概念和實(shí)現(xiàn)組件結(jié)合在一起,避免相互轉(zhuǎn)化,從而降低溝通成本;
- 重構(gòu),隨著業(yè)務(wù)的變化,對(duì)原有代碼結(jié)構(gòu)進(jìn)行優(yōu)化,在新結(jié)構(gòu)上以擴(kuò)展的方式完成新功能的添加,不斷地對(duì)代碼結(jié)構(gòu)進(jìn)行調(diào)優(yōu)
- TDD,重構(gòu)的重要保障,在優(yōu)化代碼結(jié)構(gòu)的同時(shí),保障不會(huì)破壞原有的業(yè)務(wù)邏輯
業(yè)務(wù)復(fù)雜性先簡(jiǎn)單介紹到這,接下來(lái)看下技術(shù)復(fù)雜性:
1.2. 技術(shù)復(fù)雜性
圖片
對(duì)于非功能需求,我們追求的是系統(tǒng)的高性能和高可用。
- 高性能。在同等資源下,要么讓系統(tǒng)運(yùn)行盡可能快,要么讓系統(tǒng)吞吐盡可能大
- 高可用。盡量保障系統(tǒng) 7 * 24h 不間斷的提供服務(wù),避免由于服務(wù)中斷導(dǎo)致公司損失
導(dǎo)致技術(shù)復(fù)雜性激增的原因有很多,比如:
- 用戶和并發(fā)量。隨著用戶和并發(fā)量的激增,系統(tǒng)承受的壓力將越來(lái)越大,一個(gè)小小的卡點(diǎn)便能造成巨大的損失
- 系統(tǒng)數(shù)據(jù)量。隨著系統(tǒng)數(shù)據(jù)量不斷積累,當(dāng)單表數(shù)據(jù)量超過(guò) 億 級(jí),不管是訪問(wèn)速度還是異常恢復(fù)都將面臨巨大挑戰(zhàn)
- 機(jī)器規(guī)模。當(dāng)機(jī)器規(guī)模超過(guò)一定的閾值,硬件問(wèn)題將頻繁爆發(fā),基本每天都會(huì)出現(xiàn)硬件故障,最終成為高可用的阻力
這些問(wèn)題并不能通過(guò)簡(jiǎn)單的增加代碼來(lái)解決,往往需要引入新技術(shù)或使用新方案:
- 新技術(shù)。當(dāng)數(shù)據(jù)庫(kù)表數(shù)據(jù)量達(dá)到“億”級(jí)出現(xiàn)明顯的性能瓶頸時(shí),可以應(yīng)用 分庫(kù)分表 或 TiDB 來(lái)解決
- 新方案。當(dāng)數(shù)據(jù)庫(kù)讀壓力巨大,可以調(diào)整技術(shù)方案,使用數(shù)據(jù)庫(kù)讀寫分離、增加緩存等方案解決
通過(guò)對(duì)比,是否有一種感覺(jué):“業(yè)務(wù)”與“技術(shù)” 差距巨大,可以說(shuō)是天壤之別。所以需要一種架構(gòu),能夠?qū)?“業(yè)務(wù)” 和 “技術(shù)” 進(jìn)行隔離,降低兩者的相互影響,使得:
- 技術(shù)調(diào)整不影響業(yè)務(wù)模型
- 業(yè)務(wù)調(diào)整不能依賴技術(shù)
六邊形架構(gòu),便可以解決這個(gè)問(wèn)題。
2. 六邊形架構(gòu)
六邊形架構(gòu)出自《實(shí)現(xiàn)領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)》一書,與“洋蔥架構(gòu)” 非常相似,都能很好的實(shí)現(xiàn) “業(yè)務(wù)” 與 “技術(shù)” 的分離。
在 “Command側(cè)”(詳見(jiàn)CQRS架構(gòu)) DDD 仍舊是最佳解決方案,所以在此使用 “六邊形架構(gòu)” 作為頂級(jí)架構(gòu)。
六邊形架構(gòu)如下:
圖片
該架構(gòu)由內(nèi)外兩個(gè)六邊形組成,這也是其名稱來(lái)源:
- 內(nèi)六邊形屬于業(yè)務(wù)域,用于應(yīng)對(duì)業(yè)務(wù)復(fù)雜性。
- 外六邊形屬于技術(shù)域,用于應(yīng)對(duì)技術(shù)復(fù)雜性。
- 其中內(nèi)六邊形是整個(gè)系統(tǒng)的核心,外六邊形依賴于內(nèi)六邊形,而內(nèi)六邊形不依賴于外六邊形
- 內(nèi)外兩個(gè)六邊形存在清晰的邊界,兩者相互獨(dú)立,互不影響,獨(dú)自演進(jìn)
2.1.【業(yè)務(wù)】?jī)?nèi)六邊形
內(nèi)六邊形聚焦于業(yè)務(wù)邏輯,使用良好的設(shè)計(jì)應(yīng)對(duì)業(yè)務(wù)的復(fù)雜性;通過(guò)提升系統(tǒng)的復(fù)用性和擴(kuò)展性,來(lái)應(yīng)對(duì)未來(lái)的變化。
2.1.1. DDD
DDD 是解決復(fù)雜業(yè)務(wù)的一把利器,將業(yè)務(wù)需求和代碼實(shí)現(xiàn)相結(jié)合,通過(guò)對(duì)業(yè)務(wù)領(lǐng)域的深入理解,構(gòu)建出可復(fù)用、可維護(hù)、可擴(kuò)展的領(lǐng)域模型。
DDD 分為戰(zhàn)略和戰(zhàn)術(shù)兩部分,在此重點(diǎn)闡述戰(zhàn)術(shù)部分。戰(zhàn)術(shù)模型主要包括:
- 實(shí)體(Entity):具有唯一標(biāo)識(shí)的領(lǐng)域?qū)ο螅哂胸S富的屬性和行為,表示需要持續(xù)跟蹤的領(lǐng)域概念,比如 用戶、訂單、地址等;
- 值對(duì)象(Value Object):沒(méi)有唯一標(biāo)識(shí)的領(lǐng)域?qū)ο螅哂袑傩院托袨?,一般用于表示某一個(gè)領(lǐng)域概念,比如 金額、郵箱、手機(jī)號(hào)等;
- 聚合根(Aggregate Root):一組由實(shí)體和值對(duì)象組成的高內(nèi)聚對(duì)象集合,由一個(gè)根實(shí)體來(lái)管理它們,我們也稱之為聚合根,比如 訂單+訂單項(xiàng)便組成了一個(gè)聚合,其中訂單為聚合根;
- 工廠(Factory):創(chuàng)建領(lǐng)域?qū)ο蟮囊环N機(jī)制,也是設(shè)計(jì)模式在 DDD 中的落地,它隱藏了對(duì)象創(chuàng)建細(xì)節(jié),并保障創(chuàng)建對(duì)象的有效性,主要解決復(fù)雜對(duì)象初始化問(wèn)題;
- 存儲(chǔ)庫(kù)(Repository):提供對(duì)領(lǐng)域?qū)ο蟮某志没蜋z索功能,將領(lǐng)域?qū)ο髲臄?shù)據(jù)存儲(chǔ)細(xì)節(jié)中分離出來(lái),完成領(lǐng)域?qū)ο蠛痛鎯?chǔ)引擎的解耦;
- 領(lǐng)域服務(wù)(Service):處理領(lǐng)域?qū)ο笾g的交互,沒(méi)有自己的狀態(tài),封裝了一些業(yè)務(wù)邏輯,主要用于需要多個(gè)領(lǐng)域?qū)ο笙嗷f(xié)作才能完成的業(yè)務(wù)場(chǎng)景,比如銀行轉(zhuǎn)賬;
- 領(lǐng)域事件(Event):指在領(lǐng)域內(nèi)發(fā)生的、有意義的、需要被捕捉的事件,主要完成服務(wù)內(nèi)的流程解耦和服務(wù)間的系統(tǒng)解耦;
這些領(lǐng)域?qū)ο笙嗷f(xié)作,共同承載復(fù)雜的業(yè)務(wù)場(chǎng)景,大幅提升了業(yè)務(wù)的復(fù)用性和擴(kuò)展性。
2.1.2. TDD
也稱為測(cè)試驅(qū)動(dòng)開(kāi)發(fā),它的基本思想是在編寫代碼之前先編寫測(cè)試,然后根據(jù)測(cè)試來(lái)編寫代碼,最后再運(yùn)行測(cè)試。可以幫助開(kāi)發(fā)人員更快地發(fā)現(xiàn)并修復(fù)代碼中的bug,還可以讓開(kāi)發(fā)人員更加關(guān)注代碼的設(shè)計(jì),使代碼更加健壯、可擴(kuò)展和易維護(hù)。
業(yè)務(wù)模型與基礎(chǔ)設(shè)施的解耦將為你的 TDD 帶來(lái)眾多好處:
- 提高測(cè)試的速度:使用 Mock 技術(shù)可以避免測(cè)試過(guò)程中涉及到緩慢的網(wǎng)絡(luò)或數(shù)據(jù)庫(kù)操作,從而提高測(cè)試速度,加快迭代周期;
- 提高測(cè)試可靠性:使用 Mock數(shù)據(jù)可以降低測(cè)試失敗或不可靠的情況,避免由于數(shù)據(jù)變更所造成的測(cè)試不穩(wěn)定問(wèn)題;
- 更好的測(cè)試隔離:業(yè)務(wù)邏輯和基礎(chǔ)設(shè)施可以并行開(kāi)發(fā),避免兩者相互干擾而產(chǎn)生不確定行為;
- 有利于重構(gòu):業(yè)務(wù)邏輯和外部依賴分離,在重構(gòu)代碼時(shí),可以聚焦于業(yè)務(wù)邏輯而不必?fù)?dān)心外部依賴的穩(wěn)定性和正確性;
2.1.3. 兩頂帽子
兩頂帽子,是落地重構(gòu)的重要開(kāi)發(fā)模式,是一種工作習(xí)慣,或者說(shuō)是一種高效的工作流程。
兩頂帽子法,將軟件變更落地過(guò)程分為兩個(gè)階段:
- 優(yōu)化結(jié)構(gòu)階段。在不改變軟件行為的前提下,對(duì)軟件結(jié)構(gòu)進(jìn)行優(yōu)化,使其更具擴(kuò)展性。比如:
- 【重構(gòu)】抽取公共邏輯到方法、類,以便更好的被復(fù)用;
- 【設(shè)計(jì)模式】抽取模板方法,對(duì)核心邏輯進(jìn)行統(tǒng)一;
- 【架構(gòu)模式】抽取功能微內(nèi)核,確定插件簽名和功能;
- 添加功能階段。在調(diào)整后的具備更好的擴(kuò)展性的代碼基礎(chǔ)上完成功能調(diào)整或者增加新功能,如:
- 新功能復(fù)用已有組件能力,避免重復(fù)開(kāi)發(fā);
- 以擴(kuò)展點(diǎn)的方式增加新功能,提升開(kāi)發(fā)效率;
2.2.【技術(shù)】外六邊形
外六邊形聚焦于技術(shù),與公司基礎(chǔ)設(shè)施密切相關(guān),也受所用框架的各種約束。其核心是發(fā)揮框架和中間件的優(yōu)勢(shì),更好的為業(yè)務(wù)提供服務(wù)。
根據(jù)應(yīng)用場(chǎng)景的不同,我們將外六邊形的組件分成兩類:
- 輸入適配器。將來(lái)自外部的數(shù)據(jù)轉(zhuǎn)換為系統(tǒng)可以使用的格式;
- 輸出適配器。將系統(tǒng)內(nèi)部的數(shù)據(jù)轉(zhuǎn)換為外部可以使用的格式
他們是系統(tǒng)與外部的“翻譯官”,將外部請(qǐng)求轉(zhuǎn)換為系統(tǒng)可以理解的指令,并將系統(tǒng)響應(yīng)轉(zhuǎn)換為外部世界可以理解的格式,這種松耦合可以使系統(tǒng)更加靈活更具擴(kuò)展性。
2.2.1. 輸入適配器
負(fù)責(zé)將外部系統(tǒng)的請(qǐng)求轉(zhuǎn)換為應(yīng)用服務(wù)能夠處理的格式,通常包括Web 請(qǐng)求、RPC調(diào)用、消息隊(duì)列、定時(shí)任務(wù)等,是將外部請(qǐng)求轉(zhuǎn)換為內(nèi)部指令的橋梁。
常見(jiàn)的輸入適配器主要包括:
- Web 請(qǐng)求。處理 HTTP 請(qǐng)求,對(duì)請(qǐng)求進(jìn)行轉(zhuǎn)換、驗(yàn)證,將其轉(zhuǎn)換為應(yīng)用服務(wù)所需格式,調(diào)用接口完成業(yè)務(wù)邏輯,最后將處理結(jié)果轉(zhuǎn)化為所需格式進(jìn)行返回。常見(jiàn)的框架有 Spring MVC、Struct2等;
- RPC 調(diào)用。處理遠(yuǎn)程調(diào)用請(qǐng)求,將請(qǐng)求轉(zhuǎn)換為應(yīng)用程序所需格式,調(diào)用應(yīng)用服務(wù)接口完成業(yè)務(wù)邏輯,最后返回處理結(jié)果;在 Spring Cloud 技術(shù)棧下,與 Web 請(qǐng)求高度類似,但仍舊具有自己的特點(diǎn),需要與 Web 請(qǐng)求進(jìn)行區(qū)分處理。常見(jiàn)的 RPC 框架有 Spring Cloud、gRPC、Thrift、Dubbo等;
- 消息隊(duì)列。主要指的是消息隊(duì)列的消費(fèi)端,從消息隊(duì)列中讀取消息,將信息轉(zhuǎn)換為應(yīng)用程序所需格式,調(diào)用應(yīng)用服務(wù)接口完成業(yè)務(wù)邏輯。常見(jiàn)的有 RocketMQ、Kafka、RabbitMQ等;
- 定時(shí)任務(wù)。由定時(shí)器周期性觸發(fā),調(diào)用應(yīng)用服務(wù)的業(yè)務(wù)方法,完成某種后臺(tái)任務(wù)。常見(jiàn)的有Quartz、XXL-job、Spring Task 等;
2.2.2. 輸出適配器
負(fù)責(zé)將應(yīng)用程序輸出結(jié)果轉(zhuǎn)換為外部系統(tǒng)能夠理解的格式,通常包括數(shù)據(jù)庫(kù)、RPC調(diào)用、緩存、搜索、消息隊(duì)列、文件系統(tǒng)等,是將內(nèi)部響應(yīng)轉(zhuǎn)換為外部響應(yīng)的橋梁。
常見(jiàn)的輸出適配器主要包括:
- 數(shù)據(jù)庫(kù)。將領(lǐng)域模型中的模型數(shù)據(jù)保存到數(shù)據(jù)庫(kù)進(jìn)行持久化存儲(chǔ),常用的框架包括 MyBatis、Jpa、Hibernate等,中間件主要是 MySQL;
- 緩存。模型數(shù)據(jù)發(fā)生變更后,對(duì)緩存數(shù)據(jù)進(jìn)行清理或更新,常見(jiàn)框架包括本地緩存 Guava、Caffeine、EhCache,分布式緩存有 Redis、Memcache、Tair等;
- 搜索。為應(yīng)對(duì)多維度查詢,系統(tǒng)會(huì)引入搜索引擎組件,在模型數(shù)據(jù)發(fā)生變更后,需要將變更同步到搜索引擎,常見(jiàn)的有Elasticsearch、Solr、Sphinx等;
- 消息隊(duì)列。這里主要指的是消息隊(duì)列的發(fā)送端,當(dāng)業(yè)務(wù)操作完成后,系統(tǒng)會(huì)向外發(fā)布領(lǐng)域事件,以將變更通知到下游系統(tǒng),常見(jiàn)的有 RocketMQ、Kafka、RabbitMQ等;
- RPC調(diào)用。這里主要指的是 RPC 的調(diào)用端,當(dāng)業(yè)務(wù)模型對(duì)其他領(lǐng)域服務(wù)存在依賴時(shí),需要通過(guò) RPC 進(jìn)行系統(tǒng)通信,常見(jiàn)的 RPC 框架有 Spring Cloud、gRPC、Thrift、Dubbo等;
- 文件系統(tǒng)。這里簡(jiǎn)單理解為系統(tǒng)的日志輸出即可,常見(jiàn)的有Log4j、Logback、SLF4J、JUL等;
這么多紛繁復(fù)雜的框架、中間件從另一個(gè)層面也驗(yàn)證了,外六邊形是以技術(shù)作為驅(qū)動(dòng)的,其核心就是:如何更好的使用這些技術(shù)工具,發(fā)揮每個(gè)框架的強(qiáng)項(xiàng)。
3. 小結(jié)
任意一個(gè)業(yè)務(wù)系統(tǒng)都會(huì)面對(duì)兩類需求:
- 來(lái)自業(yè)務(wù)的功能性需求;
- 來(lái)自技術(shù)的非功能性需求;
兩類需求背后的驅(qū)動(dòng)力(復(fù)雜性)完全不同:
- 功能性需求的驅(qū)動(dòng)力在于業(yè)務(wù)自身的復(fù)雜性和多變性;
- 非功能性需求的驅(qū)動(dòng)力在于架構(gòu)的變更和技術(shù)的更迭;
為了更好的應(yīng)對(duì)這兩類變化,需要在架構(gòu)上進(jìn)行隔離,避免相互影響帶來(lái)更多的復(fù)雜性,然后逐個(gè)擊破。這就是六邊形架構(gòu)最擅長(zhǎng)的領(lǐng)域:
- 內(nèi)六邊形。聚焦于業(yè)務(wù)解決功能性需求,常用的手段有 DDD、TDD、重構(gòu);
- 外六邊形。聚焦于技術(shù)解決非功能性需求,需要使用好 輸入適配器 和 輸出適配器;