將單一職責原則應用於前端FE/BFF分層架構 - Expedia

banq發表於2021-11-05

前端後端(BFF)模式是一種最近越來越流行的軟體架構模式。在 Expedia Group ,我們在整個微前端團隊中大量使用這種模式,作為我們平臺解決方案實施的一部分。在發展我們的架構的過程中,我們最近引入了一種新方法,在這個 3 篇系列文章中進行了解釋。我們希望這可以幫助將來遇到類似情況的其他人。

 

開發微前端和 BFF 的分層方法

受單一職責原則 (SRP) [2] 的啟發,我們分解了構成我們微前端功能的不同層,如下所示:

前端

  • UI 檢視層:負責所有渲染方面(React 元件、樣式、可訪問性、UX)。它處理檢視模型並且對域模型一無所知。
  • UI 狀態層:負責全域性狀態,以及對 BFF API 的 API 呼叫。

後端

  • BFF API:負責編排對下游 API 的呼叫以及支援前端所需的所有後端功能。它將下游域模型轉換為 UI 檢視模型,反之亦然。
  • 下游 API:負責基礎設施服務、持久化和業務邏輯。它處理領域模型。

如果我們尊重每一層的職責,同時保持它們之間的清晰界限,我們可以得到類似於下圖的架構。

將單一職責原則應用於前端FE/BFF分層架構 - Expedia

 

付諸實踐

在考慮這些層時,在考慮任何程式碼邏輯之前,先考慮訊息契約是有幫助的。為了完成用例,每一層需要獲取或傳送到任何相鄰層的訊息是什麼?如上圖所示,我們的架構中有兩個關鍵的“模型”:檢視模型和域模型。它們代表 Web 客戶端和 BFF API 之間的訊息契約,以及相應的 BFF API 和下游 API。所以讓我們暫時關注這些。

用例示例

讓我們將此用例作為假設能力的示例(假設您正在管理一家在全球擁有多家商店的全球公司):

作為管理員使用者,我想檢視為我的商店配置的營業時間列表,以便我可以更好地管理不同時區的員工。

以下是此用例的線框:

將單一職責原則應用於前端FE/BFF分層架構 - Expedia

 

檢視模型

為了支援像線框中那樣的檢視,我們需要一個形狀類似於以下的物件陣列:

[ 
  { 
    uri: "<the-uri>", 
    name: "EG1", 
    country: "US", 
    city: "Seattle", 
    hours: " 
    8am - 10pm", timeZone: "PDT", 
    lastUpdatedOn: "01/01 /2021", 
    lastUpdatedBy: "John Doe", 
  } 
]

現在我們已經定義了一個潛在的檢視模型,讓我們看看域模型。

 

領域模型

此示例假設下游 API 已經建立並且可能由不同的團隊管理,因此在實現 BFF 時,我們需要做的就是找出 (a) 這些端點是什麼,以及 (b) 它們的 API 契約是什麼(輸入請求形狀和輸出響應形狀)。理想情況下,我們需要單個下游 API 呼叫來為我們提供所需的資料,但有時我們可能需要執行多個 API 呼叫。無論情況如何,我們都可以將所有實現邏輯留在 BFF 層,以便我們的客戶端不知道任何域級別的細節。

在這個例子中,我們假設有一個下游 API 呼叫需要對我們的檢視模型進行水合: GET: /stores

 

將領域模型對映到檢視模型

現在我們已經定義了一個檢視模型並且我們理解了處理相關領域模型的契約,下一個問題是如何將後者擬合到前者中。進入mapper,它只是一個下游 API 響應調整為我們的 Web 客戶端期望的層(即檢視模型)。如上圖所示,這是 BFF 層的職責,因此這就是我們實現它的地方。

這個對映器簡單地將域模型訊息作為輸入(或它們的組合),並返回檢視模型訊息。然後控制器可以自由地返回客戶端理解的這個改編的訊息。

這個對映器實現的細節超出了這篇文章的範圍,但這個例子或多或少地總結了它:

// 示例:從域模型對映 `name` 屬性
onst toViewModel = domainModel => domainModel.map(m => m.name);

更復雜的對映操作也可以在這裡完成(例如,連線域模型屬性以構造檢視模型屬性)。

 

利弊

就像您做出的任何架構決策一樣,總是有利有弊。這是在這種情況下要考慮的方面的簡要列表。

 

系統複雜性

最初,這種方法的缺點之一是在將系統作為一個整體來理解時更復雜。特別是,如果您的應用程式正在從概念證明 (POC) 轉移到更成熟的程式碼庫。在 POC 階段,我們傾向於(甚至可能被迫)走捷徑,並且很可能不優先考慮重構或使用健全的軟體工程原則。在那個階段,我們的首要任務通常是證明一個概念和/或獲得市場牽引力。

當我們通過我們的程式碼庫採用更分層的方法時,更多的抽象出現了,隨之而來的是更多的間接層次,需要以不同的方式對程式碼進行推理。對於某些開發人員來說,這種思維方式的轉變可能具有挑戰性,尤其是從第一天起就一直在研究 POC 程式碼庫的開發人員(時間越長,情況越糟)。儘管如此,它在某種程度上是一種必要的“邪惡”。程式碼結構與我們人類在日常生活中建立以幫助我們管理複雜性的層次結構沒有什麼不同。抽象,尤其是在正確實施的情況下,可以使理解整個畫面變得更加困難,但也可以幫助我們實現新的生產力和可擴充套件性水平。

想想我們可以通過像 React 這樣的庫實現的生產力水平。在基本層面上,React 可以被認為是 HTML 渲染邏輯的抽象。也許您並不完全瞭解幕後發生的事情,但這並不能阻止您用它實現偉大的事業。

分而治之

在 UI 和 BFF 之間有明確的邊界/契約,獨立於下游服務 API 可以為團隊效率帶來一些有趣的機會。例如,如果下游服務仍在開發中,則前端工作不必擱置。

同樣,如果 BFF 端點尚未實現,甚至沒有定義,前端開發人員可以開始取得進展。所需要的只是一個清晰的檢視模型定義來定義合同,這通常可以從需求和/或 UX 線框圖中得出。

 

單元測試

在完成 BFF 開發工作的同時,UI 開發人員可以使用基於商定的檢視模型的模擬資料來實現 UI 渲染和狀態管理層。此外,這個合同將允許您更輕鬆地在 UI 和 BFF 上執行 TDD,如果這在您的工具帶中。

通過前端和 BFF(即檢視模型)之間定義的契約,我們可以嘗試在雙方進行更多的黑盒測試。事實上,即使我們仍在進行單元測試,只要我們在兩個程式碼庫之間保持這些“檢視模型契約”同步,我們就可以傾向於擁有更多類似整合的測試策略,同時仍然不必忍受建立一個真正的整合環境。

注意:這並不是說這種技術應該取代真正的整合測試。但是,它可以成為我們整體測試方法中非常有用的工具。

將單一職責原則應用於前端FE/BFF分層架構 - Expedia

前端單元測試——藍色箭頭表示測試輸入;綠色箭頭表示測試輸出;檢視模型(以前設計為與後端通訊)可用於存根後端 API 響應。這允許我們在前端做更多的黑盒測試方法。

將單一職責原則應用於前端FE/BFF分層架構 - Expedia

後端單元測試——藍色箭頭表示測試輸入;綠色箭頭表示測試輸出(即檢視模型);下游 API 可以被存根。這允許我們在後端做更多的黑盒測試方法。

明天再來看看這個由三部分組成的系列的第 2 部分,我們將更詳細地瞭解使用這種方法時的前端注意事項。

 

相關文章