這篇文章是軟件架構(gòu)編年史(譯)的一部分没龙,這部編年史由一系列關(guān)于軟件架構(gòu)的文章組成铺厨。在這一系列文章中,我將寫下我對軟件架構(gòu)的學(xué)習(xí)和思考硬纤,以及我是如何運(yùn)用這些知識的解滓。如果你閱讀了這個系列中之前的文章,本篇文章的的內(nèi)容將更有意義筝家。
大學(xué)畢業(yè)之后我做了一名高中老師洼裤,直到幾年前我決定成為一名全職軟件開發(fā)者。
從那時起肛鹏,我時常覺得我必須找回“失去的”時間逸邦,盡力地多學(xué)一點(diǎn)恩沛,學(xué)快一點(diǎn)。所以我變得有一點(diǎn)沉迷于實(shí)驗(yàn)缕减、閱讀和寫作雷客,特別關(guān)注的就是軟件設(shè)計(jì)和架構(gòu)。幫助我學(xué)習(xí)正是我寫下這些文章的初衷桥狡。
在之前的文章中搅裙,我記錄了許多學(xué)到的概念和原則和我的一些思考。但我知道這只是管中窺豹裹芝。
今天的文章內(nèi)容是我如何將這些碎片融合成一種新的架構(gòu)部逮,看起來我得給它起個名字,就叫做清晰架構(gòu)吧嫂易。而且兄朋,這些概念全都經(jīng)歷過“炮火的洗禮”,在高水準(zhǔn)的平臺生產(chǎn)代碼中得到了應(yīng)用怜械。其中一個是擁有數(shù)千家遍布全球的網(wǎng)上商店的 SaaS 電子商務(wù)平臺颅和,另一個是已經(jīng)在兩個國家上線的市場,它擁有可以每月處理超過兩千萬條消息的消息總線缕允。
系統(tǒng)的基本構(gòu)建塊
我們從 EBI 架構(gòu)以及端口和適配器架構(gòu)(譯)的回顧開始峡扩。它們都有清晰的代碼劃分,哪些代碼在應(yīng)用內(nèi)部障本,哪些代碼在外部教届,而哪些代碼用來連接它們。
除此之外驾霜,端口和適配器架構(gòu)還明確地識別出了一個系統(tǒng)中的三個基本代碼構(gòu)建塊:
- 運(yùn)行用戶界面所需的構(gòu)建塊案训,無論是哪種用戶界面;
- 系統(tǒng)的業(yè)務(wù)邏輯寄悯,或者應(yīng)用核心萤衰,用戶界面要使用這個構(gòu)建塊達(dá)成目的堕义;
- 基礎(chǔ)設(shè)施代碼猜旬,這個構(gòu)建塊將我們的應(yīng)用核心和諸如數(shù)據(jù)庫、搜索引擎或第三方 API 這樣的工具連接起來倦卖。
我們真正關(guān)心的應(yīng)該是應(yīng)用核心洒擦。這部分代碼才是我們編寫代碼的目的,它們才是我們的應(yīng)用怕膛。它們可能使用一些不同的用戶界面(漸進(jìn)式 Web 應(yīng)用熟嫩,移動應(yīng)用、命令行褐捻、接口...)掸茅,但完成實(shí)際工作的代碼是一模一樣的椅邓,它們就在應(yīng)用核心內(nèi)部,它們不用關(guān)心是哪種用戶界面觸發(fā)了它們昧狮。
你可以想像得到景馁,典型的應(yīng)用控制流開始于用戶界面中的代碼,經(jīng)過應(yīng)用核心到達(dá)基礎(chǔ)設(shè)施代碼逗鸣,又返回應(yīng)用核心合住,最后將響應(yīng)傳達(dá)給用戶界面。
工具
在遠(yuǎn)離我們系統(tǒng)中最重要的代碼-——應(yīng)用核心-——的地方撒璧,還有一些應(yīng)用會用到的工具透葛,例如數(shù)據(jù)庫引擎、搜索引擎卿樱、Web 服務(wù)器或者命令行控制臺(雖然最后兩種工具也是傳達(dá)機(jī)制)僚害。
把命令行控制臺和數(shù)據(jù)庫引擎放在同一個“籃子”中感覺有點(diǎn)奇怪,盡管它們有著不用的用途繁调,但它們實(shí)際都是應(yīng)用使用的工具贡珊。關(guān)鍵的區(qū)別在于,命令行控制臺和 Web 服務(wù)器告訴我們的應(yīng)用它要做什么涉馁,而數(shù)據(jù)庫引擎是由我們的應(yīng)用來告訴它做什么门岔。這是針鋒相對的差別,強(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)該做什么的適配器被稱為主適配器或主動適配器帮坚,而那些由我們的應(yīng)用告知它該做什么的適配器被稱為從適配器或者被動適配器妻往。
端口
而這些適配器并非是隨意創(chuàng)建的。它們需要按照應(yīng)用核心某個特定的入口的要求來創(chuàng)建试和,即端口讯泣。端口無外乎是一份工具如何使用應(yīng)用核心或者如何被應(yīng)用核心使用的說明書。這份說明書阅悍,即端口好渠,在大多數(shù)語言里最簡單的形式就是接口,但實(shí)際上也可能由多個接口和 DTO 組成节视。
端口(接口)位于業(yè)務(wù)邏輯內(nèi)部拳锚,而適配器位于其外部,這一點(diǎn)要特別注意寻行。要讓這種模式按照設(shè)想發(fā)揮作用霍掺,端口按照應(yīng)用核心的需要來設(shè)計(jì)而不是簡單地套用工具的 API,這一點(diǎn)再怎么強(qiáng)調(diào)都不為過。
主適配器或主動適配器
主適配器或主動適配器包裝端口并通過它告知應(yīng)用核心應(yīng)該做什么杆烁。它們將來自傳達(dá)機(jī)制的信息轉(zhuǎn)換成對應(yīng)用核心的方法調(diào)用牙丽。
換句話說,我們的主動適配器就是 Controller 或者控制臺命令兔魂,它們需要的接口(端口)由其他類實(shí)現(xiàn)剩岳,這些類的對象通過構(gòu)造方法注入到 Controller 或者控制臺命令。
再舉一個更具體的例子入热,端口就是 Controller 需要的 Service 接口或者 Repository 接口拍棕。Service、Repository 或 Query 的具體實(shí)現(xiàn)被注入到 Controller 供 Controller 使用勺良。
此外绰播,端口還可以是命令總線接口或者查詢總線接口。這種情況下尚困,命令總線或者查詢總線的具體實(shí)現(xiàn)將被注入到 Controller 中蠢箩, Controller 將創(chuàng)建命令或查詢并傳遞給相應(yīng)的總線。
從適配器或被動適配器
和主動適配器包裝端口不同事甜,被動適配器實(shí)現(xiàn)一個端口(接口)并被注入到需要這個端口的應(yīng)用核心里谬泌。
舉個例子袋狞,假設(shè)有一個需要存儲數(shù)據(jù)的簡單應(yīng)用宁舰。我們創(chuàng)建了一個符合應(yīng)用要求的持久化接口,這個接口有一個保存數(shù)據(jù)數(shù)組的方法和一個根據(jù) ID 從表中刪除一行的方法听想。接口創(chuàng)建好之后邦马,無論何時應(yīng)用需要保存或刪除數(shù)據(jù)贱鼻,都應(yīng)該使用實(shí)現(xiàn)了這個持久化接口的對象,而這個對象是通過構(gòu)造方法注入的滋将。
現(xiàn)在我們創(chuàng)建了一個專門針對 MySQL 實(shí)現(xiàn)了該接口的適配器邻悬。它擁有保存數(shù)組和刪除表中一行數(shù)據(jù)的方法,然后在需要使用持久化接口的地方注入它随闽。
如果未來我們決定更換數(shù)據(jù)庫供應(yīng)商父丰,比如換成 PostgreSQL 或者 MongoDB,我們只用創(chuàng)建一個專門針對 PostgreSQL 實(shí)現(xiàn)了該接口的適配器掘宪,在注入時用新適配器代替舊適配器蛾扇。
控制反轉(zhuǎn)
這種模式有一個特征值得留意,適配器依賴特定的工具和特定的端口(它需要提供接口的特定實(shí)現(xiàn))添诉。但業(yè)務(wù)邏輯只依賴按照它的需求設(shè)計(jì)的端口(接口)屁桑,它并不依賴特定的適配器或工具医寿。
這意味著依賴的方向是由外向內(nèi)的栏赴,這就是架構(gòu)層面的控制反轉(zhuǎn)原則。
再一次強(qiáng)調(diào)靖秩,端口按照應(yīng)用核心的需要來設(shè)計(jì)而不是簡單地套用工具的 API须眷。
組織應(yīng)用核心的結(jié)構(gòu)
洋蔥架構(gòu)采用了 DDD 的分層竖瘾,將它們?nèi)诤线M(jìn)了端口和適配器架構(gòu)。這種分層想要為位于端口和適配器架構(gòu)“六邊形”內(nèi)的業(yè)務(wù)邏輯帶來一種結(jié)構(gòu)組織花颗,和端口與適配器架構(gòu)一樣捕传,依賴的方向也是由外向內(nèi)。
應(yīng)用層
在應(yīng)用中扩劝,由一個或多個用戶界面觸發(fā)的應(yīng)用核心中的過程就是用例庸论。例如,在一個 CMS 系統(tǒng)中棒呛,我們可以提供普通用戶使用的應(yīng)用 UI聂示、CMS 管理員使用的獨(dú)立的 UI、命令行 UI 以及 Web API簇秒。這些 UI(應(yīng)用)可以觸發(fā)的用例可能是專門為它設(shè)計(jì)的鱼喉,也可以是多個 UI 復(fù)用的。
用例定義在應(yīng)用層中趋观,這是 DDD 提供的第一個被洋蔥架構(gòu)使用的層扛禽。
這個層包括了作為一等公民的應(yīng)用服務(wù)(以及它們的接口),也包括了端口與適配器架構(gòu)中的接口皱坛,例如 ORM 接口编曼、搜索引擎接口、消息接口等等剩辟。如果我們使用了命令總線和/或查詢總線灵巧,命令和查詢分別對應(yīng)的處理程序也屬于這一層。
應(yīng)用服務(wù)和/或命令處理程序包含了展現(xiàn)一個用例抹沪,一個業(yè)務(wù)過程的邏輯刻肄。通常,它們的作用是:
- 使用 Repostitory 查找一個或多個實(shí)體融欧;
- 讓這些實(shí)體執(zhí)行一些領(lǐng)域邏輯敏弃;
- 再次使用 Repostitory 讓這些實(shí)體持久化,有效地保存數(shù)據(jù)變化噪馏。
命令處理程序有兩種不同使用方式:
- 它們可以包含執(zhí)行用例的實(shí)際邏輯麦到;
- 它們可以僅僅作為我們應(yīng)用中的連接片段,接收命令然后簡單地觸發(fā)應(yīng)用服務(wù)中的邏輯欠肾。
使用哪種方式是由上下文決定的瓶颠,例如:
- 我們已經(jīng)有了合適的應(yīng)用服務(wù),現(xiàn)在要做的是添加命令總線刺桃?
- 命令總線允許指定任意類/方法作為處理程序嗎粹淋?還是說它們需要擴(kuò)展已有的類或者實(shí)現(xiàn)已有的接口?
應(yīng)用層還包括應(yīng)用事件的觸發(fā),這也代表著某些用例的產(chǎn)出桃移。這些事件觸發(fā)的邏輯是用例的副作用屋匕,比如發(fā)送郵件、通知第三方 PAI借杰、發(fā)送推送通知过吻,或是發(fā)起屬于其他應(yīng)用組件的另一個用例。
領(lǐng)域?qū)?/h2>
繼續(xù)向內(nèi)一層就是領(lǐng)域?qū)诱岷狻_@一層中的對象包含了數(shù)據(jù)和操作數(shù)據(jù)的邏輯纤虽,它們只和領(lǐng)域本身有關(guān),獨(dú)立于調(diào)用這些邏輯的業(yè)務(wù)過程绞惦。它們完全獨(dú)立廓推,對應(yīng)用層完全無感知。
領(lǐng)域服務(wù)
如前所述翩隧,應(yīng)用服務(wù)的作用是:
- 使用 Repostitory 查找一個或多個實(shí)體樊展;
- 讓這些實(shí)體執(zhí)行一些領(lǐng)域邏輯;
- 再次使用 Repostitory 讓這些實(shí)體持久化堆生,有效地保存數(shù)據(jù)變化专缠。
然而,有時我們還會碰到某種領(lǐng)域邏輯淑仆,它涉及不同的實(shí)體涝婉。這些實(shí)體也許是同一個類型,也許不是蔗怠,而且我們覺得這種領(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í)體并對它們執(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)域模型對象杨何。
領(lǐng)域模型
在架構(gòu)的正中心,是完全不依賴外部任何層次的領(lǐng)域模型悔政。它包含了那些表示領(lǐng)域中某個概念的業(yè)務(wù)對象晚吞。這些對象的例子首先就是實(shí)體延旧,還有值對象谋国、枚舉以及其它領(lǐng)域模型種用到的任何對象。
領(lǐng)域事件也“活在”領(lǐng)域模型中迁沫。當(dāng)一組特定的數(shù)據(jù)發(fā)生變化時就會觸發(fā)這些事件芦瘾,而這些時間會攜帶這些變化的信息。換句話說集畅,當(dāng)實(shí)體變化時近弟,就會觸發(fā)一個領(lǐng)域事件,它攜帶著發(fā)生變化的屬性的新值挺智。這些事件可以完美地應(yīng)用于事件溯源祷愉。
組件
目前為止,我們都是使用層次來劃分代碼赦颇,但這是細(xì)粒度的代碼隔離二鳄。根據(jù) Robert C. Martin 在尖叫架構(gòu)中表達(dá)的觀點(diǎn),按照子域和限界上下文對代碼進(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)脖苏、賬單、用戶定踱、評論或帳號帆阳,而它們總是都和領(lǐng)域相關(guān)。像認(rèn)證和/或授權(quán)這樣的限界上下文應(yīng)該被看作外部工具屋吨,我們應(yīng)該為它們創(chuàng)建適配器蜒谤,把它們隱藏在某個端口之后。
組件解耦
和細(xì)粒度的代碼單元(類至扰、接口鳍徽、特質(zhì)、混合等等)一樣敢课,粗粒度的代碼單元(組件)也會從高內(nèi)聚低耦合中受益阶祭。
我們使用了依賴注入绷杜,通過將依賴注入類而不是在類內(nèi)部初始化依賴;以及依賴倒置濒募;讓類依賴抽象(接口和/或抽象類)而不是具體類來解耦類鞭盟。這意味著類不用知道它要使用的具體類的任何信息,不用引用所依賴的類的完全限定類名瑰剃。
以同樣的方式完全解耦的組件意味著組件不會直接了解其它任何組件的信息齿诉。換句話說,它不會引用任何來自其它組件的細(xì)粒度的代碼單元晌姚,甚至都不會引用接口粤剧!這意味著依賴注入和依賴倒置對組件解耦是不夠用的,我們還需要一些架構(gòu)層級的結(jié)構(gòu)挥唠。我們需要事件抵恋、共享內(nèi)核、最終一致性甚至發(fā)現(xiàn)服務(wù)宝磨!
觸發(fā)其它組件的邏輯
當(dāng)一個組件(組件 A)中有事情發(fā)生需要另一個組件(組件B)做些什么時弧关,我們不能簡單地從組件 A 直接調(diào)用組件 B 中的類/方法,因?yàn)檫@樣 A 就和 B 耦合在一起了唤锉。
但是我們可以讓 A 使用事件派發(fā)器世囊,派發(fā)一個領(lǐng)域事件,這個事件將會投遞給任何監(jiān)聽它的組件腌紧,例如 B茸习,然后 B 的事件監(jiān)聽器會觸發(fā)期望的操作。這意味著組件 A 將依賴事件派發(fā)器壁肋,但和 B 解耦了号胚。
然而,如果事件本身“活在” A 中浸遗,這將意味著 B 知道了 A 的存在猫胁,就和 A 存在耦合。要去掉這個依賴跛锌,我們可以創(chuàng)建一個包含應(yīng)用核心功能的庫弃秆,由所有組件共享,這就是共享內(nèi)核髓帽。這意味著兩個組件都依賴共享內(nèi)核菠赚,而它們之間卻沒有耦合。共享內(nèi)核包含了應(yīng)用事件和領(lǐng)域事件這樣的功能郑藏,而且還包含規(guī)格對象衡查,以及其它任何有理由共享的東西。記住共享內(nèi)核的范圍應(yīng)該盡可能的小必盖,因?yàn)樗娜魏巫兓紩绊懰袘?yīng)用組件拌牲。而且俱饿,如果我們的系統(tǒng)是語言異構(gòu)的,比如使用不同語言編寫的微服務(wù)生態(tài)塌忽,共享內(nèi)核需要做到與語言無關(guān)的拍埠,這樣它才能被所有組件理解,無論它們是用哪種語言編寫的土居。例如枣购,共享內(nèi)核應(yīng)該包含像 JSON 這樣無關(guān)語言的事件描述(例如,名稱装盯、屬性坷虑,也許還有方法甲馋,盡管它們對規(guī)格對象來說更有意義)而不是事件類埂奈,這樣所有組件/微服務(wù)都可以解析它,還可以自動生成各自的具體實(shí)現(xiàn)定躏。請?jiān)谖业南乱黄恼路N了解更多內(nèi)容:超越同心圓分層(譯)
這種方法既適用于單體應(yīng)用账磺,也適用于像微服務(wù)生態(tài)系統(tǒng)這樣的分布式應(yīng)用。然而痊远,這種方法只適用于事件異步投遞的情況垮抗,在需要即時完成觸發(fā)其它組件邏輯的上下文中并不適用!組件 A 將需要向組件 B 發(fā)起直接的 HTTP 調(diào)用碧聪。這種情況下冒版,要解耦組件,我們需要一個發(fā)現(xiàn)服務(wù)逞姿,A 可以詢問它得知請求應(yīng)該發(fā)送到哪里才能觸發(fā)期望的操作辞嗡,又或是向發(fā)現(xiàn)服務(wù)發(fā)起請求并由發(fā)現(xiàn)服務(wù)將請求代理給相關(guān)服務(wù)并最終返回響應(yīng)給請求方。這種方法會把組件和發(fā)現(xiàn)服務(wù)耦合在一起滞造,但會讓組件之間解耦续室。
從其它組件獲得數(shù)據(jù)
我的看法是,組件不允許修改不“屬于”它的數(shù)據(jù)谒养,但可以查詢和使用任何數(shù)據(jù)挺狰。
組件之間共享的數(shù)據(jù)存儲
當(dāng)一個組件需要使用屬于其它組件的數(shù)據(jù)時,比如說賬單組件需要使用屬于賬戶組件的客戶名字买窟,賬單組件會包含一個查詢對象丰泊,可以在數(shù)據(jù)存儲中查詢該數(shù)據(jù)。簡單的說就是賬單組件知道任何數(shù)據(jù)集始绍,但它只能通過查詢只讀地使用不“屬于”它的數(shù)據(jù)瞳购。
按組件隔離的數(shù)據(jù)存儲
這種情況下,這種模式同樣有效疆虚,但數(shù)據(jù)存儲層面的復(fù)雜度更高苛败。
組件擁有各自的數(shù)據(jù)存儲意味著每個數(shù)據(jù)存儲都包含:
- 一組屬于它的數(shù)據(jù)满葛,并且只允許它自己修改這些數(shù)據(jù),讓它成為單一事實(shí)來源罢屈;
- 一組其它組件數(shù)據(jù)的副本嘀韧,它自己不能修改這些數(shù)據(jù),但組件的功能需要這些數(shù)據(jù)缠捌,而且一旦數(shù)據(jù)在其所屬的組件中發(fā)生了變化锄贷,這些副本需要更新。
每個組件都會創(chuàng)建其所需的其它組件數(shù)據(jù)的本地副本曼月,在必要時使用谊却。當(dāng)數(shù)據(jù)在其所屬的組件中發(fā)生了變化,該組件將觸發(fā)一個攜帶數(shù)據(jù)變更的領(lǐng)域事件哑芹。擁有這些數(shù)據(jù)副本的組件將監(jiān)聽這個領(lǐng)域事件并相應(yīng)地更新它們的本地副本炎辨。
控制流
如前所述,控制流顯然從用戶出發(fā)聪姿,進(jìn)入應(yīng)用核心碴萧,抵達(dá)基礎(chǔ)設(shè)施工具,再返回應(yīng)用核心并最終返回給用戶末购。但這些類到底是是如何配合的破喻?哪些類依賴哪些類?我們怎樣把它們組合在一起盟榴?
根據(jù) Uncle Bob 在他關(guān)于整潔架構(gòu)的文章中的說法曹质,我來試著用 UML 圖解釋控制流...
沒有命令/查詢總線
如果沒有命令總線,控制器要么依賴應(yīng)用服務(wù)擎场,要么依賴查詢對象羽德。
[2017-11-18 編輯] 之前我完全忘記了查詢返回數(shù)據(jù)中的 DTO,現(xiàn)在我把它加了回來顶籽。謝謝指出我這處錯誤的MorphineAdministered玩般。
上圖中我們使用了應(yīng)用服務(wù)接口,盡管我們會質(zhì)疑這并沒有必要礼饱。因?yàn)閼?yīng)用服務(wù)是我們應(yīng)用代碼的一部分坏为,而且我們不會想用另外一種實(shí)現(xiàn)來替換它,盡管我們可能會徹底地重構(gòu)它镊绪。
查詢對象包含優(yōu)化過的查詢匀伏,簡單地返回一些給用戶看的原始數(shù)據(jù)就好。這些數(shù)據(jù)將放在 DTO 中返回蝴韭,并注入到 ViewModel够颠。ViewModel 中可能有一些 View 邏輯,它被用來填充 View榄鉴。
另一方面履磨,應(yīng)用服務(wù)還包含用例邏輯蛉抓,不是瀏覽數(shù)據(jù)這么簡單,我們需要在系統(tǒng)中做一些事情時觸發(fā)這些邏輯剃诅。應(yīng)用服務(wù)依賴 Repository 返回實(shí)體巷送,這些實(shí)體中包含著需要觸發(fā)的邏輯。它也可能依賴領(lǐng)域服務(wù)來整合多個實(shí)體來完成領(lǐng)域流程矛辕,但這種情況很少出現(xiàn)笑跛。
展開用例之后,應(yīng)用服務(wù)可能想通知整個系統(tǒng)聊品,這個用例已經(jīng)發(fā)生了飞蹂。這種情況下,它還要依賴事件派發(fā)器來觸發(fā)事件翻屈。
很有意思的是我們在持久化引擎和資源庫之上都放上了接口陈哑。盡管看起來有些多余,但它們服務(wù)于不用的目標(biāo):
- 持久化接口是 ORM 之上的抽象層妖胀,這樣我們可以切換使用的 ORM 而不用修改應(yīng)用核心芥颈。
- 資源庫接口是持久化引擎自身的抽象惠勒。比方說我們想要從 MySQL 切換為 MongoDB赚抡。如果我們想繼續(xù)使用同樣的 ORM,持久化接口可以保持不變纠屋,甚至持久化適配器也可以保持不變涂臣。然而,兩者的查詢語言完全不同售担,所以赁遗,我們可以創(chuàng)建使用同樣持久化機(jī)制的新資源庫,可以實(shí)現(xiàn)相同的資源庫接口族铆,但使用 MongoDB 查詢語言而不是 SQL 來構(gòu)建查詢岩四。
有命令/查詢總線
如果我們的應(yīng)用使用了命令/查詢總線,UML 圖基本沒有變化哥攘,唯一的區(qū)別是控制器現(xiàn)在會依賴總線剖煌、命令或查詢。它將實(shí)例化命令或查詢逝淹,將它們傳遞給總線耕姊。總線會找到合適的處理程序接收并處理命令栅葡。
在下圖中茉兰,命令處理程序接下來將使用應(yīng)用服務(wù)。然而欣簇,這不總是必須的规脸,實(shí)際上大多數(shù)情況下坯约,處理程序?qū)美乃羞壿嫛V挥性谄渌幚沓绦蛐枰赜猛瑯拥倪壿嫊r莫鸭,我們才需要把處理程序中的邏輯提取出來放到單獨(dú)的應(yīng)用服務(wù)中鬼店。
[2017-11-18 編輯] 之前我完全忘記了查詢返回數(shù)據(jù)中的 DTO,現(xiàn)在我把它加了回來黔龟。謝謝指出我這處錯誤的MorphineAdministered妇智。
你可能注意到了,總線和命令查詢氏身,以及處理程序之間沒有依賴巍棱。這是因?yàn)閷?shí)際上它們之間應(yīng)該互相無感知,才能提供足夠的解耦蛋欣。只有通過配置才能設(shè)置總線可以發(fā)現(xiàn)哪些命令航徙,或者查詢應(yīng)該由哪個處理程序處理。
如你所見陷虎,兩種情況下到踏,所有跨越應(yīng)用核心邊界的箭頭——依賴——都指向內(nèi)部。如前所述尚猿,這是端口和適配器架構(gòu)窝稿、洋蔥架構(gòu)以及整潔架構(gòu)的基本規(guī)則。
總結(jié)
一如既往凿掂,這些架構(gòu)的目標(biāo)是得到高內(nèi)聚低耦合的代碼庫伴榔,這樣變化才會簡單、快速和安全庄萎。
計(jì)劃不名一文踪少,但制訂計(jì)劃的過程至關(guān)重要】诽危——艾森豪威爾
這份信息圖是一份概念地圖援奢。了解并理解所有這些概念將幫助我們規(guī)劃出健康的架構(gòu)和應(yīng)用。
不過:
地圖并非疆域忍捡〖——阿爾弗雷德·柯日布斯基
這只是一份指南!應(yīng)用才是你的疆域锉罐,現(xiàn)實(shí)情況和具體用例才是運(yùn)用這些知識的地方帆竹,它們才能勾勒出實(shí)際架構(gòu)的輪廓!
我們需要理解所有這些模式脓规,但我們還時常需要思考和理解我們的應(yīng)用需要什么栽连,我們應(yīng)該在追求解耦和內(nèi)聚的道路上走多遠(yuǎn)。這個決定可能受到許多因素的影響,包括項(xiàng)目的功能需求秒紧,也包括構(gòu)建應(yīng)用的時間期限绢陌,應(yīng)用壽命,開發(fā)團(tuán)隊(duì)的體驗(yàn)等等因素熔恢。
就到這里脐湾,這是我對一切的理解。這就是我腦海中對這一切的梳理叙淌。
在下篇文章中我將更深入地展開這些話題: 超越同心圓分層(譯)秤掌。
然而,我們?nèi)绾螌⑦@些全部展現(xiàn)在代碼庫中呢鹰霍?這是再下一篇文章的主題闻鉴,我如何將架構(gòu)和領(lǐng)域反映在代碼之中。
最后茂洒,感謝我的同事Francesco Mastrogiacomo孟岛,幫助我制作了漂亮的信息圖。
譯者:我將這份信息圖翻譯成了中文版