端口和適配器架構

覃宇,Android 開發者 / ThoughtWorks 技術教練 // 譯者,熱衷於探究軟件開發的方方面面,從端到雲,從工具到實踐。喜歡通過翻譯來學習和分享知識,譯作有《Kotlin 實戰》、《領域驅動設計精粹》、《Serverless 架構:無服務器應用與 AWS Lambda》和《雲原生安全與 DevOps 保障》。

2005 年,Alistair Cockburn 構思了端口和適配器架構 (又稱六邊形架構) 並記錄在他的博客中。下面這句話就是他對該架構的目標的定義:

讓用戶、程序、自動化測試和批處理腳本可以平等地驅動應用,讓應用的開發和測試可以獨立於其最終運行的設備和數據庫。——Alistair Cockburn 2005,端口和適配器

有許多文章在談及端口和適配器架構時會花很多篇幅在分層上。然而, 我並沒有在 Alistair Cockburn 的原文中找到關於分層的隻言片語。

其思想是將我們的應用看作是一個系統的中心交付物,輸入和輸出都是通過端口出入應用,這些端口將應用和外部工具、技術以及傳達機制隔離開來。應用不應該關心是誰在發送輸入或接收輸出。這就是爲了保護產品免受技術和業務需求演進的影響。由於技術 / 供應商鎖定,這些演進可能導致產品剛開發沒多久就被廢棄。

我將在本文中剖析以下主題:

◐  傳統架構方式的問題

傳統的架構方式在前端和後端都可能給我們帶來問題。

在前端,業務邏輯最終可能會滲透到 UI(例如,我們把用例的邏輯放到控制器或視圖裏,導致這些邏輯不能在其它 UI 界面中重用), 甚至 UI 會反過來滲透到業務邏輯中 (例如,我們會爲了模板中需要的業務邏輯在實體中創建對應的方法)。

而在後端,我們可能會在自己的業務邏輯裏使用外部類的類型提示、繼承或者實例化它們,這會導致對這些外部的庫和技術直接引用,最後任由它們滲透到業務邏輯中。

◐  分層架構的演化

託 EBI (譯)和 DDD(譯)的福, 2005 年我們已經知道了 “系統中真正重要的是位於中間的層次”。業務邏輯(應該) 存在於這些層次之中,它們纔是我們和競品的真正區別。這纔是真正的“應用”。

但是,Alistair Cockburn 意識到 頂部和底部的層次從另一方面來說,就是應用的入口 / 出口。儘管實際中它們不一樣,卻有着十分相似的目標,在設計上也是對稱的。而且,如果我們想要隔離出應用中間的層次,這些入口和出口能以另一種相似的方式使用。

區別於典型的分層架構圖,我們將它們畫在系統的左右兩側,而不是上下兩邊。

雖然我們識別出了系統中對稱的兩側,但兩側都可能有若干入口 / 出口。例如, API 和 UI 就是位於應用左側的兩個不同的入口 / 出口。爲了表示應用有若干個入口 / 出口,我們把應用的形狀改成了多邊形。應用的形狀可以是有多條邊的任意多邊形,但最終六邊形獲得了青睞。這也是 “六邊形架構” 的由來。

端口和適配器架構使用了實現爲端口和適配器的抽象層次,解決了傳統架構方式帶來的問題。

什麼是端口?

端口是對其消費者無感知的進入 / 離開應用的入口和出口。在許多編程語言裏,端口就是接口。例如,在搜索引擎裏它可能是執行搜索的接口。在應用中,我們把這個接口當成入口 / 出口使用,而不用去關心它的具體實現,實際上在所有將接口定義爲類型提示的地方,這些實現會被注入。

什麼是適配器?

適配器是將一個接口轉換 (適配) 成另一個接口的類。

例如,一個適配器實現了接口 A 並被注入了接口 B。當這個適配器被實例化時,一個實現了接口 B 的對象將從構造方法注入進來。實現了接口 A 的 對象會被注入到需要接口 A 的地方,然後接收方法請求,將其轉換並代理給那個實現了接口 B 的內部對象。

如果我說的不夠明白,別慌,後面我會給出一個更具體的例子。

適配器的兩種不同類型

左側代表 UI 的適配器被稱爲主適配器或者主動適配器,因爲是它們發起了對應用的一些操作。而右側表示和後端工具鏈接的適配器,被稱爲從適配器或者被動適配器,因爲它們只會對主適配器的操作作出響應。

端口 / 適配器的用法也有一點區別:

◐ ** 端口和適配器架構有哪些優勢?**

使用這種應用位於系統中心的端口 / 適配器設計,讓我們可以保持應用和實現細節之間的隔離,這些實現細節包括曇花一現的技術、工具和傳達機制。它還讓可重用的概念更容易更快速地得到驗證並被創建出來。

實現隔離和技術隔離

上下文

我們的應用使用 SOLR 作爲搜索引擎,並使用一個開源庫連接它並執行搜索。

傳統架構方式

傳統架構方式下,我們會直接在我們的代碼中使用庫 (SOLR) 裏的類,作爲類型提示,或者實例化和 / 或作爲我們實現的基類。

端口和適配器架構方式

如果採用端口和適配器架構的話,我們會創建一個接口,比如叫做 UserSearchInterface,在代碼中用這個接口作爲類型提示。我們還會爲 SOLR 創建一個實現該接口的適配器,比如叫做 UserSearchSolrAdapter。這個實現是 SOLR 的包裝,SOLR 會被注入其中並用來實現接口指定的方法。

問題

不久之後,我們想用 Elasticsearch 換掉 SOLR。甚至,對於同樣的搜索行爲,我們希望有些時候使用 SOLR,有些時候使用 Elasticsearch,在運行時決定就好。

如果我們採用傳統架構,我們需要查找所有使用 SOLR 的代碼並替換成 Elasticsearch。然而,這可不是簡單的查找替換:兩個引擎的用法不同,方法、輸入、輸出也不盡相同,替換並不是一件輕鬆的任務。而在運行時在決定使用那個引擎甚至是不可能的。

然而,假設我們使用了端口和適配器架構,我們只需要創建一個新的適配器,比如就叫 UserSearchElasticsearchAdapter,在注入時使用它換掉 SOLR 的適配器,也許改一下 DCI 中的配置就可以做到。我們完全可以使用工廠來決定注入那個適配器,實現在運行時注入不同的實現。

傳達機制的隔離

和上面這個例子類似,假設我們的應用需要 Web GUI,CLI 和 Web API。我們想在全部三種 UI 中提供某個功能,比如叫做 UserProfileUpdate 的功能。

使用端口和適配器架構的話,我們會在一個應用服務的方法中實現這個功能並將其作爲一個用例。服務會實現一個接口,該接口說明了方法、輸入以及輸出。

每個版本的 UI 都有各自的控制器 (或控制檯命令) 來通過這個接口觸發期望的邏輯,應用服務接口的具體實現會被注入到 UI 中。這種情況下,適配器實際上就是控制器(或 CLI 命令)。

之後我們可以修改 UI,因爲我們知道這些修改不會影響業務邏輯。

測試

上面兩個例子中,使用端口和適配器架構會讓測試更加容易。第一個例子中,我們用接口 (端口) 的 Mock 就可以測試應用,而不需要使用 SOLR 或 Elasticsearch 。

第二個例子中,所有的 UI 都可以獨立於應用進行測試。我們的用例也可以獨立於 UI 進行測試,傳給服務一些輸入再斷言結果就好。

◐ 總結

在我看來,端口和適配器架構只有一個目標:將業務邏輯和系統使用的傳達機制以及工具隔離。爲此,它使用了常見的編程語言結構:接口。

在 UI 側 (主動適配器),我們創建使用應用接口的適配器,比如控制器。

在基礎設施側 (被動適配器),我們創建實現應用接口的適配器,比如資源庫。

這就是全部!

然而,我驚訝的發現早在十三年前同樣的思想就已經公開發表了,儘管它沒有刻意地強調要將工具和傳達機制從應用核心中隔離出來。

系統和角色的任何交互都要通過邊界對象。按照 Jacobson 的描述,角色可以是客戶或者管理員 (操作員) 這樣的人類用戶,也可以是定時器或者打印機這樣的非人類“用戶”,它們分別對應着端口和適配器架構中的主動適配器和被動適配器。

◐  引用來源

☼ 素履之往:2011 年秋攝於美國內華達州紅石峽谷(Red Rock Canyon)。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/w2UInmPwuH4L4bkb4pOWeg