原文:https://herbertograca.com/2017/09/14/ports-adapters-architecture/
這篇文章是軟件架構編年史(譯)的一部分,這部編年史由一系列關于軟件架構的文章組成。在這一系列文章中,我將寫下我對軟件架構的學習和思考,以及我是如何運用這些知識的。如果你閱讀了這個系列中之前的文章,本篇文章的的內容將更有意義。
2005年,Alistair Cockburn構思了端口和適配器架構 (又稱六邊形架構)并記錄在他的博客中。下面這句話就是他對該架構的目標的定義:
讓用戶、程序、自動化測試和批處理腳本可以平等地驅動應用,讓應用的開發和測試可以獨立于其最終運行的設備和數據庫?!狝listair 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 的描述,角色可以是客戶或者管理員(操作員)這樣的人類用戶,也可以是定時器或者打印機這樣的非人類“用戶”,它們分別對應著端口和適配器架構中的主動適配器和被動適配器。
引用來源
1992 – Ivar Jacobson – Object-Oriented Software Engineering: A use case driven approach
200? – Alistair Cockburn – Hexagonal Architecture
2005 – Alistair Cockburn – Ports and Adapters
2012 – Benjamin Eberlei – OOP Business Applications: Entity, Boundary, Interactor
2014 – Fideloper – Hexagonal Architecture
2014 – Philip Brown – What is Hexagonal Architecture?
2014 – Jan Stenberg – Exploring the Hexagonal Architecture
2017 – Grzegorz Ziemoński – Hexagonal Architecture Is Powerful
2017 – Shamik Mitra – Hello, Hexagonal Architecture