兩種解耦方法

Carson Gross

將 REST 架構風格與其他基於網路的風格區分開來的核心特徵是它強調組件之間的統一介面。透過將軟體工程的通用性原則應用於組件介面,整體系統架構得以簡化,並且提高了互動的可見性。實作與它們提供的服務解耦,從而鼓勵獨立的演化。

-Roy Fielding, https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm#sec_5_1_5

在這篇文章中,我們將探討網頁應用程式中兩種不同的解耦類型

我們將看到,在應用程式層級,超媒體 API 會緊密耦合你的前端和後端。儘管如此,令人驚訝的是,超媒體 API 在面對變更時實際上更具彈性。

#耦合

耦合是軟體系統的一種屬性,其中系統的兩個模組或方面具有高度的相互依賴性。解耦軟體是指減少不相關模組之間的這種相互依賴性,以便它們可以彼此獨立地演化。

耦合和解耦的概念與內聚密切相關(且呈反比)。高內聚性的軟體在模組或概念邊界內具有相關邏輯,而不是分散在整個程式碼庫中。(一個相關的概念是我們自己的 行為局部性 概念)

廣泛來說,經驗豐富的開發人員會力求建立解耦和高內聚的系統。

#JSON 資料 API - 應用程式層級解耦

現在建構網頁應用程式的常見方法是建立一個 JSON 資料 API,然後使用 JavaScript 框架(例如 React)來使用該 JSON API。這種應用程式層級的架構決策將前端程式碼與後端程式碼解耦,並允許在其他情況下重複使用 JSON API,例如行動應用程式、第三方客戶端整合等。

這是一種應用程式層級的解耦,因為解耦的決策和實作是由應用程式開發人員自己完成的。JSON API 在兩個軟體元件之間提供了一個「硬」介面。

以我最喜歡的例子來說,考慮一個銀行的簡單 JSON,它在 https://example.com/account/12345 有一個 GET 端點。此 API 可能會傳回以下內容

HTTP/1.1 200 OK

{
    "account": {
        "account_number": 12345,
        "balance": {
            "currency": "usd",
            "value": -50.00
        },
        "status": "overdrawn"
    }
}

任何客戶端都可以使用此資料 API:網頁應用程式、行動客戶端、第三方等。因此,它與任何特定客戶端解耦。

#透過 JSON API 實作解耦

到目前為止,一切都很好。但是這種解耦在實務上是如何運作的?

在我們的文章 拆分你的資料和應用程式 API:更進一步中,你會發現以下引述

我目前工作中最糟糕的部分是為前端開發人員設計 API。對話總是不可避免地變成這樣

開發人員 – 所以,這個螢幕有資料元素 x、y、z…你能否建立一個回應格式為 {x: , y:, z: } 的 API

我 – 好的

Jean-Jacques Dubray - https://www.infoq.com/articles/no-more-mvc-frameworks

這段引述顯示,儘管我們用乾草叉(或者在我們的例子中,用 JSON API)將耦合驅逐出去,但它又透過要求網頁應用程式特定的 JSON API 端點回來了。這類要求最終會重新耦合前端和後端程式碼:JSON API 不再提供通用的 JSON 資料 API,而是針對前端需求的特定 API。

更糟糕的是,這些前端需求通常會隨著應用程式的演進而頻繁變更,需要修改你的 JSON API。如果其他非網頁應用程式的客戶端已經開始依賴原始 API 怎麼辦?

這個問題導致了許多 JSON 資料 API 開發人員在支援網頁應用程式和其他非網頁應用程式客戶端時面臨的「版本地獄」。

#一個解決方案:GraphQL

解決這個問題的一個潛在解決方案是引入 GraphQL,這讓你可以擁有更具表現力的 JSON API。這表示當你的 API 客戶端的需求變更時,你不需要經常變更它。

這是解決上述問題的一個合理方法,但是它也存在一些問題。我們看到的最大問題是安全性,正如我們在 API 變更/安全性取捨 這篇文章中所概述的。

顯然,Facebook 使用 白名單 來處理 GraphQL 引入的安全性問題,但是許多使用 GraphQL 的開發人員似乎不了解它所涉及的安全性威脅。

#另一個解決方案:拆分你的應用程式和通用資料 API

Max Chernyak 在他的文章 不要建構一個通用的 API 來為你自己的前端提供支援中建議的另一種方法是建立兩個 JSON API

這是一個務實的解決方案,可以解決網頁應用程式前端和支援它的後端程式碼之間似乎固有的耦合,而且它不涉及一般 GraphQL API 中涉及的安全性取捨。

#超媒體 - 網路架構解耦

現在讓我們考慮一下超媒體 API 如何解耦軟體。

考慮一下我們在上面看到的對 https://example.com/account/12345 的同一個 GET 的可能回應

HTTP/1.1 200 OK

<html>
  <body>
    <div>Account number: 12345</div>
    <div>Balance: $100.00 USD</div>
    <div>Links:
        <a href="/accounts/12345/deposits">deposits</a>
        <a href="/accounts/12345/withdrawals">withdrawals</a>
        <a href="/accounts/12345/transfers">transfers</a>
        <a href="/accounts/12345/close-requests">close-requests</a>
    </div>
  <body>
</html>

(是的,這是一個 API 回應。它恰好是一個超媒體格式的回應,在這個例子中是 HTML。)

在這裡我們看到,在應用程式層級,這個回應不可能與「前端」更緊密地耦合。事實上,它就是前端,因為 API 回應不僅指定了資源的資料,還提供了有關如何向使用者顯示此資料的確切配置資訊。

回應還包含超媒體控制項,在本例中是連結,最終使用者可以從中選擇來繼續導覽此超媒體驅動應用程式提供的超媒體 API。

那麼,這種情況下的解耦在哪裡呢?

#REST & 統一介面

這種情況下的解耦發生在較低的層級。它發生在網路架構層級,也就是說,在系統層級。超媒體系統旨在將超媒體客戶端(在網頁的情況下,是瀏覽器)與超媒體伺服器解耦。

這主要是透過 REST 的統一介面約束來完成的,特別是透過使用超媒體作為應用程式狀態引擎 (HATOEAS)。

這種解耦方式允許在較高的應用程式層級實現更緊密的耦合(我們已經看到這可能是一種固有的耦合),同時仍為整個系統保留解耦的好處。

#透過超媒體實作解耦

這種解耦在實務上是如何運作的?好吧,假設我們希望移除從我們的銀行向其他銀行轉帳的功能以及關閉帳戶的功能。

現在我們對此 GET 請求的超媒體回應看起來如何?

HTTP/1.1 200 OK

<html>
  <body>
    <div>Account number: 12345</div>
    <div>Balance: $100.00 USD</div>
    <div>Links:
        <a href="/accounts/12345/deposits">deposits</a>
        <a href="/accounts/12345/withdrawals">withdrawals</a>
    </div>
  <body>
</html>

你可以看到,在此回應中,這兩個動作的連結已從 HTML 中移除。瀏覽器只是簡單地將新的 HTML 渲染給使用者。在四捨五入的誤差範圍內,沒有任何客戶端在使用的 API。API 被編碼在超媒體中並透過超媒體發現。

這表示我們可以大幅變更我們的 API 而不會破壞我們的客戶端。

這種彈性是 RESTful 網路架構的關鍵,尤其是 HATEOAS 的關鍵。

如你所見,儘管我們的前端和後端之間有更緊密的應用程式層級耦合,但由於 RESTful 超媒體系統的統一介面方面為我們提供的網路架構解耦,我們實際上擁有更大的彈性。

#但是這是一個很糟糕的 (資料) API!

許多人會反對說,當然,這個超媒體 API 對於我們的網頁應用程式來說可能很靈活,但它會成為一個糟糕的通用 API。

這是非常正確的。這個超媒體 API 是針對特定的網頁應用程式調整的。嘗試下載此 HTML、解析它並嘗試從中提取資訊將會很麻煩且容易出錯。此超媒體 API 只有在作為更大的超媒體系統的一部分時才有意義,並由適當的超媒體客戶端使用。

這正是我們在 拆分你的資料和應用程式 API:更進一步中建議在你的超媒體 API 旁邊建立一個通用 JSON API 的原因。你可以利用超媒體的彈性來處理你自己的網頁應用程式,同時為行動應用程式、第三方應用程式等提供通用的 JSON API。

(不過,我們應該提到,基於超媒體的行動應用程式也可能是一個不錯的選擇!)

#結論

在這篇文章中,我們探討了兩種不同的解耦類型

我們看到,儘管基於超媒體的應用程式中存在更緊密的應用程式層級耦合,但超媒體系統更能優雅地處理變更。

</>