你想成為一名架構(gòu)師,對(duì)嗎?別對(duì)我撒謊,我知道你想成為架構(gòu)師。即使你不想,你還是想成為一名更好的開發(fā)者。否則,你就不會(huì)花時(shí)間閱讀這篇文章
這種態(tài)度值得贊賞。畢竟,我們都希望在自己所從事的領(lǐng)域變得更好,即使不能稱為最好。我在這里就是為了幫助你實(shí)現(xiàn)這一目標(biāo)。
那么,你如何成為一名架構(gòu)師呢?當(dāng)然是通過學(xué)習(xí)所有的架構(gòu)!顯然這不現(xiàn)實(shí)。你不需要知道所有的架構(gòu)。你也不需要對(duì)所有的架構(gòu)都有經(jīng)驗(yàn)。但是,至少了解最流行的幾種架構(gòu) ,比如 N-Layered、DDD、Hexagon、Onion 和 Clean 架構(gòu);了解它們的歷史、用途以及它們之間的區(qū)別,無疑會(huì)讓你在與其他開發(fā)者的比較中脫穎而出。
希望你感興趣,讓我們開始吧。
一切始于何處?
回到那些美好的過去,根本沒有架構(gòu)的概念。那是多么幸福的日子啊,你只需要了解 GoF 設(shè)計(jì)模式,就能自稱為架構(gòu)師。
然而,隨著計(jì)算機(jī)變得更加強(qiáng)大,用戶的需求也增加,導(dǎo)致應(yīng)用程序的復(fù)雜性不斷增加。
開發(fā)人員首先解決的問題是將用戶界面與業(yè)務(wù)邏輯分離。 根據(jù)不同的用戶界面框架,出現(xiàn)了各種類似 MVC 的模式:
這雖然有效,但效果不是很好。如果你和我一樣來自 C# 社區(qū),你可能錯(cuò)誤地認(rèn)為那些圖表上稱為 “Model” 的黃色方框只是 DTO(數(shù)據(jù)傳輸對(duì)象)。這完全是因?yàn)槲④浀腻e(cuò)。他們用 ASP MVC 框架把我們搞糊涂了??蓯旱奈④洠?/p>
實(shí)際上,在這里,“Model” 代表的是領(lǐng)域模型,也就是業(yè)務(wù)邏輯,在任何應(yīng)用程序中都非常關(guān)鍵。
你能猜到上述三個(gè)組件中哪個(gè)引起的問題最多嗎?視圖只是簡(jiǎn)單的圖像和按鈕,控制器充當(dāng)中間人,而所有的復(fù)雜性都集中在模型中。
那個(gè)時(shí)期,GoF 設(shè)計(jì)模式已經(jīng)不夠用。因此,新的想法必須出現(xiàn)。我們?nèi)绾翁幚韽?fù)雜性呢?沒錯(cuò)!分而治之。 我們已經(jīng)在 MVC 中這樣做過了,所以讓我們?cè)俅芜@樣做。
2002 年:N-Layered(N 層架構(gòu))
理想的架構(gòu)并非一蹴而就。就像所有事物一樣,它是通過嘗試和錯(cuò)誤發(fā)展而來的。
那位開創(chuàng)軟件開發(fā)架構(gòu)并對(duì)接下來的幾代開發(fā)者產(chǎn)生影響的人叫 Martin Fowler。他的觀點(diǎn)是:
于是他們開始行動(dòng)。
他發(fā)表了《企業(yè)應(yīng)用架構(gòu)模式》一書,其中描述了 N 層架構(gòu)。
這個(gè)想法很簡(jiǎn)單,就是將所有相關(guān)的代碼分組并將其稱為不同的層 。
但是,還有更多的事情要做。Fowler 知道不一致的危害有多大。因此,為了避免我們自己給自己惹麻煩,他試圖給出一些限制和指導(dǎo):
你可以按照自己的方式為各個(gè)層命名。
你可以根據(jù)需要設(shè)置任意多的層。
你可以在層之間添加新的層。
你可以在同一層中擁有多個(gè)組件。
只需確保層之間存在明確的層級(jí)關(guān)系,以便它們按順序相互引用。
這些規(guī)則不僅幫助開發(fā)人員擺脫了代碼重復(fù),而且最終能幫助他們構(gòu)建代碼。
盡管這些規(guī)則相當(dāng)靈活,但在實(shí)踐中,對(duì)于大多數(shù)項(xiàng)目來說,3 個(gè)層已經(jīng)足夠了。
用戶界面(UI):負(fù)責(zé)與用戶進(jìn)行交互。
業(yè)務(wù)邏輯層(BLL):表示業(yè)務(wù)概念。它定義了應(yīng)用程序的行為,使其與其他應(yīng)用程序獨(dú)特區(qū)別開來。
數(shù)據(jù)訪問層(DAL):在內(nèi)存中持久化數(shù)據(jù)并維護(hù)應(yīng)用程序的狀態(tài)。
在這里,我們明確將業(yè)務(wù)邏輯與用戶界面分離開來。數(shù)據(jù)庫(kù)的重要性與業(yè)務(wù)規(guī)則相當(dāng),因此它有自己的層次。實(shí)際上,所有外部技術(shù)也可以放在這最后一層。一切都按照書中所說的進(jìn)行。
如果你對(duì)那些彩色矩形和箭頭的意義感到困惑,不用擔(dān)心,很簡(jiǎn)單。這些層只是解決方案中的項(xiàng)目,箭頭表示它們之間的依賴關(guān)系。
分離并不一定要通過項(xiàng)目進(jìn)行物理上的分離,而可以通過文件夾進(jìn)行邏輯上的分離。你也可以將兩種方法結(jié)合起來,使用最適合你的方式。
文件夾和項(xiàng)目之間的區(qū)別很大。 實(shí)際上,項(xiàng)目能夠幫助你更好地控制依賴關(guān)系。而使用文件夾時(shí),你可能甚至不會(huì)意識(shí)到某個(gè)層開始使用另一個(gè)層的組件。另一方面,如果項(xiàng)目過多,代碼會(huì)變得更加脆弱和難以維護(hù)。
請(qǐng)記住,這并沒有嚴(yán)格的規(guī)定。你可以根據(jù)實(shí)際情況選擇適合你的方式。這總是一個(gè)可靠性和復(fù)雜性之間的權(quán)衡。我在這里的建議是,除非確實(shí)需要,不要?jiǎng)?chuàng)建過多的項(xiàng)目 ,一個(gè)項(xiàng)目對(duì)應(yīng)一個(gè)層已經(jīng)足夠了。
每個(gè)層通過其 API 調(diào)用下一層,通常以接口的形式表示。每個(gè)類的訪問修飾符和這些層同樣重要:
現(xiàn)在這對(duì)你來說可能顯而易見,但那只是因?yàn)槟銢]有經(jīng)歷過真正困難的時(shí)刻。使用總是容易的,發(fā)明卻很難。
2003 年:DDD(領(lǐng)域驅(qū)動(dòng)設(shè)計(jì))
在 2003 年,來自波士頓的一位年輕開發(fā)者 Eric Evans 發(fā)表了他自己的書《領(lǐng)域驅(qū)動(dòng)設(shè)計(jì):軟件核心復(fù)雜性應(yīng)對(duì)之道》,這本書至少讓 Martin 感到非常傷心。
實(shí)際上,DDD 是一個(gè)獨(dú)立的主題,需要在自己的系列文章中詳細(xì)描述,所以我們現(xiàn)在不會(huì)展開介紹,只關(guān)注它所引入的所有架構(gòu)變化。
Evans 贊同 Fowler 的所有觀點(diǎn),即項(xiàng)目的依賴關(guān)系應(yīng)該是單向的。然而,他也提到低層模塊可以調(diào)用上層模塊,前提是不違反依賴關(guān)系的方向規(guī)則。這可以通過回調(diào)、觀察者模式等方式實(shí)現(xiàn)。
他還注意到控制器具有過多的邏輯,于是將其移至另一個(gè)稱為應(yīng)用層的層級(jí)中。我們開始看到用例的雛形,但尚未完全發(fā)展起來。
然而,Evans 所做的最重要的事情是說 “忽略數(shù)據(jù)庫(kù),業(yè)務(wù)邏輯更重要 ”。他說了這句話,然后卻沒有采取實(shí)質(zhì)性的行動(dòng)。是的,是的,我知道……DDD 等等。然而,從架構(gòu)的角度來看,他并沒有做出太多改變。
在他的架構(gòu)中,定義了以下層級(jí):
表示層(Presentation Layer):負(fù)責(zé)與用戶進(jìn)行交互。
應(yīng)用層(Application Layer):協(xié)調(diào)任務(wù)并將工作委派給領(lǐng)域?qū)ο蟆?/p>
領(lǐng)域?qū)樱―omain Layer):代表業(yè)務(wù)概念。它決定了應(yīng)用程序要做什么,并使其與其他應(yīng)用程序獨(dú)特區(qū)別開來。
基礎(chǔ)設(shè)施層(Infrastructure Layer):在內(nèi)存中持久化數(shù)據(jù)并維護(hù)應(yīng)用程序的狀態(tài)。
你可以看到,他進(jìn)行了一些重命名。
用戶界面(User Interface)意味著你有用戶,但并不總是這樣。有時(shí)它是針對(duì)用戶的圖形用戶界面(GUI),有時(shí)是針對(duì)開發(fā)人員的命令行界面(CLI),而通常它是針對(duì)程序的應(yīng)用程序編程接口(API)。表示層(Presentation Layer)只是一個(gè)更通用和合適的名稱。
業(yè)務(wù)邏輯(Business logic)對(duì)一些開發(fā)人員來說很令人困惑,尤其是那些根本沒有做業(yè)務(wù)的開發(fā)人員,因此引入了一個(gè)新名稱 —— 領(lǐng)域(Domain)。
數(shù)據(jù)庫(kù)并不是我們使用的唯一外部工具,所以所有的電子郵件發(fā)送器、事件總線、SQL 和其他瑣碎的東西都被移動(dòng)到了基礎(chǔ)設(shè)施層。
基本上就是這樣。在這里進(jìn)行了一些重命名,再加上新增了一個(gè)層級(jí)。我們?cè)谠擃I(lǐng)域付出了很多努力。但這仍然是相同的架構(gòu),具有相同的依賴關(guān)系。要是他當(dāng)時(shí)知道依賴反轉(zhuǎn)原則就好了。
2005 年:六邊形架構(gòu)(Ports and Adapters)
以前,模塊必須引用行中的下一個(gè)模塊。隨著依賴反轉(zhuǎn)原則的發(fā)現(xiàn),一切都改變了。
這對(duì)于軟件開發(fā)人員來說是一個(gè)難得的機(jī)會(huì)。我們終于學(xué)會(huì)了如何控制依賴關(guān)系的方向,將其指向我們希望的方式!這意味著業(yè)務(wù)邏輯不再引用數(shù)據(jù)訪問層。如果你想知道為什么這是可能的,以及接口與此有何關(guān)系,你可以在這里找到答案:
https://medium.com/@iamprovidence/from-3-layered-to-ddd-architecture-in-one-step-f3de204bec2e
第一個(gè)看到這個(gè)潛力的人是 Alistair Cockburn。這家伙吸很嗨,畫了一個(gè)六邊形,試圖召喚撒旦,等等。我不需要告訴你,你自己更了解在搖滾派對(duì)上發(fā)生的情況。沒什么特別的,有一天你喝了很多酒,第二天早上醒來時(shí)帶著嚴(yán)重的宿醉,你意外地發(fā)現(xiàn)了一種新的架構(gòu)。
Alistair 對(duì)矩形感到厭倦,于是他畫了一個(gè)六邊形,為每個(gè)東西想出了兩個(gè)名字,試圖讓它們變得神圣起來。但不要被嚇到,我的開發(fā)伙伴。實(shí)際上,這種架構(gòu)并不比 N 層架構(gòu)復(fù)雜:
這其中有很多旋轉(zhuǎn)和移動(dòng),讓我們看看實(shí)際發(fā)生了什么。
Cockburn 實(shí)現(xiàn)了 Evans 的夢(mèng)想?,F(xiàn)在,Domain 是系統(tǒng)的核心組件,不僅在言辭上,而且在行動(dòng)上也是。它不再引用其他項(xiàng)目。
為了強(qiáng)調(diào)它真正是核心,業(yè)務(wù)邏輯被重命名為核心(Core)。
基礎(chǔ)設(shè)施模塊被分成兩部分 —— 抽象(接口)和實(shí)現(xiàn) 。抽象成為業(yè)務(wù)邏輯的一部分,并被重命名為端口(ports)。實(shí)現(xiàn)部分保留在基礎(chǔ)設(shè)施層中,現(xiàn)在它們被稱為適配器(adapters)。實(shí)際上,UI 和數(shù)據(jù)庫(kù)(DB)位于相同的框架層,因此它們經(jīng)歷了相同的命運(yùn)。
將基礎(chǔ)設(shè)施的接口放在業(yè)務(wù)邏輯中,使 Domain 變得自治且無依賴。結(jié)果,業(yè)務(wù)邏輯可以在任何環(huán)境中使用任何工具。想要更改數(shù)據(jù)庫(kù)?只需更改實(shí)現(xiàn)部分,實(shí)現(xiàn)所需的適配器,并將其 “插入” 到可用的端口中。
任何適配器的更改(數(shù)據(jù)庫(kù)、電子郵件發(fā)送器、UI)都不會(huì)影響業(yè)務(wù)邏輯。接口保持不變。
每個(gè)組件都可以單獨(dú)部署。如果更改數(shù)據(jù)訪問,只需重新構(gòu)建數(shù)據(jù)訪問部分。如果只更改 UI,只需更改 UI 部分。
由于可以單獨(dú)部署模塊,這意味著它們可以單獨(dú)開發(fā)。
只有優(yōu)點(diǎn)。
我忘了提到,調(diào)用我們系統(tǒng)的適配器稱為主要適配器(驅(qū)動(dòng))。被我們系統(tǒng)調(diào)用的適配器稱為次要適配器(被驅(qū)動(dòng))。雖然這不重要,但了解這一點(diǎn)會(huì)讓你聽起來很博學(xué)。
就解決方案結(jié)構(gòu)而言,以下對(duì)我來說效果最好:
再次強(qiáng)調(diào),文件夾與項(xiàng)目是你自己決定的事情。
只需按照引用關(guān)系,并確保它們不會(huì)跨越不應(yīng)跨越的地方:
2008 年:洋蔥架構(gòu)
這個(gè)故事有點(diǎn)令人毛骨悚然,所以做好準(zhǔn)備吧。
Jeffrey Palermo。這是一個(gè)充滿悲傷和黑暗的故事,講述了一個(gè)男孩童年時(shí)被洋蔥的殘忍思考所困擾的悲傷故事。隨著他的成長(zhǎng),他心中燃燒著一種憤恨,懷著復(fù)仇的承諾。
而相信我,他對(duì)這個(gè)承諾始終如一。這個(gè)小洋蔥讓全世界數(shù)百萬開發(fā)人員哭泣,向他們的母親尋求安慰。
這種架構(gòu)從端口和適配器中得到了很多提升。它仍然涉及依賴反轉(zhuǎn)。它按照抽象和實(shí)現(xiàn)分割代碼。端口仍然是業(yè)務(wù)邏輯的一部分。只不過這次,Palermo 從 Evans 的模式中添加了應(yīng)用層,該層也可以包含一些端口。
這種架構(gòu)面臨的最大挑戰(zhàn),也是導(dǎo)致混淆的原因,是模塊之間的依賴關(guān)系。
然而,規(guī)則很簡(jiǎn)單:任何外層只能且只能依賴于內(nèi)層 。
不夠簡(jiǎn)單,對(duì)吧?我也是這么想的。那么,讓我們來剖析一下這個(gè)洋蔥。
Domain 位于中心。它內(nèi)部沒有內(nèi)層,因此不應(yīng)依賴于任何其他層。
應(yīng)用層僅包裹領(lǐng)域,所以它只應(yīng)該依賴于 Domain。
基礎(chǔ)設(shè)施層和展示層位于同一級(jí)別,它們不能相互依賴,但可以依賴于應(yīng)用層和 Domain,因?yàn)樗兴杞涌诙荚谄渲卸x。
你還可以看到它擁有 DDD 架構(gòu)中的所有模塊,但以不同的方式處理它們。這實(shí)際上非常重要!關(guān)鍵在于將很少發(fā)生修改的組件放在中間,并將頻繁發(fā)生修改的組件放在邊緣。
應(yīng)用層或任何其他層的更改不會(huì)影響領(lǐng)域,只會(huì)影響相關(guān)的層。 只有當(dāng)業(yè)務(wù)邏輯發(fā)生變化時(shí),Domain 才會(huì)發(fā)生變化,而這種情況無論如何都會(huì)影響整個(gè)系統(tǒng)。
這是理論上的情況。在實(shí)踐中,你的組合根(Main() 函數(shù),在其中注冊(cè)所有依賴項(xiàng)并將模塊組合在一起)將成為展示層(ASP、WPF、CLI)的一部分,因此圖表將如下所示:
對(duì)你來說這個(gè)看起來熟悉嗎?這就是 N 層架構(gòu),只是組件的順序不同。
不管它的外觀如何,無論是六邊形、端口還是洋蔥,你的最終目標(biāo)是將依賴關(guān)系以無環(huán)圖或樹形結(jié)構(gòu)的形式呈現(xiàn)出來。
2012 年:清潔架構(gòu)
有個(gè)名叫 Bob 的人, 他是最優(yōu)雅的程序員, 他的敏捷之舞和完美架構(gòu), 讓你的代碼嶄新光亮。
我是說,要講述關(guān)于架構(gòu)的文章,就不能不提到 Robert C.Martin。
他看到了關(guān)于架構(gòu)的熱潮,并決定加入其中。Martin 了解開發(fā)者的主要秘密,因此他毫不掩飾地借用別人的想法,并將其稱為自己的。
開個(gè)玩笑,如今很少有原創(chuàng)的想法,大家都在相互借鑒。讓我們看看 Martin 在這里帶來了什么:
我們可憐的 Domain 再次改名,現(xiàn)在稱為實(shí)體(Entities)。但那不僅僅是改名而已。它意味著你不會(huì)再有領(lǐng)域服務(wù)和貧血模型,而是擁有數(shù)據(jù)和行為的豐富類。
倉(cāng)儲(chǔ)接口和其他端口從領(lǐng)域?qū)右频綉?yīng)用層。而應(yīng)用層也得到了一個(gè)更合適的名稱 —— 用例(Use Cases)。
展示層和基礎(chǔ)設(shè)施層保持不變。然而,Martin 還在頂部添加了一個(gè)額外的層,其中包括框架、DLL 和其他外部依賴。這并不意味著你的數(shù)據(jù)庫(kù)將引用實(shí)體,它只是防止內(nèi)部層引用外部工具。
再次強(qiáng)調(diào),沒有嚴(yán)格的規(guī)定。你可以在任何級(jí)別添加任意多的層。所以如果你想為領(lǐng)域服務(wù)定義一個(gè)層,你可以這樣做。
Martin 還在架構(gòu)旁邊畫了一個(gè)小圖。
圖中顯示用戶通過觸發(fā)控制器的端點(diǎn)與系統(tǒng)進(jìn)行交互,控制器調(diào)用一個(gè)用例,然后通過展示器返回?cái)?shù)據(jù)(黑線)。用例可以通過接口調(diào)用任何它所需的端口(綠線)。而實(shí)際的實(shí)現(xiàn)則位于外層(橙線)。
圖表試圖強(qiáng)調(diào)執(zhí)行流程(虛線)并不總是與依賴關(guān)系方向(實(shí)線)相對(duì)應(yīng),這就是依賴倒置原則。
基本上,它再次強(qiáng)調(diào)了控制反轉(zhuǎn)的使用。在我們討論端口和適配器時(shí),你已經(jīng)見過這一點(diǎn)。
通常在 ASP 中,我們沒有單獨(dú)的展示器組件。這也由控制器來完成。因此,整個(gè)圖表可以在代碼中表示為:
class?OrderController?:?ControllerBase,?IInputPort,?IOutputPort { ????[HttpGet] ????public?IActionResult?Get(int?id) ????{ ????????_getOrderUserCase.Execute(id); ????????return?DisplayOutputPortResult(); ????} }
其他形式的隔離
所有這些架構(gòu)都旨在通過分離責(zé)任來將一個(gè)代碼從另一個(gè)代碼中隔離出來。然而,還有其他形式的隔離:垂直切片、有界上下文、模塊、微服務(wù)等。這些方法的目標(biāo)是根據(jù)功能來劃分代碼。
有些人不認(rèn)為它們是 “真正的” 架構(gòu)方法,而有些人則認(rèn)為它們是。這取決于你的觀點(diǎn)。最終,它們可能會(huì)發(fā)展到一種程度,在那個(gè)程度上它們可能會(huì)使用上述任何一種架構(gòu)風(fēng)格,甚至是它們的組合:
結(jié)論
在本文中,我們討論了 N-layered、DDD、六邊形、洋蔥和清潔架構(gòu)。這些只是眾多存在的架構(gòu)中的一部分,是一些比較出名的架構(gòu)。你可能還聽說過 BCE、DCI 等。
盡管在細(xì)節(jié)上可能存在一些差異,但所有這些架構(gòu)實(shí)際上是非常相似的。它們都有著相同的目標(biāo) —— 分離責(zé)任 。它們通過將代碼分割成不同的層來實(shí)現(xiàn)這一目標(biāo)。唯一的區(qū)別在于定義了哪些組件以及這些層之間存在什么樣的依賴關(guān)系。
現(xiàn)在你對(duì)整個(gè)情況有了全面的了解,我強(qiáng)烈鼓勵(lì)你再次閱讀本文。自己明白不同架構(gòu)之間的差異。你還可以嘗試自己動(dòng)手進(jìn)行項(xiàng)目實(shí)踐。編寫一些帶有接口的類,關(guān)注項(xiàng)目之間的引用關(guān)系,接口和實(shí)現(xiàn)的放置位置,以及所使用的訪問修飾符。
希望從現(xiàn)在開始,每當(dāng)你創(chuàng)建一個(gè)類、審查一個(gè) Pull Request,或者與你的同事進(jìn)行討論時(shí),你都能有意識(shí)地思考并質(zhì)疑這些事情。
編輯:黃飛
?
評(píng)論
查看更多