學(xué)會洋蔥架構(gòu),落地DDD得心應(yīng)手
領(lǐng)域是一個知識的范疇。它指的是我們的軟件所要模擬的業(yè)務(wù)知識。領(lǐng)域驅(qū)動設(shè)計的中心是領(lǐng)域模型,它對一個領(lǐng)域的流程和規(guī)則有著深刻的理解。洋蔥架構(gòu)實現(xiàn)了這一概念,并極大地改善了代碼的品質(zhì),降低了復(fù)雜性,并且支持不斷發(fā)展的企業(yè)系統(tǒng)。
一、為什么要用洋蔥架構(gòu)?
領(lǐng)域?qū)嶓w是核心和中心部分。洋蔥架構(gòu)是建立在一個領(lǐng)域模型上的,其中各層是通過接口連接的。其背后的思想是,在領(lǐng)域?qū)嶓w和業(yè)務(wù)規(guī)則構(gòu)成架構(gòu)的核心部分時,盡可能將外部依賴性保持在外。
- 它提供了靈活、可持續(xù)和可移植的架構(gòu)。
- 各層之間沒有緊密的耦合,并且有關(guān)注點的分離。
- 由于所有的代碼都依賴于更深的層或者中心,所以提供了更好的可維護性。
- 提高了整體代碼的可測試性,因為單元測試可以為單獨的層創(chuàng)建,而不會影響到其他的模塊。
- 框架/技術(shù)可以很容易地改變而不影響核心領(lǐng)域。例如,RabbitMQ 可以被 ActiveMQ 取代,SQL 可以被 MongoDB 取代。
二、原則
洋蔥架構(gòu)是由多個同心層構(gòu)成,它們相互連接,并朝向代表領(lǐng)域的核心。它是基于控制反轉(zhuǎn)(Inversion of Control,IoC)的原則。該架構(gòu)并不關(guān)注底層技術(shù)或框架,而是關(guān)注實際的領(lǐng)域模型。它是基于以下原則:
1、依賴性
圓圈代表不同的責(zé)任層。一般來說,我們潛入得越深,就越接近于領(lǐng)域和業(yè)務(wù)規(guī)則。外圈代表機制,內(nèi)圈代表核心領(lǐng)域邏輯。外層依賴于內(nèi)層,而內(nèi)層則對外圈一無所知。通常情況下,屬于外圈的類、方法、變量和源代碼依賴于內(nèi)圈,但是反過來也一樣。
數(shù)據(jù)格式/結(jié)構(gòu)可能因?qū)佣?。外層的?shù)據(jù)格式不應(yīng)該被內(nèi)層使用。例如,API 中使用的數(shù)據(jù)格式可以與 DB 中用于持久化的數(shù)據(jù)格式不同。數(shù)據(jù)流可以使用數(shù)據(jù)傳輸對象。每當數(shù)據(jù)跨層/跨界時,它應(yīng)該以方便該層的形式出現(xiàn)。例如,API 可以有 DTO,DB 層可以有 Entity Objects,這取決于存儲在數(shù)據(jù)庫中的對象與領(lǐng)域模型的不同。
2、數(shù)據(jù)封裝
每個層/圈封裝或隱藏內(nèi)部的實現(xiàn)細節(jié),并向外層公開接口。所有的層也需要提供便于內(nèi)層消費的信息。其目的是最小化層與層之間的耦合,最大化跨層垂直切面內(nèi)的耦合。我們在較深的層定義抽象接口,并在最外層提供其具體實現(xiàn)。這樣可以確保我們專注于領(lǐng)域模型,而不必過多地擔(dān)心實現(xiàn)細節(jié)。我們還可以使用依賴性注入框架,比如 Spring,在運行時將接口與實現(xiàn)連接起來。例如,領(lǐng)域中使用的存儲庫和應(yīng)用服務(wù)中使用的外部服務(wù)在基礎(chǔ)設(shè)施層實現(xiàn)。
洋蔥架構(gòu)中的數(shù)據(jù)封裝
3、關(guān)注點的分離
應(yīng)用被分為若干層,每一層都有一組職責(zé),并解決不同的關(guān)注點。每一層都作為應(yīng)用中的模塊/包/命名空間。
4、耦合性
低耦合性,可以使一個模塊與另一個模塊交互,而不需要關(guān)注另一個模塊的內(nèi)部。所有的內(nèi)部層都不需要關(guān)注外部層的內(nèi)部實現(xiàn)。
三、洋蔥架構(gòu)層
讓我們通過一個創(chuàng)建訂單的用例來了解架構(gòu)的不同層和它們的職責(zé)。當收到一個創(chuàng)建訂單的請求時,我們會對這個訂單進行驗證,將這個訂單保存在數(shù)據(jù)庫中,更新所有訂單項目的庫存,借記訂單金額,最后向客戶發(fā)送訂單完成的通知。
說明各層之間的依賴關(guān)系的包圖
1、領(lǐng)域模型/實體
領(lǐng)域?qū)嶓w是領(lǐng)域驅(qū)動設(shè)計的基本構(gòu)件,它們被用來在代碼中為通用語言的概念建模。實體是在問題域中具有唯一身份的領(lǐng)域概念。領(lǐng)域?qū)嶓w封裝了屬性和實體行為。它應(yīng)該是獨立于數(shù)據(jù)庫或網(wǎng)絡(luò) API 等特定技術(shù)的。例如,在訂單領(lǐng)域,訂單是一個實體,并具有像 OrderId、Address、UserInfo、OrderItems、PricingInfo 這樣的屬性以及像 AddOrderItems、GetPricingInfo、ValidateOrder 這樣的行為。
訂單實體類
2、領(lǐng)域服務(wù)
領(lǐng)域服務(wù)負責(zé)保持領(lǐng)域邏輯和業(yè)務(wù)規(guī)則。所有的業(yè)務(wù)邏輯應(yīng)該作為領(lǐng)域服務(wù)的一部分來實現(xiàn)。領(lǐng)域服務(wù)由應(yīng)用服務(wù)協(xié)調(diào),以服務(wù)于業(yè)務(wù)用例。它們不是典型的 CRUD 服務(wù),通常是獨立的服務(wù)。領(lǐng)域服務(wù)負責(zé)復(fù)雜的業(yè)務(wù)規(guī)則,如在處理訂單時計算價格和稅收信息,保存和更新訂單的訂單庫接口,更新購買物品信息的庫存接口等。
它包含了對其目標非常關(guān)鍵的算法,并且將用例作為應(yīng)用的核心來實現(xiàn)。
3、應(yīng)用服務(wù)
應(yīng)用服務(wù)也被稱為“用例”,是只負責(zé)協(xié)調(diào)請求步驟的服務(wù),不應(yīng)該有任何業(yè)務(wù)邏輯。應(yīng)用服務(wù)與其他服務(wù)交互,以滿足客戶的請求。讓我們考慮一下用例,用一個物品清單創(chuàng)建一個訂單。我們首先需要計算價格,包括稅收計算/折扣等,保存訂單項目并向客戶發(fā)送訂單確認通知。定價計算應(yīng)該是領(lǐng)域服務(wù)的一部分,但涉及定價計算、檢查可用性、保存訂單和通知用戶的協(xié)調(diào)工作應(yīng)該是應(yīng)用服務(wù)的一部分。應(yīng)用服務(wù)只能由基礎(chǔ)設(shè)施服務(wù)調(diào)用。
4、基礎(chǔ)設(shè)施服務(wù)
基礎(chǔ)設(shè)施服務(wù)也被稱為基礎(chǔ)設(shè)施適配器,是洋蔥架構(gòu)的最外層。這些服務(wù)負責(zé)與外部世界交互,不解決任何領(lǐng)域的問題。這些服務(wù)只是與外部資源通信,沒有任何邏輯。例如:外部通知服務(wù)、GRPC 服務(wù)器端點、Kafka 事件流適配器、數(shù)據(jù)庫適配器。
5、可觀察性服務(wù)
可觀察性服務(wù)負責(zé)監(jiān)控應(yīng)用。這些服務(wù)有助于執(zhí)行以下任務(wù):
- 數(shù)據(jù)收集(指標、日志、痕跡):主要使用庫/側(cè)線來收集代碼執(zhí)行期間的各種數(shù)據(jù)。
- 數(shù)據(jù)存儲:使用能夠集中存儲所收集的數(shù)據(jù)的工具(分類、索引等)。
- 可視化:使用允許你對收集的數(shù)據(jù)進行可視化的工具。
一些例子包括 Splunk、ELK、Grafana、Graphite、Datadog。
四、測試策略
洋蔥架構(gòu)的不同層有不同的職責(zé),相應(yīng)地也有不同的測試策略。測試金字塔是一個很好的框架,它規(guī)定了不同類型的測試。屬于領(lǐng)域模型、領(lǐng)域服務(wù)和應(yīng)用服務(wù)的業(yè)務(wù)規(guī)則應(yīng)通過單元測試進行測試。當我們移動到外層時,在基礎(chǔ)設(shè)施服務(wù)中進行集成測試更有意義。對于我們的應(yīng)用,端到端測試和 BDD 是最合適的測試策略。
針對不同層的測試策略
五、微服務(wù)
當孤立地看待每個微服務(wù)時,洋蔥架構(gòu)也適用于微服務(wù)。每個微服務(wù)都有自己的模型、自己的用例,并定義了自己的外部接口,用于檢索或修改數(shù)據(jù)。這些接口可以用一個適配器來實現(xiàn),該適配器通過公開 HTTP Rest、GRPC、Thrift Endpoints 等連接到另一個微服務(wù)。它很適合微服務(wù),在微服務(wù)中,數(shù)據(jù)訪問層不僅包括數(shù)據(jù)庫,還包括例如一個 http 客戶端,以從另一個微服務(wù),甚至從外部系統(tǒng)獲取數(shù)據(jù)。
六、應(yīng)用結(jié)構(gòu)和層數(shù)
應(yīng)用結(jié)構(gòu)和層,包括層如何映射到模塊以及它們之間的依賴關(guān)系。它還描述了對不同層使用什么樣的測試策略。
七、模塊化與打包
有兩種方法來組織應(yīng)用的源代碼:
- 要么我們可以將所有的包放在一個模塊/項目中
- 要么將應(yīng)用分為不同的模塊/項目,每個模塊/項目負責(zé)洋蔥架構(gòu)中的一個層
這在很大程度上取決于應(yīng)用的復(fù)雜性和項目的規(guī)模,將源代碼分為多個模塊。在微服務(wù)架構(gòu)中,模塊化可能有意義,也可能沒有意義,這取決于復(fù)雜性和用例。
八、框架、客戶端和驅(qū)動
基礎(chǔ)設(shè)施層由網(wǎng)絡(luò)或服務(wù)器的框架、數(shù)據(jù)庫的客戶端、隊列或外部服務(wù)組成。它負責(zé)配置和縫合所有的外部服務(wù)和框架。洋蔥架構(gòu)提供了解耦功能,因此在任何時候交換技術(shù)都會變得更容易。
九、我們需要每個層嗎?
將我們的應(yīng)用分層組織有助于實現(xiàn)關(guān)注點的分離。但我們需要所有的層嗎?也許需要,也許不需要。這取決于用例和應(yīng)用的復(fù)雜性。根據(jù)應(yīng)用的需要,也可以創(chuàng)建更多的抽象層。例如,對于沒有很多業(yè)務(wù)邏輯的小型應(yīng)用,擁有領(lǐng)域服務(wù)可能沒有意義。無論哪一層,依賴關(guān)系都應(yīng)該是從外層到內(nèi)層。
十、總結(jié)
洋蔥架構(gòu)在開始時可能似乎有些困難,但是在業(yè)界已經(jīng)得到了普遍的認可。這是一種讓軟件易于演進的強有力架構(gòu)。通過把應(yīng)用劃分為幾層,可以使系統(tǒng)更加易于測試、維護和移植。它有助于在舊框架過時時輕松采用新框架/技術(shù)。與其他架構(gòu)風(fēng)格類似,如六邊形、分層、簡潔的架構(gòu)等,它為常見問題提供了一個解決方案。