這篇文章是軟件架構(gòu)編年史(譯)的一部分,這部編年史由一系列關(guān)于軟件架構(gòu)的文章組成。在這一系列文章中,我將寫下我對(duì)軟件架構(gòu)的學(xué)習(xí)和思考,以及我是如何運(yùn)用這些知識(shí)的。如果你閱讀了這個(gè)系列中之前的文章,本篇文章的的內(nèi)容將更有意義。
大學(xué)畢業(yè)之后我做了一名高中老師,直到幾年前我決定成為一名全職軟件開(kāi)發(fā)者。
從那時(shí)起,我時(shí)常覺(jué)得我必須找回“失去的”時(shí)間,盡力地多學(xué)一點(diǎn),學(xué)快一點(diǎn)。所以我變得有一點(diǎn)沉迷于實(shí)驗(yàn)、閱讀和寫作,特別關(guān)注的就是軟件設(shè)計(jì)和架構(gòu)。幫助我學(xué)習(xí)正是我寫下這些文章的初衷。
在之前的文章中,我記錄了許多學(xué)到的概念和原則和我的一些思考。但我知道這只是管中窺豹。
今天的文章內(nèi)容是我如何將這些碎片融合成一種新的架構(gòu),看起來(lái)我得給它起個(gè)名字,就叫做清晰架構(gòu)吧。而且,這些概念全都經(jīng)歷過(guò)“炮火的洗禮”,在高水準(zhǔn)的平臺(tái)生產(chǎn)代碼中得到了應(yīng)用。其中一個(gè)是擁有數(shù)千家遍布全球的網(wǎng)上商店的 SaaS 電子商務(wù)平臺(tái),另一個(gè)是已經(jīng)在兩個(gè)國(guó)家上線的市場(chǎng),它擁有可以每月處理超過(guò)兩千萬(wàn)條消息的消息總線。
系統(tǒng)的基本構(gòu)建塊
我們從 EBI 架構(gòu)以及端口和適配器架構(gòu)(譯)的回顧開(kāi)始。它們都有清晰的代碼劃分,哪些代碼在應(yīng)用內(nèi)部,哪些代碼在外部,而哪些代碼用來(lái)連接它們。
除此之外,端口和適配器架構(gòu)還明確地識(shí)別出了一個(gè)系統(tǒng)中的三個(gè)基本代碼構(gòu)建塊:
- 運(yùn)行用戶界面所需的構(gòu)建塊,無(wú)論是哪種用戶界面;
- 系統(tǒng)的業(yè)務(wù)邏輯,或者應(yīng)用核心,用戶界面要使用這個(gè)構(gòu)建塊達(dá)成目的;
- 基礎(chǔ)設(shè)施代碼,這個(gè)構(gòu)建塊將我們的應(yīng)用核心和諸如數(shù)據(jù)庫(kù)、搜索引擎或第三方 API 這樣的工具連接起來(lái)。

我們真正關(guān)心的應(yīng)該是應(yīng)用核心。這部分代碼才是我們編寫代碼的目的,它們才是我們的應(yīng)用。它們可能使用一些不同的用戶界面(漸進(jìn)式 Web 應(yīng)用,移動(dòng)應(yīng)用、命令行、接口...),但完成實(shí)際工作的代碼是一模一樣的,它們就在應(yīng)用核心內(nèi)部,它們不用關(guān)心是哪種用戶界面觸發(fā)了它們。
你可以想像得到,典型的應(yīng)用控制流開(kāi)始于用戶界面中的代碼,經(jīng)過(guò)應(yīng)用核心到達(dá)基礎(chǔ)設(shè)施代碼,又返回應(yīng)用核心,最后將響應(yīng)傳達(dá)給用戶界面。

工具
在遠(yuǎn)離我們系統(tǒng)中最重要的代碼-——應(yīng)用核心-——的地方,還有一些應(yīng)用會(huì)用到的工具,例如數(shù)據(jù)庫(kù)引擎、搜索引擎、Web 服務(wù)器或者命令行控制臺(tái)(雖然最后兩種工具也是傳達(dá)機(jī)制)。

把命令行控制臺(tái)和數(shù)據(jù)庫(kù)引擎放在同一個(gè)“籃子”中感覺(jué)有點(diǎn)奇怪,盡管它們有著不用的用途,但它們實(shí)際都是應(yīng)用使用的工具。關(guān)鍵的區(qū)別在于,命令行控制臺(tái)和 Web 服務(wù)器告訴我們的應(yīng)用它要做什么,而數(shù)據(jù)庫(kù)引擎是由我們的應(yīng)用來(lái)告訴它做什么。這是針?shù)h相對(duì)的差別,強(qiáng)烈地暗示著我們應(yīng)該如何構(gòu)建連接這些工具和應(yīng)用核心的代碼。
將傳達(dá)機(jī)制和工具連接到應(yīng)用核心
連接工具和應(yīng)用核心的代碼單元被稱為適配器(端口和適配器架構(gòu))。適配器有效地實(shí)現(xiàn)了讓業(yè)務(wù)邏輯和特定工具之間可以相互通信的代碼。
告知我們的應(yīng)用應(yīng)該做什么的適配器被稱為主適配器或主動(dòng)適配器,而那些由我們的應(yīng)用告知它該做什么的適配器被稱為從適配器或者被動(dòng)適配器。
端口
而這些適配器并非是隨意創(chuàng)建的。它們需要按照應(yīng)用核心某個(gè)特定的入口的要求來(lái)創(chuàng)建,即端口。端口無(wú)外乎是一份工具如何使用應(yīng)用核心或者如何被應(yīng)用核心使用的說(shuō)明書。這份說(shuō)明書,即端口,在大多數(shù)語(yǔ)言里最簡(jiǎn)單的形式就是接口,但實(shí)際上也可能由多個(gè)接口和 DTO 組成。
端口(接口)位于業(yè)務(wù)邏輯內(nèi)部,而適配器位于其外部,這一點(diǎn)要特別注意。要讓這種模式按照設(shè)想發(fā)揮作用,端口按照應(yīng)用核心的需要來(lái)設(shè)計(jì)而不是簡(jiǎn)單地套用工具的 API,這一點(diǎn)再怎么強(qiáng)調(diào)都不為過(guò)。
主適配器或主動(dòng)適配器
主適配器或主動(dòng)適配器包裝端口并通過(guò)它告知應(yīng)用核心應(yīng)該做什么。它們將來(lái)自傳達(dá)機(jī)制的信息轉(zhuǎn)換成對(duì)應(yīng)用核心的方法調(diào)用。

換句話說(shuō),我們的主動(dòng)適配器就是 Controller 或者控制臺(tái)命令,它們需要的接口(端口)由其他類實(shí)現(xiàn),這些類的對(duì)象通過(guò)構(gòu)造方法注入到 Controller 或者控制臺(tái)命令。
再舉一個(gè)更具體的例子,端口就是 Controller 需要的 Service 接口或者 Repository 接口。Service、Repository 或 Query 的具體實(shí)現(xiàn)被注入到 Controller 供 Controller 使用。
此外,端口還可以是命令總線接口或者查詢總線接口。這種情況下,命令總線或者查詢總線的具體實(shí)現(xiàn)將被注入到 Controller 中, Controller 將創(chuàng)建命令或查詢并傳遞給相應(yīng)的總線。
從適配器或被動(dòng)適配器
和主動(dòng)適配器包裝端口不同,被動(dòng)適配器實(shí)現(xiàn)一個(gè)端口(接口)并被注入到需要這個(gè)端口的應(yīng)用核心里。

舉個(gè)例子,假設(shè)有一個(gè)需要存儲(chǔ)數(shù)據(jù)的簡(jiǎn)單應(yīng)用。我們創(chuàng)建了一個(gè)符合應(yīng)用要求的持久化接口,這個(gè)接口有一個(gè)保存數(shù)據(jù)數(shù)組的方法和一個(gè)根據(jù) ID 從表中刪除一行的方法。接口創(chuàng)建好之后,無(wú)論何時(shí)應(yīng)用需要保存或刪除數(shù)據(jù),都應(yīng)該使用實(shí)現(xiàn)了這個(gè)持久化接口的對(duì)象,而這個(gè)對(duì)象是通過(guò)構(gòu)造方法注入的。
現(xiàn)在我們創(chuàng)建了一個(gè)專門針對(duì) MySQL 實(shí)現(xiàn)了該接口的適配器。它擁有保存數(shù)組和刪除表中一行數(shù)據(jù)的方法,然后在需要使用持久化接口的地方注入它。
如果未來(lái)我們決定更換數(shù)據(jù)庫(kù)供應(yīng)商,比如換成 PostgreSQL 或者 MongoDB,我們只用創(chuàng)建一個(gè)專門針對(duì) PostgreSQL 實(shí)現(xiàn)了該接口的適配器,在注入時(shí)用新適配器代替舊適配器。
控制反轉(zhuǎn)
這種模式有一個(gè)特征值得留意,適配器依賴特定的工具和特定的端口(它需要提供接口的特定實(shí)現(xiàn))。但業(yè)務(wù)邏輯只依賴按照它的需求設(shè)計(jì)的端口(接口),它并不依賴特定的適配器或工具。

這意味著依賴的方向是由外向內(nèi)的,這就是架構(gòu)層面的控制反轉(zhuǎn)原則。
再一次強(qiáng)調(diào),端口按照應(yīng)用核心的需要來(lái)設(shè)計(jì)而不是簡(jiǎn)單地套用工具的 API。
組織應(yīng)用核心的結(jié)構(gòu)
洋蔥架構(gòu)采用了 DDD 的分層,將它們?nèi)诤线M(jìn)了端口和適配器架構(gòu)。這種分層想要為位于端口和適配器架構(gòu)“六邊形”內(nèi)的業(yè)務(wù)邏輯帶來(lái)一種結(jié)構(gòu)組織,和端口與適配器架構(gòu)一樣,依賴的方向也是由外向內(nèi)。
應(yīng)用層
在應(yīng)用中,由一個(gè)或多個(gè)用戶界面觸發(fā)的應(yīng)用核心中的過(guò)程就是用例。例如,在一個(gè) CMS 系統(tǒng)中,我們可以提供普通用戶使用的應(yīng)用 UI、CMS 管理員使用的獨(dú)立的 UI、命令行 UI 以及 Web API。這些 UI(應(yīng)用)可以觸發(fā)的用例可能是專門為它設(shè)計(jì)的,也可以是多個(gè) UI 復(fù)用的。
用例定義在應(yīng)用層中,這是 DDD 提供的第一個(gè)被洋蔥架構(gòu)使用的層。

這個(gè)層包括了作為一等公民的應(yīng)用服務(wù)(以及它們的接口),也包括了端口與適配器架構(gòu)中的接口,例如 ORM 接口、搜索引擎接口、消息接口等等。如果我們使用了命令總線和/或查詢總線,命令和查詢分別對(duì)應(yīng)的處理程序也屬于這一層。
應(yīng)用服務(wù)和/或命令處理程序包含了展現(xiàn)一個(gè)用例,一個(gè)業(yè)務(wù)過(guò)程的邏輯。通常,它們的作用是:
- 使用 Repostitory 查找一個(gè)或多個(gè)實(shí)體;
- 讓這些實(shí)體執(zhí)行一些領(lǐng)域邏輯;
- 再次使用 Repostitory 讓這些實(shí)體持久化,有效地保存數(shù)據(jù)變化。
命令處理程序有兩種不同使用方式:
- 它們可以包含執(zhí)行用例的實(shí)際邏輯;
- 它們可以僅僅作為我們應(yīng)用中的連接片段,接收命令然后簡(jiǎn)單地觸發(fā)應(yīng)用服務(wù)中的邏輯。
使用哪種方式是由上下文決定的,例如:
- 我們已經(jīng)有了合適的應(yīng)用服務(wù),現(xiàn)在要做的是添加命令總線?
- 命令總線允許指定任意類/方法作為處理程序嗎?還是說(shuō)它們需要擴(kuò)展已有的類或者實(shí)現(xiàn)已有的接口?
應(yīng)用層還包括應(yīng)用事件的觸發(fā),這也代表著某些用例的產(chǎn)出。這些事件觸發(fā)的邏輯是用例的副作用,比如發(fā)送郵件、通知第三方 PAI、發(fā)送推送通知,或是發(fā)起屬于其他應(yīng)用組件的另一個(gè)用例。
領(lǐng)域?qū)?/h2>
繼續(xù)向內(nèi)一層就是領(lǐng)域?qū)?。這一層中的對(duì)象包含了數(shù)據(jù)和操作數(shù)據(jù)的邏輯,它們只和領(lǐng)域本身有關(guān),獨(dú)立于調(diào)用這些邏輯的業(yè)務(wù)過(guò)程。它們完全獨(dú)立,對(duì)應(yīng)用層完全無(wú)感知。

領(lǐng)域服務(wù)
如前所述,應(yīng)用服務(wù)的作用是:
- 使用 Repostitory 查找一個(gè)或多個(gè)實(shí)體;
- 讓這些實(shí)體執(zhí)行一些領(lǐng)域邏輯;
- 再次使用 Repostitory 讓這些實(shí)體持久化,有效地保存數(shù)據(jù)變化。
然而,有時(shí)我們還會(huì)碰到某種領(lǐng)域邏輯,它涉及不同的實(shí)體。這些實(shí)體也許是同一個(gè)類型,也許不是,而且我們覺(jué)得這種領(lǐng)域領(lǐng)域邏輯并不屬于這些實(shí)體,這種邏輯不是這些實(shí)體的直接責(zé)任。
所以,我們的第一反應(yīng)也許是把這些邏輯放到實(shí)體外的應(yīng)用服務(wù)中。然而,這意味著這些領(lǐng)域邏輯就不能被其它的用例復(fù)用。領(lǐng)域邏輯應(yīng)該放在應(yīng)用層之外!
解決方法是創(chuàng)建領(lǐng)域服務(wù),它的作用是接收一組實(shí)體并對(duì)它們執(zhí)行某種業(yè)務(wù)邏輯。領(lǐng)域服務(wù)屬于領(lǐng)域?qū)?,因此它并不了解?yīng)用層中的類,比如應(yīng)用服務(wù)或者 Repository[譯注:Repository 屬于應(yīng)用服務(wù)層??]。另一方面,它可以使用其他領(lǐng)域服務(wù),當(dāng)然還可以使用領(lǐng)域模型對(duì)象。
領(lǐng)域模型
在架構(gòu)的正中心,是完全不依賴外部任何層次的領(lǐng)域模型。它包含了那些表示領(lǐng)域中某個(gè)概念的業(yè)務(wù)對(duì)象。這些對(duì)象的例子首先就是實(shí)體,還有值對(duì)象、枚舉以及其它領(lǐng)域模型種用到的任何對(duì)象。
領(lǐng)域事件也“活在”領(lǐng)域模型中。當(dāng)一組特定的數(shù)據(jù)發(fā)生變化時(shí)就會(huì)觸發(fā)這些事件,而這些時(shí)間會(huì)攜帶這些變化的信息。換句話說(shuō),當(dāng)實(shí)體變化時(shí),就會(huì)觸發(fā)一個(gè)領(lǐng)域事件,它攜帶著發(fā)生變化的屬性的新值。這些事件可以完美地應(yīng)用于事件溯源。
組件
目前為止,我們都是使用層次來(lái)劃分代碼,但這是細(xì)粒度的代碼隔離。根據(jù) Robert C. Martin 在尖叫架構(gòu)中表達(dá)的觀點(diǎn),按照子域和限界上下文對(duì)代碼進(jìn)行劃分這種粗粒度的代碼隔離同樣重要。這通常被叫做“按特性分包”或者“按組件分包”,和“按層次分包”相呼應(yīng)。Simon Brown 的文章“Package by component and architecturally-aligned testing”很好地闡述了這種劃分:
![]() |
![]() |
![]() |
|---|
我是“按組件分包”方式的堅(jiān)定擁護(hù)者,在此我厚著臉皮將 Simon Brown 按組件分包的示意圖做了如下修改:

這些代碼塊在前面描述的分層基礎(chǔ)上再進(jìn)行了“橫切”,它們是應(yīng)用的組件(譯)。組件的例子包括認(rèn)證、授權(quán)、賬單、用戶、評(píng)論或帳號(hào),而它們總是都和領(lǐng)域相關(guān)。像認(rèn)證和/或授權(quán)這樣的限界上下文應(yīng)該被看作外部工具,我們應(yīng)該為它們創(chuàng)建適配器,把它們隱藏在某個(gè)端口之后。

組件解耦
和細(xì)粒度的代碼單元(類、接口、特質(zhì)、混合等等)一樣,粗粒度的代碼單元(組件)也會(huì)從高內(nèi)聚低耦合中受益。
我們使用了依賴注入,通過(guò)將依賴注入類而不是在類內(nèi)部初始化依賴;以及依賴倒置;讓類依賴抽象(接口和/或抽象類)而不是具體類來(lái)解耦類。這意味著類不用知道它要使用的具體類的任何信息,不用引用所依賴的類的完全限定類名。
以同樣的方式完全解耦的組件意味著組件不會(huì)直接了解其它任何組件的信息。換句話說(shuō),它不會(huì)引用任何來(lái)自其它組件的細(xì)粒度的代碼單元,甚至都不會(huì)引用接口!這意味著依賴注入和依賴倒置對(duì)組件解耦是不夠用的,我們還需要一些架構(gòu)層級(jí)的結(jié)構(gòu)。我們需要事件、共享內(nèi)核、最終一致性甚至發(fā)現(xiàn)服務(wù)!

觸發(fā)其它組件的邏輯
當(dāng)一個(gè)組件(組件 A)中有事情發(fā)生需要另一個(gè)組件(組件B)做些什么時(shí),我們不能簡(jiǎn)單地從組件 A 直接調(diào)用組件 B 中的類/方法,因?yàn)檫@樣 A 就和 B 耦合在一起了。
但是我們可以讓 A 使用事件派發(fā)器,派發(fā)一個(gè)領(lǐng)域事件,這個(gè)事件將會(huì)投遞給任何監(jiān)聽(tīng)它的組件,例如 B,然后 B 的事件監(jiān)聽(tīng)器會(huì)觸發(fā)期望的操作。這意味著組件 A 將依賴事件派發(fā)器,但和 B 解耦了。
然而,如果事件本身“活在” A 中,這將意味著 B 知道了 A 的存在,就和 A 存在耦合。要去掉這個(gè)依賴,我們可以創(chuàng)建一個(gè)包含應(yīng)用核心功能的庫(kù),由所有組件共享,這就是共享內(nèi)核。這意味著兩個(gè)組件都依賴共享內(nèi)核,而它們之間卻沒(méi)有耦合。共享內(nèi)核包含了應(yīng)用事件和領(lǐng)域事件這樣的功能,而且還包含規(guī)格對(duì)象,以及其它任何有理由共享的東西。記住共享內(nèi)核的范圍應(yīng)該盡可能的小,因?yàn)樗娜魏巫兓紩?huì)影響所有應(yīng)用組件。而且,如果我們的系統(tǒng)是語(yǔ)言異構(gòu)的,比如使用不同語(yǔ)言編寫的微服務(wù)生態(tài),共享內(nèi)核需要做到與語(yǔ)言無(wú)關(guān)的,這樣它才能被所有組件理解,無(wú)論它們是用哪種語(yǔ)言編寫的。例如,共享內(nèi)核應(yīng)該包含像 JSON 這樣無(wú)關(guān)語(yǔ)言的事件描述(例如,名稱、屬性,也許還有方法,盡管它們對(duì)規(guī)格對(duì)象來(lái)說(shuō)更有意義)而不是事件類,這樣所有組件/微服務(wù)都可以解析它,還可以自動(dòng)生成各自的具體實(shí)現(xiàn)。請(qǐng)?jiān)谖业南乱黄恼路N了解更多內(nèi)容:超越同心圓分層(譯)

這種方法既適用于單體應(yīng)用,也適用于像微服務(wù)生態(tài)系統(tǒng)這樣的分布式應(yīng)用。然而,這種方法只適用于事件異步投遞的情況,在需要即時(shí)完成觸發(fā)其它組件邏輯的上下文中并不適用!組件 A 將需要向組件 B 發(fā)起直接的 HTTP 調(diào)用。這種情況下,要解耦組件,我們需要一個(gè)發(fā)現(xiàn)服務(wù),A 可以詢問(wèn)它得知請(qǐng)求應(yīng)該發(fā)送到哪里才能觸發(fā)期望的操作,又或是向發(fā)現(xiàn)服務(wù)發(fā)起請(qǐng)求并由發(fā)現(xiàn)服務(wù)將請(qǐng)求代理給相關(guān)服務(wù)并最終返回響應(yīng)給請(qǐng)求方。這種方法會(huì)把組件和發(fā)現(xiàn)服務(wù)耦合在一起,但會(huì)讓組件之間解耦。
從其它組件獲得數(shù)據(jù)
我的看法是,組件不允許修改不“屬于”它的數(shù)據(jù),但可以查詢和使用任何數(shù)據(jù)。
組件之間共享的數(shù)據(jù)存儲(chǔ)
當(dāng)一個(gè)組件需要使用屬于其它組件的數(shù)據(jù)時(shí),比如說(shuō)賬單組件需要使用屬于賬戶組件的客戶名字,賬單組件會(huì)包含一個(gè)查詢對(duì)象,可以在數(shù)據(jù)存儲(chǔ)中查詢?cè)摂?shù)據(jù)。簡(jiǎn)單的說(shuō)就是賬單組件知道任何數(shù)據(jù)集,但它只能通過(guò)查詢只讀地使用不“屬于”它的數(shù)據(jù)。
按組件隔離的數(shù)據(jù)存儲(chǔ)
這種情況下,這種模式同樣有效,但數(shù)據(jù)存儲(chǔ)層面的復(fù)雜度更高。
組件擁有各自的數(shù)據(jù)存儲(chǔ)意味著每個(gè)數(shù)據(jù)存儲(chǔ)都包含:
- 一組屬于它的數(shù)據(jù),并且只允許它自己修改這些數(shù)據(jù),讓它成為單一事實(shí)來(lái)源;
- 一組其它組件數(shù)據(jù)的副本,它自己不能修改這些數(shù)據(jù),但組件的功能需要這些數(shù)據(jù),而且一旦數(shù)據(jù)在其所屬的組件中發(fā)生了變化,這些副本需要更新。
每個(gè)組件都會(huì)創(chuàng)建其所需的其它組件數(shù)據(jù)的本地副本,在必要時(shí)使用。當(dāng)數(shù)據(jù)在其所屬的組件中發(fā)生了變化,該組件將觸發(fā)一個(gè)攜帶數(shù)據(jù)變更的領(lǐng)域事件。擁有這些數(shù)據(jù)副本的組件將監(jiān)聽(tīng)這個(gè)領(lǐng)域事件并相應(yīng)地更新它們的本地副本。
控制流
如前所述,控制流顯然從用戶出發(fā),進(jìn)入應(yīng)用核心,抵達(dá)基礎(chǔ)設(shè)施工具,再返回應(yīng)用核心并最終返回給用戶。但這些類到底是是如何配合的?哪些類依賴哪些類?我們?cè)鯓影阉鼈兘M合在一起?
根據(jù) Uncle Bob 在他關(guān)于整潔架構(gòu)的文章中的說(shuō)法,我來(lái)試著用 UML 圖解釋控制流...
沒(méi)有命令/查詢總線
如果沒(méi)有命令總線,控制器要么依賴應(yīng)用服務(wù),要么依賴查詢對(duì)象。
[2017-11-18 編輯] 之前我完全忘記了查詢返回?cái)?shù)據(jù)中的 DTO,現(xiàn)在我把它加了回來(lái)。謝謝指出我這處錯(cuò)誤的MorphineAdministered。

上圖中我們使用了應(yīng)用服務(wù)接口,盡管我們會(huì)質(zhì)疑這并沒(méi)有必要。因?yàn)閼?yīng)用服務(wù)是我們應(yīng)用代碼的一部分,而且我們不會(huì)想用另外一種實(shí)現(xiàn)來(lái)替換它,盡管我們可能會(huì)徹底地重構(gòu)它。
查詢對(duì)象包含優(yōu)化過(guò)的查詢,簡(jiǎn)單地返回一些給用戶看的原始數(shù)據(jù)就好。這些數(shù)據(jù)將放在 DTO 中返回,并注入到 ViewModel。ViewModel 中可能有一些 View 邏輯,它被用來(lái)填充 View。
另一方面,應(yīng)用服務(wù)還包含用例邏輯,不是瀏覽數(shù)據(jù)這么簡(jiǎn)單,我們需要在系統(tǒng)中做一些事情時(shí)觸發(fā)這些邏輯。應(yīng)用服務(wù)依賴 Repository 返回實(shí)體,這些實(shí)體中包含著需要觸發(fā)的邏輯。它也可能依賴領(lǐng)域服務(wù)來(lái)整合多個(gè)實(shí)體來(lái)完成領(lǐng)域流程,但這種情況很少出現(xiàn)。
展開(kāi)用例之后,應(yīng)用服務(wù)可能想通知整個(gè)系統(tǒng),這個(gè)用例已經(jīng)發(fā)生了。這種情況下,它還要依賴事件派發(fā)器來(lái)觸發(fā)事件。
很有意思的是我們?cè)诔志没婧唾Y源庫(kù)之上都放上了接口。盡管看起來(lái)有些多余,但它們服務(wù)于不用的目標(biāo):
- 持久化接口是 ORM 之上的抽象層,這樣我們可以切換使用的 ORM 而不用修改應(yīng)用核心。
- 資源庫(kù)接口是持久化引擎自身的抽象。比方說(shuō)我們想要從 MySQL 切換為 MongoDB。如果我們想繼續(xù)使用同樣的 ORM,持久化接口可以保持不變,甚至持久化適配器也可以保持不變。然而,兩者的查詢語(yǔ)言完全不同,所以,我們可以創(chuàng)建使用同樣持久化機(jī)制的新資源庫(kù),可以實(shí)現(xiàn)相同的資源庫(kù)接口,但使用 MongoDB 查詢語(yǔ)言而不是 SQL 來(lái)構(gòu)建查詢。
有命令/查詢總線
如果我們的應(yīng)用使用了命令/查詢總線,UML 圖基本沒(méi)有變化,唯一的區(qū)別是控制器現(xiàn)在會(huì)依賴總線、命令或查詢。它將實(shí)例化命令或查詢,將它們傳遞給總線。總線會(huì)找到合適的處理程序接收并處理命令。
在下圖中,命令處理程序接下來(lái)將使用應(yīng)用服務(wù)。然而,這不總是必須的,實(shí)際上大多數(shù)情況下,處理程序?qū)美乃羞壿嫛V挥性谄渌幚沓绦蛐枰赜猛瑯拥倪壿嫊r(shí),我們才需要把處理程序中的邏輯提取出來(lái)放到單獨(dú)的應(yīng)用服務(wù)中。
[2017-11-18 編輯] 之前我完全忘記了查詢返回?cái)?shù)據(jù)中的 DTO,現(xiàn)在我把它加了回來(lái)。謝謝指出我這處錯(cuò)誤的MorphineAdministered。

你可能注意到了,總線和命令查詢,以及處理程序之間沒(méi)有依賴。這是因?yàn)閷?shí)際上它們之間應(yīng)該互相無(wú)感知,才能提供足夠的解耦。只有通過(guò)配置才能設(shè)置總線可以發(fā)現(xiàn)哪些命令,或者查詢應(yīng)該由哪個(gè)處理程序處理。
如你所見(jiàn),兩種情況下,所有跨越應(yīng)用核心邊界的箭頭——依賴——都指向內(nèi)部。如前所述,這是端口和適配器架構(gòu)、洋蔥架構(gòu)以及整潔架構(gòu)的基本規(guī)則。

總結(jié)
一如既往,這些架構(gòu)的目標(biāo)是得到高內(nèi)聚低耦合的代碼庫(kù),這樣變化才會(huì)簡(jiǎn)單、快速和安全。
計(jì)劃不名一文,但制訂計(jì)劃的過(guò)程至關(guān)重要?!劳?/p>
這份信息圖是一份概念地圖。了解并理解所有這些概念將幫助我們規(guī)劃出健康的架構(gòu)和應(yīng)用。
不過(guò):
地圖并非疆域?!柛ダ椎隆た氯詹妓够?/p>
這只是一份指南!應(yīng)用才是你的疆域,現(xiàn)實(shí)情況和具體用例才是運(yùn)用這些知識(shí)的地方,它們才能勾勒出實(shí)際架構(gòu)的輪廓!
我們需要理解所有這些模式,但我們還時(shí)常需要思考和理解我們的應(yīng)用需要什么,我們應(yīng)該在追求解耦和內(nèi)聚的道路上走多遠(yuǎn)。這個(gè)決定可能受到許多因素的影響,包括項(xiàng)目的功能需求,也包括構(gòu)建應(yīng)用的時(shí)間期限,應(yīng)用壽命,開(kāi)發(fā)團(tuán)隊(duì)的體驗(yàn)等等因素。
就到這里,這是我對(duì)一切的理解。這就是我腦海中對(duì)這一切的梳理。
在下篇文章中我將更深入地展開(kāi)這些話題: 超越同心圓分層(譯)。
然而,我們?nèi)绾螌⑦@些全部展現(xiàn)在代碼庫(kù)中呢?這是再下一篇文章的主題,我如何將架構(gòu)和領(lǐng)域反映在代碼之中。
最后,感謝我的同事Francesco Mastrogiacomo,幫助我制作了漂亮的信息圖。
譯者:我將這份信息圖翻譯成了中文版



