天天看點

軟體架構(11)-端口和擴充卡/六邊形架構,高階進階之路

作者:架構師狂飙

這篇文章是 軟體架構編年史的一部分,這是一系列關于軟體架構的文章。在其中,我寫下了我在軟體架構方面學到的知識、我對它的看法以及我如何使用這些知識。如果您閱讀了本系列的前幾篇文章,這篇文章的内容可能會更有意義。

端口和擴充卡架構(又名六邊形架構)由 Alistair Cockburn 于 2005 年提出并寫在他的部落格上。他用一句話定義其目标:

允許應用程式同樣由使用者、程式、自動測試或批處理腳本驅動,并在獨立于其最終運作時裝置和資料庫的情況下進行開發和測試。
Alistair Cockburn 2005, 端口和擴充卡

我看過一些關于端口和擴充卡架構的文章,其中對層進行了很多闡述。但是,我還沒有在 Alistair Cockburn 的原帖中讀到任何關于層的内容。

這個想法是将我們的應用程式視為系統的中心人工制品,其中所有輸入和輸出都通過一個端口到達/離開應用程式,該端口将應用程式與外部工具、技術和傳遞機制隔離開來。應用程式應該不知道誰/什麼正在發送輸入或接收其輸出。這是為了針對技術和業務需求的發展提供一些保護,由于技術/供應商鎖定,這可能會使産品在開發後不久就過時。

在這篇文章中,我将深入探讨以下主題:

  • 傳統方法的問題
  • 從分層架構演進
  • 什麼是端口?什麼是擴充卡?兩種不同類型的擴充卡
  • 有什麼好處?
  • 實施和技術隔離傳遞機制隔離測試
  • 結論

傳統方法的問題

傳統的方式很可能會給我們帶來前端和後端兩個方面的問題。

在前端,我們最終将業務邏輯洩漏到 UI 中(即,當我們将用例邏輯放在控制器或視圖中時,使其無法在其他 UI 螢幕中重用)甚至将 UI 洩漏到業務邏輯中(即,當我們由于模闆中需要的某些邏輯而在我們的實體中建立方法時)。

軟體架構(11)-端口和擴充卡/六邊形架構,高階進階之路

在後端,我們可能會将外部庫和技術洩漏到業務邏輯中,因為我們最終可能會通過類型提示、子類化甚至執行個體化業務邏輯中的庫類來直接引用它們。

從分層架構演進

到 2005 年,由于EBI和DDD,我們已經知道系統中真正相關的是内層。這些層是所有業務邏輯存在(或應該存在)的層,它們是我們與競争對手的真正差別。這才是真正的“應用”。

軟體架構(11)-端口和擴充卡/六邊形架構,高階進階之路

但在某些時候,Alistair Cockburn 意識到,另一方面,頂層和底層隻是應用程式的入口/出口點。盡管它們實際上不同,但它們的目标非常相似,并且在設計中存在對稱性。此外,如果我們想要隔離我們的應用程式内層,我們可以以類似的方式使用這些入口/出口點來實作。

軟體架構(11)-端口和擴充卡/六邊形架構,高階進階之路

為了擺脫典型的分層圖,我們将系統的這兩個方面表示為左右,而不是頂部和底部。

雖然我們可以識别應用程式的兩個對稱邊,但每一邊都可以有多個入口/出口點。例如,API 和 UI 是我們應用程式左側的兩個不同的入口/出口點,而 ORM 和搜尋引擎是我們應用程式右側的兩個不同的入口/出口點。為了表示我們的應用程式有多個入口/出口點,我們将繪制具有多個面的應用程式圖。該圖可能是有多個邊的任何多邊形,但最終選擇的是六邊形。是以得名“六邊形架構”。

軟體架構(11)-端口和擴充卡/六邊形架構,高階進階之路

Ports & Adapters Architecture通過使用一個抽象層解決了前面發現的問題,實作為一個端口和一個擴充卡。

什麼是端口?

端口是消費者不可知的進出應用程式的入口點和出口點。在許多語言中,它将是一個接口。例如,它可以是用于在搜尋引擎中執行搜尋的界面。在我們的應用程式中,我們将使用此接口作為入口和/或出口點,而不知道在接口定義為類型提示的情況下實際注入的具體實作。

什麼是擴充卡?

擴充卡是将接口轉換(适配)到另一個接口的類。

例如,擴充卡實作接口 A 并注入接口 B。當擴充卡被執行個體化時,它會在其構造函數中注入一個實作接口 B 的對象。然後在需要接口 A 的任何地方注入該擴充卡并接收它的方法請求轉換并代理到實作接口 B 的内部對象。

如果我讓你感到困惑,不用擔心,我在下面給出了一個更具體的例子。

兩種不同類型的擴充卡

左側的擴充卡代表 UI,稱為主擴充卡或驅動擴充卡 ,因為它們是在應用程式上啟動某些操作的擴充卡,而右側的擴充卡代表與後端工具的連接配接,稱為次要或驅動擴充卡,因為它們總是對主要擴充卡的操作做出反應。

端口/擴充卡的使用方式也有所不同:

  • 在左側,擴充卡依賴于端口并被注入端口的具體實作,其中包含用例。在這方面,端口及其具體實作(用例)都屬于應用程式内部;
  • 在右側,擴充卡是端口的具體實作,雖然我們的業務邏輯隻知道接口,但它被注入到我們的業務邏輯中。在這方面,端口屬于應用程式内部,但它的具體實作屬于應用程式外部,它環繞着一些外部工具。
軟體架構(11)-端口和擴充卡/六邊形架構,高階進階之路

有什麼好處?

使用這種端口/擴充卡設計,将我們的應用程式置于系統的中心,使我們能夠将應用程式與臨時技術、工具和傳遞機制等實施細節隔離開來,進而使測試和建立可重用證明變得更加容易和快速的概念。

實施和技術隔離

語境

我們有一個使用 SOLR 作為搜尋引擎的應用程式,我們使用一個開源庫連接配接到它并執行搜尋。

傳統方法

使用傳統方法,我們将直接在我們的代碼庫中使用該庫類,作為我們實作的類型提示、執行個體和/或超類。

端口和擴充卡方法

使用端口和擴充卡,我們将建立一個接口,我們稱它為 UserSearchInterface,我們将在需要時将其用作類型提示。我們還将為 SOLR 建立擴充卡,它将實作該接口,我們将其命名為 UserSearchSolrAdapter。此實作是 SOLR 庫的包裝器,是以它會注入庫并使用它來實作接口中指定的方法。

問題

在某些時候,我們想從 SOLR 切換到 Elasticsearch。此外,對于相同的搜尋,有時我們想使用 SOLR,而其他時候我們想使用 Elasticsearch,并在運作時做出決定。

如果我們使用傳統的方法,我們将不得不為 Elasticsearch 庫搜尋和替換 SOLR 庫的用法。然而,這不是簡單的搜尋和替換:庫有不同的使用方式,不同的方法有不同的輸入和輸出,是以替換庫不是一件容易的事。并且在運作時使用一個庫而不是另一個庫是不可能的。

但是,如果我們使用 Ports & Adapters,我們隻需要建立一個新的擴充卡,我們将其命名為 UserSearchElasticsearchAdapter,然後注入它而不是 SOLR 擴充卡,也許隻需更改 DIC 中的配置即可。要在運作時注入不同的實作,我們可以使用工廠來決定注入哪個擴充卡。

傳遞機制隔離

以與前面示例類似的方式,假設我們有一個需要 Web GUI、CLI 和 Web API 的應用程式。我們還希望在所有三個 UI 中提供一些功能,我們稱該功能為 UserProfileUpdate。

使用端口和擴充卡,我們将在應用程式服務方法中實作此功能并将其視為用例。該服務将實作一個指定方法、輸入和輸出的接口。

然後,每個 UI 版本都會有一個控制器(或控制台指令),該控制器将使用該界面來觸發所需的邏輯,并将注入服務的具體實作。在這裡,Adapter 實際上是控制器(或 CLI Command)。

然後我們可以完全改變 UI,因為我們知道我們不會影響業務邏輯。

測試

在前面的兩個示例中,使用端口和擴充卡架構進行測試變得更加容易。在第一個示例中,我們可以模拟或存根接口(端口)并在不使用 SOLR 或 Elasticsearch 的情況下測試我們的應用程式。

在第二個示例中,我們可以通過簡單地為我們的服務提供一些輸入并斷言結果來測試與我們的應用程式隔離的所有 UI,以及與 UI 隔離的用例。

結論

在我看來,端口和擴充卡架構隻有一個目标:将業務邏輯與系統使用的傳遞機制和工具隔離開來。它通過使用一種通用的程式設計語言結構來實作:接口。

在UI端(驅動擴充卡),我們建立使用我們的應用程式接口的擴充卡,即。控制器。

在基礎設施方面(驅動擴充卡),我們建立了實作我們的應用程式接口的擴充卡,即。存儲庫。

僅此而已!

不過,奇怪的是,同樣的想法早在 13 年前就已發表,盡管沒有明确強調将工具和傳遞機制與應用程式核心隔離開來的目标。

軟體架構(11)-端口和擴充卡/六邊形架構,高階進階之路

伊瓦爾·雅各布森 1992 年,第 171 頁

系統與參與者的任何互動都通過邊界對象。正如 Jacobson 所描述的,Actor 可以是人類使用者,如客戶或管理者(操作員),但也可能是非人類“使用者”,如警報器或列印機,對應于 Driving Adapters和Driven Adapters端口和擴充卡架構。

參考資料:https://herbertograca.com/2017/09/14/ports-adapters-architecture/

繼續閱讀