使用 REST 進行企業整合

大多數內部 REST API 都是一次性的 API,專門建置用於單一整合點。在本文中,我將討論非公開 API 的限制和彈性,以及從跨多個團隊執行大規模 RESTful 整合中學到的教訓。

2013 年 11 月 18 日



為何企業整合要使用 REST?

汰換舊系統很困難。事實上,我願意打賭,大規模汰換舊系統是整個 IT 產業中最困難的工作。我們大多數人永遠不會撰寫公司數十年來持續依賴的軟體,但此類軟體在大型汰換舊系統專案中很常見,其作者應該受到讚揚。儘管如此,此類軟體早於我們所學到的許多設計、測試和良好操作實務,因此可能難以理解和變更。

我們經常在進行此類汰換時,想像新系統將具備完美無瑕的架構,並且嚴重低估了此項工作的難度。某些模式會浮現,當在錯綜複雜、自訂建置且難以變更的舊系統背景下檢視時,這些模式是可以理解的。首先,我們購買廠商套件,目標是將內部開發工作減少到整合,並發誓不再依賴沒有外部支援的訂製系統。其次,我們採用服務導向架構進行整合,目標是將新系統的個別部分的替換性納入其中,並減少未來不可避免的汰換舊系統專案的痛苦。

Thoughtworks 已參與過多個大型規模的傳統替換專案,儘管我們並非總能公開討論這些專案。對於許多此類專案而言,HTTP 上的 REST 是一個有吸引力的選項,而且我們通常會倡導使用此選項。它易於使用和理解,而且不需要重量級架構或工具鏈即可開始使用。它很適合測試,而且將許多營運問題減少到管理網站時使用的相同做法。在架構上,REST 已證明具有可擴充性,而且很適合網域建模。

許多關於 REST 的線上討論會深入探討內容類型和超媒體作為應用程式狀態引擎 (HATEOAS) 的細節,但並未提供任何建議,說明在大型整合專案中讓 REST 發揮作用所需的工程和管理實務。我的假設是,此類專案的成功與了解 HATEOAS 的細微差別關係不大,而與了解您的部署和測試策略等面向關係更大。以下是我從執行大型規模 RESTful 整合中學到的教訓。

定義邏輯環境,針對每個需求定義一個

許多大型 IT 組織繼承了大型主機或大型廠商安裝的昂貴環境,並嘗試將服務塞進預先定義的不靈活環境清單中。不幸的是,管理所有開發人員都必須使用的全企業環境集會放棄 RESTful 服務的主要優勢之一:輕量級環境。儘管服務可能是需要大量馬力的應用程式之前的門面,但服務本身通常易於部署和主機,而且可透過瀏覽器和命令列進行測試。此外,使用類似 ATOM 的事件饋送等技術可避免在建立新環境時需要廣泛的中介軟體基礎架構。關鍵見解是了解邏輯環境的概念

邏輯環境是一組適當隔離的相互關聯應用程式、服務和基礎架構元件,用於滿足業務或開發需求。

滿足開發需求所需的元件對於不同的團隊和角色而言可能與滿足業務需求所需的元件大不相同。大型組織中很少有開發人員預期會執行孤立的完整堆疊環境,而且隔離只應做到讓開發人員有生產力即可。例如,在零售專案中,訂單輸入團隊中的開發人員可能需要產品目錄和客戶管理服務,但可能不需要倉庫管理服務。在製作環境中,這些服務可能各有一個負載平衡叢集來支援它們,但開發人員和 QA 人員重視隔離勝於效能和可用性。在極端情況下,不同的開發人員可能在同一 VM 上有不同的邏輯環境。在這種情況下,隔離可透過將埠和資料庫名稱設為環境組態的一部分來完成。

圖 1:環境隔離與主機環境的硬體無關

共用環境的另一個問題是,所有人都會在同一時間升級,這在混亂的開發世界中通常是不適當的。更好的做法是將發布時程交給受其影響的個人處理,這對於生產版本和開發人員在沙盒環境中升級他們依賴的服務同樣適用。這對於 QA 來說特別重要,QA 是一個在邏輯環境中強烈需要管理發布節奏的角色。測試需要已知且穩定的服務版本集,而開發人員在版本已知時會發現修復錯誤更容易。

在一個大型專案中,我們使用 Yaml 定義了環境的宣告式描述。頂層金鑰會定義環境名稱,子金鑰會定義該環境所需的服務,而下一層金鑰會定義每個服務的環境特定設定

order-entry-dev:
  product:
    webservers: [localhost]
    port: 8080
    logPath: /var/log/product
    dbserver: localhost
    dbname: product
  customer:
    webservers: [localhost]
    port: 9080
    logPath: /var/log/customer
    dbserver: localhost
    dbname: customer

無情地關注部署自動化和適當的基礎設施投資,意味著某些服務存在於超過 50 個邏輯環境中,對於習慣大型主機的公司來說,這是一個驚人的數字。像 Ansible 這樣的工具有助於宣告式地描述環境,而無需大量前期投資。為了允許開發人員使用的輕量級臨時環境,通常有助於定義一個以 localhost 作為伺服器名稱的單一環境,可以使用類似 Vagrant 的東西在本地虛擬機器上啟動。這允許環境彈性,方法是使用相同的環境設定但使用不同的 VM。

套件呢?

廠商套件會使環境建立複雜化,因為它們很少被建置為支援簡易部署和環境彈性。其中許多套件會附帶安裝文件,該文件會隨著每次升級而變更,而且沒有可靠的機制可以在多個環境中重播變更。授權也會增加障礙,儘管大多數廠商會允許低成本開發授權。

如果您發現自己負擔著難以部署的廠商套件,則有幾種補救策略。如果套件在安裝期間不需要複雜的授權,您可能可以執行廠商自動化安裝和升級的工作。或者,您可以設定一個可複製的 VM,這會為您提供彈性,但會使升級複雜化。基本上,這是組態管理討論中的 烘焙與油炸 區別。

當兩個選項都不可用時,還有其他方法可以達到一定程度的隔離,儘管沒有任何方法可以與實際的環境隔離相提並論。可能有一種方法是使用應用程式中的自然資料邊界來允許一定程度的開發人員隔離。不同的使用者帳戶往往是一個簡單的資料邊界,儘管使用者傾向於共用全域狀態。更好的方法是為個別開發人員提供不同的租戶,因為多租戶應用程式旨在防止跨租戶流量。這種方法顯然是一種權宜之計,具有擴充挑戰,而且不提供發布時程獨立性。

套件選擇的標準之一應為易於部署和環境管理。

當然,最佳的解決方案是在供應商選擇過程中審查這些營運考量。套件選擇的標準之一應為易於部署和環境管理。在選擇過程中,我們不僅要考慮功能設定和是否符合目的,還要考慮整合的容易度和整合開發人員的生產力。

版本控制應作為最後的手段

邏輯環境定義的一個重要推論是內聚性的概念 - 每個環境應只有一個特定服務的執行個體。不幸的是,在每個團隊進度不同的大型專案中,很容易遇到通常與編譯時間相依性相關的經典菱形相依性問題

圖 2:不相容的版本需求

根據我的經驗,RESTful 架構師會採取的第一個解決方案之一就是版本控管。我採取較具爭議性的觀點。借用 Jamie Zawinski 對正規表示法的著名批評

有些人遇到問題時,會想「我知道了,我要使用版本控管。」現在他們有 2.1.0 個問題。

問題在於版本控管會大幅增加系統的理解、測試和疑難排解的複雜度。只要有多個不相容的服務版本供多個消費者使用,開發人員就必須在所有受支援的版本中維護錯誤修正。如果這些版本在單一程式碼庫中維護,開發人員因為共用程式碼路徑而新增新版本的新功能,就有可能損壞舊版本。如果這些版本獨立部署,營運足跡就會變得更難以監控和支援。這種額外的複雜度不是被忽略,就是以簡化相依服務的發布程序為由合理化。然而,發布的複雜度可以透過有紀律地使用基於消費者的測試(下一節會討論)大幅降低,這是企業 API 可用的誘人選項,但公開 API 卻沒有。

對於許多類型的變更,可以透過其他技術來避免版本控制。波斯定律指出,您應該對接受的內容持開放態度,而對發送的內容持保守態度。這是服務開發的明智建議。不幸的是,某些反序列化器的預設行為會違反此建議,當消費者提供意外欄位時會擲回例外。這很不幸,因為消費者可能只是傳遞額外的診斷資訊,而沒有預期會被使用,或者消費者可能正在準備生產者的未來更新,在生產者準備好處理之前傳遞新的欄位。生產者可能會將新的欄位加入回應主體中,而消費者可以自由地忽略它。這些情況都不會引發例外。

自動反序列化通常會陷入消費者與生產者耦合的陷阱。

通常可以設定反序列化器以提高容忍度。雖然這不是主流建議,但我比較喜歡完全避免自動反序列化。自動反序列化通常會陷入 WSDL 的陷阱,即在兩者中複製靜態類別結構,進而導致消費者與生產者耦合。手動編碼的反序列化也可以減少對輸入資料的假設。正如馬丁·福勒在容忍讀取器中所描述的,使用 XPath 表達式(例如 //order)可以在不中斷反序列化的情況下,在 order 元素上方巢狀變更。

在服務合約設計方面,一點前期設計可以帶來巨大的好處。在一個專案中,合約的屬性大小寫不一致,例如 firstNameLastName。消費者團隊的開發人員在根據合約開發時無疑會暗自咒罵,但當合約在沒有通知的情況下隨後「修復」時,他們會大聲咒罵。

在大型 SOA 專案中,我比較喜歡在服務邊界撰寫許多故事。這確實會導致一個明顯的挑戰,即確保端對端功能與商業目標一致(我稍後會討論這個問題 稍後),但有許多優點。首先,它們自然會讓技術負責人或架構師參與分析,讓他們有時間思考概念的粒度,並模擬合約以形成資源的內聚描述。撰寫驗收標準需要思考各種錯誤狀況和回應碼。在服務邊界進行 QA 檢閱提供了另一個機會來捕捉明顯的錯誤,例如剛剛提到的大小寫問題。最後,與測試驅動開發鼓勵鬆散耦合的方式非常相似,透過確保每個類別至少有兩個消費者(它撰寫的消費者測試)來確保服務端點是可重複使用的,而不是過於特定於最初開發的端對端功能。能夠獨立測試服務端點可以防止它與它支援的端對端工作流程過度緊密耦合。

生產者也可以使用 語意版本控制 來表示他們需要進行重大變更。這是一個簡單的方案,可以為版本的 MAJOR.MINOR.PATCH 部分新增眾所周知的意思,包括為重大變更增加 MAJOR 版本。如果您有一組有紀律的消費者驅動測試(稍後說明),您可能可以在同一個版本上升級所有消費者。當然,這並不總是可行的,而且在某個時候,支援相同服務的多個版本的複雜性可能是合理的,因為協調其依賴項的版本更為複雜。一旦您到達必須使用版本控制的階段,就有兩種主要的技術可供選擇:URL 版本控制和 HTTP 標頭版本控制。在您的選擇中,了解您選擇的版本控制方案首先且最重要的是一個版本管理策略非常重要。

URL 版本控制涉及在 URL 中包含版本號碼(例如 /customers/v1/… - 語意版本控制中的 MAJOR 版本就足夠了)。對於這一點,消費者必須等到生產者發布後。URL 版本控制的優點是能見度很高,並且可以透過瀏覽器進行測試。然而,當一個服務提供連結到另一個服務並預期消費者會追蹤連結時,URL 版本控制會出現一個重要的缺點(這在嘗試使用超媒體來驅動工作流程時最常見)。如果超連結服務升級到新版本,協調此類依賴項的升級可能會很棘手。例如,客戶服務連結到產品服務,而 UI 盲目追蹤該連結,因為版本嵌入在提供的連結中,所以不知道產品版本控制。當我們對產品服務進行非向後相容的升級時,我們最終也希望升級客戶服務以更新連結,但我們必須小心先升級 UI。在這種情況下,最簡單的解決方案通常是同時升級所有三個元件,這實際上與完全不進行版本控制的版本管理策略相同。

鄧肯·博蒙特·克拉格建議簡單地擴展 URL 空間,而不是對其進行版本控制。當您需要進行不兼容的變更時,只需建立一個新資源,而不是對現有的資源進行版本控制。表面上,/customers/v2/profile/customers/extendedProfile 之間有一個小的變更。它們甚至可能以相同的方式實作。然而,從溝通的觀點來看,這兩個選項之間存在著天壤之別。版本控制是一個更廣泛的主題,在大型組織中,版本控制通常需要與多個外部團隊協調,包括架構和發布管理,而團隊往往擁有新增資源的自主權。

HTTP 標頭版本控制將資訊放入 HTTP 標頭中,指出消費者將接受哪個版本。這最常與 Content-Type 關聯,例如 application/vnd.acme.customer-v1+json,它允許內容協商來管理版本。客戶端可以在 Accept 標頭中傳送受支援版本的清單,伺服器可以在 Content-Type 標頭中回應所使用的版本,或針對不受支援的版本要求傳送 415 HTTP 狀態碼。這對純正的 RESTafarian 很有吸引力,並且不受上述 URL 版本控制中提到的缺陷影響,因為最終消費者可以決定要要求哪個版本。當然,透過瀏覽器進行測試變得更加困難,在開發時也更容易被忽略。在請求和回應主體中加入版本號(如果存在)以提供額外的可見性,有助於擴充標頭版本控制。標頭版本控制也會帶來快取的挑戰。Vary 標頭旨在讓同一個 URL 能以不同的方式快取,但它會增加網路組態的複雜性,而且您有遇到忽略 Vary 標頭的錯誤組態網路快取的風險。

使用基於消費者的測試來找出整合問題

基於消費者的測試是我所見過的最有價值的實務之一,它讓 REST 在企業內部擴展,但在我們深入探討之前,我們需要了解部署管線的概念。

部署管道

在他們關於 持續交付 的開創性著作中,傑茲·漢伯和戴夫·法利將部署管線描繪為程式碼從簽入到生產的途徑。如果我們在大型組織中追蹤簽入到生產版本的過程,我們可能會發現以下步驟

  • 開發人員簽入新程式碼。
  • 持續整合工具編譯、封裝並針對原始碼執行單元測試(通常稱為提交階段)。
  • 持續整合工具部署到沙盒環境,以針對已部署服務執行一組自動化測試,同時進行隔離。
  • 應用程式團隊部署到展示環境,由業務利害關係人進行內部使用者驗收。
  • 中央 QA 團隊部署到系統整合測試 (SIT) 環境,他們會與其他應用程式和服務一起進行測試。
  • 發布管理部署到預生產環境,應用程式團隊、安全性及營運會對發布品質進行一些手動驗證。
  • 發布管理部署到生產環境。

將工作流程建模為一系列階段,可提供部署管線,讓我們可以視覺化每個管線階段中服務的版本

圖 3:簡單的部署管線

上面描繪的管線描述了單一服務在隔離中的流程。隨著我們加入整合,大規模 SOA 專案的實際情況會複雜許多

圖 4:整合的部署管線

請注意,在整合的管線中,我在早期階段省略了很多細節。不同的團隊通常有不同的階段,用於團隊面向的管線部分,這些部分可能與真正的外部依賴性隔離。例如,有些團隊可能會新增手動 QA 或效能測試階段。在組織階段(SIT、預生產和生產),所有程式碼以相同方式進行處理是很常見的,這些階段會測試整合在一起的服務。您在管線中走得越遠,整合程度就越高,環境也越接近生產環境。

在管線的早期階段,投資於 stubbing 可以獲得豐厚的回報。當你的建置變為紅色時,知道這是因為團隊內部程式碼中斷,而不是你碰巧正在測試的環境發生變化,這令人感到安慰。使用測試替身有助於消除測試中非決定性的主要原因。像VCR這樣的函式庫允許你記錄真實的服務呼叫,並在自動化測試執行期間稍後重播回應。不過,使用測試替身確實會讓你面臨整合問題,而企業中的大部分複雜性都涉及整合。幸運的是,Ian Robinson 描述了解決整合複雜性的方法,這非常符合我們的部署管線。

基於消費者的測試

基於消費者的測試與直覺相反,因為它依賴於消費者為生產者撰寫測試。在撰寫合約測試時,消費者會針對其使用的服務撰寫測試,以確認服務合約滿足消費者的需求。例如,訂單輸入團隊可能依賴於產品服務的程式碼和說明,以及月費是一個數字,因此他們會撰寫這樣的測試

[Test]
public void ValidateProductAttributes()
{
    var url = UrlForTestProduct();
    var response = new HttpResource(url)
                        .ThatAccepts("application/xml")
                        .Get();

    Assert.That(response.StatusCode, Is.EqualTo(200));
    AssertHasXPath(response.Body, "//productCode");
    AssertHasXPath(response.Body, "//description");
    AssertHasXPath(response.Body, "//monthlyCharge");
    AssertNumeric(ValueFor(response.Body, "//monthlyCharge"));
}


這使得部署管線中有一個巧妙的技巧。在個別服務通過其內部管線後,所有服務和消費者都會經歷一個統一的合約測試階段。它可以由消費者變更測試或生產者提交變更至服務來觸發。每個消費者針對變更服務的新版本執行其測試,任何失敗都會阻止新版本在管線中進行。

在我們的範例中,假設訂單輸入服務的新建置進展到合約測試階段

圖 5:合約測試階段

它依賴於產品和計費服務,由於新程式碼可能已變更其合約測試,因此它針對產品和計費服務的最後一個版本執行其測試,以進入合約測試階段。使用者介面依賴於訂單輸入服務,因此合約測試階段中的使用者介面程式碼的最後一個版本針對訂單輸入執行其測試。這表示合約測試階段需要服務及其消費者測試。由於產品服務沒有依賴關係,因此它沒有消費者測試。讓我們再看一次我們的菱形;這次請注意,只有一個版本的每個服務依賴於。

圖 6:範例合約測試執行

僅觸發與特定變更相關的測試可能會很棘手,但每次將新服務部署到管線的合約測試階段時,只需執行所有合約測試,即可走很長一段路。這將包含 圖 6 中的灰色線條,這些線條與所引入的變更無關。這是測試執行速度與您願意讓相依性管理變得複雜的程度之間的權衡。

假設所有測試都通過,我們現在有一組已證明可以一起運作的服務。我們可以一起記錄它們的集合及其相關版本。

圖 7:成功的合約測試執行

此時,所涉及服務的所有版本都擷取在單一可部署人工製品組,或 DAS 中。此 DAS 可以成為部署管線更高階段的單一可部署人工製品。或者,如果需要支援獨立版本,它可以提供相容性參考。無論哪種方式,它都代表一組已證明使用相同語言的元件。

如果新的訂單輸入程式碼中斷了 UI 使用者測試,則合併的人工製品不會進行。這不表示服務中有問題;可能是使用者誤解合約。或者,可能是生產者故意進行中斷變更,儘管根據語意版本控制禮儀,如果這是故意的,他們應該增加他們的 MAJOR 數字。無論如何,失敗的合約測試會在兩團隊之間觸發對話,而不是在使用者更新其環境後幾天或幾週。

圖 8:中斷合約變更

資料呢?

取得全面的使用者測試時,較困難的挑戰之一是產生有價值的測試資料。上述合約測試假設已知測試產品。我使用 UrlForTestProduct 方法隱藏了此假設,這可能需要特定產品的 URL。對測試時可用的資料進行硬式編碼假設可能會是一種脆弱的方法,因為無法保證該產品在未來會持續存在。此外,某些端點可能需要多個服務之間的資料一致性才能運作。例如,訂單輸入可能會將訂單提交給帳務,其中包含一組相關產品。帳務端點需要有一組一致的產品資料。

其中一個更強健的策略是在測試執行期間建立測試資料,因為這樣可以保證在使用資料之前,資料已經存在。這預設每個服務都允許建立新的資源,但這並不總是如此,雖然在一位客戶處,我們新增了僅限測試的端點,以利於建立測試資料。這些端點並未在製作環境中公開。此策略在上述帳單範例中,仍然可能需要複雜的測試設定。在建立測試產品後,您必須強制與訂單輸入和帳單服務進行同步,而這項操作通常是異步的。另一種策略是讓每個服務發布一組保證穩定的黃金測試資料。這通常最好封裝在一些較高層級的資料邊界中。例如,您可以建立一個跨越所有服務的測試行銷品牌或業務線,其唯一目的是提供不會對製作環境造成影響的假資料。無論如何,爭取測試資料應該是建立強健服務部署管線的首要考量。

不要讓系統獨佔資源

定義資料邊界不佳是架構師可能會犯的最昂貴錯誤之一。一種常見的反模式是嘗試將所有關於實體的資訊儲存在單一資料儲存中,並視需要匯出到依賴系統中,這是一種策略,其鼓勵對主資料管理 (MDM) 有膚淺的誤解。這種策略的問題在於它違反了康威定律,該定律指出軟體架構勢必反映建立它們的組織的結構[1]

我們來看一個產品目錄的範例。在舊系統中,一個團隊輸入新的產品代碼及其相關費率。一個調配團隊使用另一個工具輸入適當的組態,例如下游電話調配系統所需的代碼,以及在另一個應用程式中開啟電視頻道的服務代碼。財務團隊在他們的財務工具中輸入產品的總帳資訊,而發票團隊在另一個不同的應用程式中新增特殊的發票規則。這些團隊之間的溝通是由業務管理,而非技術。

轉換至單一應用程式以取得所有產品資料可能會是一場災難性的練習,主要是因為那些不同的業務團隊對於產品的定義各不相同。客戶服務代表認為的單一產品可能必須拆成兩個才能支援適當的會計。發票團隊非常關注透過簡化發票來降低通話費率,通常需要將兩個產品代碼合併成帳單上的單一行。當然還有行銷部門,他們會肆無忌憚地重新定義產品。嘗試將整個企業對產品的觀點合理化成單一目錄,只會讓該目錄變得脆弱且不靈活。實際上,現在整個企業都必須深入產品目錄才能進行變更。變更的表面積大幅增加,而且變更產生的漣漪效應變得難以推論。康威定律遭到破壞,因為系統的溝通路徑不再代表組織的溝通路徑。

圖 9:資料建模災難

對於通用資料模型,我抱持著極大的懷疑,這些資料模型嘗試將像產品一樣重要的東西的標準表示標準化,並橫跨企業及其整合夥伴。電信產業就建立了這樣的資料模型,稱為 TM Forum 共享資訊/資料模型 (TMF SID)。聲稱透過標準化 SID,不同的公司或公司內的不同部門可以使用相同的術語來描述相同的邏輯實體。我懷疑如此大規模的計畫若沒有某些成功案例就不會持續,但我從未見過。

我建議的解決方案,借用 Eric Evans 的 領域驅動設計,是將每個團隊對產品的定義包裝在受限的脈絡之後。受限的脈絡是一個語言界線,在其中一個術語保證在任何使用的地方都代表相同的意思。在受限的脈絡之外,不存在這樣的保證,而且整合和業務流程的組合必須管理翻譯。現在可以透過符合康威定律的方式,對金融產品與可提供產品的不同之處進行建模。

在封裝周圍提供明確定義的邊界內容是門面服務的一大用途。供應商讓他們的封裝支援多項業務的自然結果是,封裝的功能組可能會超出企業需求。將所有與封裝的通訊包覆在門面服務後方,可讓您限制並將封裝資料塑造成業務流程定義的邊界內容,並提供一個乾淨的 API,將封裝特定的語言轉換成企業定義的語言。

對集中式實體使用一小部分資料

我們在技術上執行此操作的方式是縮減存在於產品目錄中的主資料組,並將該資料複製到其他系統,在這些系統中會擴充該資料。此技術有時會與「旋轉椅整合」相關聯,但兩者並不相同。在真正的旋轉椅整合中,會在未直接整合的多個應用程式中輸入相同資訊。在邊界內容中,主資料會被複製,並在相依的應用程式中依據內容擴充和轉換。

定義產品等中央資源實體的關鍵是了解建立新產品的業務團隊認為產品是什麼。在我們的假設範例中,他們決定產品定義一些基本費率和所有部門使用的常見描述性資訊。這讓我們在產品服務中定義我們的產品:產品是我們可以銷售給客戶且具有單一費率的商品。在舊系統中,在建立產品後,產品團隊會寄送電子郵件給其他部門。此事件很可能值得自動化,我們可以透過在我們的服務中公開佇列或變更饋送來執行此操作。不過,我們不應假裝我們可以自動化此事件觸發的業務流程,也不應將該業務流程移至中央目錄中。

當財務部門收到時,他們必須決定如何分解產品。假設它是一個以折扣價購買的電視體育頻道套裝,這個概念完全符合產品團隊對產品的定義。然而,財務部門需要確保套裝中的每個體育頻道都能獲得版權費,因此他們需要將單一費率發送到不同的總帳科目。現在我們有了財務定義:產品是將收入與總帳科目關聯起來。在剛剛描述的使用案例中,我們可以想像財務應用程式收到NewProduct事件,並提供使用者介面讓使用者將收入的一部分分配到帳戶。

每個業務部門都有不同的模型,用於在它們的限定上下文中明確轉換的常見實體

當帳單部門收到事件時,他們需要決定是否要按比例分配套裝。也許,由於擔心太多客戶訂購按比例分配的體育頻道,卻在觀看大賽後隔天就取消訂閱,因此他們決定這個特定的體育套裝需要預先支付一個月的費用。我們從產品的定義開始,即向客戶收取經常性費用,並增加一組可能很複雜的設定,而這些設定超出了簡單的月費。

發票部門將產品定義為帳單上的明細。他們可能會決定,也許是由於行銷策略驅動,購買兩個不同體育套裝的客戶應該只在帳單上看到一行名為「超級體育套裝」的明細,並顯示總金額。同樣地,我們可以想像一個應用程式促進接收NewProduct事件並允許進行這種合併,或者我們可以想像開發人員在引入這些產品時編寫新的規則。

圖 10:使用限定上下文的範例整合

這個範例顯示了四個不同的限定上下文,以及使用通知饋送傳播NewProduct事件,這是一種常見的 RESTful 方法。當建立新產品時,產品服務會將新產品記錄為事件,並在端點上公開該事件。使用者會輪詢端點以接收自上次輪詢以來發生的所有事件,而端點可能類似於/notifications?since=2013-10-01

使用史詩來協調業務功能

稍早,我建議將服務端點視為故事邊界,但前提是我們可能會失去傳統敏捷故事的好處,也就是它們與業務功能保持一致。由於團隊以不同的優先順序和速度工作,因此這個問題在大型 SOA 中會更加嚴重,我們有風險會見樹不見林。想像一個業務功能,用於對產品的第一個月進行計費。真正的業務流程可能需要在該點之前進行一系列服務呼叫,包括建立客戶、查詢產品、建立訂單和現場技術人員核准。在規模上,這些服務端點將由不同的團隊實作。

敏捷工具箱始終包含史詩,用於沿著單一高階功能協調故事群組。我建議將它們視為大規模 SOA 程式管理的一級公民。事實上,我相信我們附加使用者故事的許多儀式都應該在這種專案的史詩層級,因為史詩通常代表我們對需求的商業友善帳戶。

例如,一個建立客戶的史詩可能涉及訂單輸入和帳單團隊,以及客戶管理團隊,每個團隊都有個別的服務導向或特定應用程式的追蹤工作故事。在我們假設的範例中,服務是由一個獨立團隊開發的使用者介面所使用,我們可能無法在完成完整的系統流程之前向業務展示我們工作的成果。

一些史詩的照護和管理可以從較小專案的故事實務中提升。取得史詩的架構審查以定義跨功能需求,以及讓商業分析師定義驗收需求,有助於將整體概況放在心上。跨團隊展示應該在史詩層級管理,而這些可能是展示實際業務使用者流程的第一個展示。

程式層級指標將史詩視為追蹤速度的主要指標,因為團隊使用者故事速度可能會造成進度錯覺。

重要的考量是程式層級指標將史詩視為追蹤速度的主要指標,因為團隊使用者故事速度可能會造成進度錯覺。要觀察的症狀是,當速度燃盡圖顯示程式準時交付,但似乎什麼都不起作用。我參與過一個專案,根據個別團隊追蹤的個別故事速度,就是這種情況。有些團隊準時,其他團隊稍微落後進度,但我們在幾個月的開發後無法向業務展示任何內容。僅將程式層級燃盡圖變更為顯示已完成的史詩數量,而不是已完成的故事數量,就能清楚說明。儘管個別團隊顯示出顯著進展,我們只完成了一個史詩。更糟的是,在所有史詩中至少有三分之二的完整故事需要同時發布。傳統的軟體看板方法嘗試限制故事層級的進行中工作。當我們了解問題的範圍時,我們能夠透過重新排列故事開發來修正課程,以限制同時進行中的史詩數量。

總結

無論技術或架構為何,擴充軟體開發都是棘手的業務。我們經常假裝「只是整合」來欺騙自己,否則會認為這很簡單。Eric Evans 曾經說過,在任何大型系統中,其中一些將會設計不良。我的經驗,即使是技術高超的團隊,也讓我相信他是對的。因此,我們整合的主要目標是確保我們將自己與其他子系統的設計隔離。

我倡導採用 RESTful 服務整合策略。我相信 REST 可簡化開發,而且由於 RESTful 訊息往往具有自述性,因此可簡化測試和疑難排解。然而,它遠非某些人想像中的萬靈丹,而且大規模的 RESTful 整合需要重視上述說明的教訓。我自己的經驗讓我相信

  • 環境隔離很重要。重視部署自動化至關重要。選擇忽略良好部署實務的套件會拖慢所有需要與該套件整合的人員。
  • 版本控制過早會為系統增加不必要的複雜性。例如容忍反序列化和基於端點的故事分析等實務有助於延後版本控制,即使您隨後加入語意版本控制支援,它們仍然是有用的實務。
  • 使用消費者測試可大幅降低升級一組相互依賴服務的版本管理複雜性,並進一步延後版本控制。
  • 嘗試讓服務控制所有關於實體的資料會造成災難。不要忽略業務流程和企業對實體的不同定義。
  • 大規模協調業務功能不太可能發生在使用者故事層級。史詩是對業務版本進行排序所必需的。

在 RESTful 討論中,超媒體、內容協商和統一介面備受關注,它們是有價值的技術,但要讓整合解決方案具備擴充性,我們必須跳脫 REST 的機制,並探討社會和組織問題。成功應對此類問題並不表示每個個別服務或元件都會設計得很好。它確實表示您將制定穩固的實務,以逐步傳遞業務價值,並確保整合層具有適當的穩健性,而這些實務可能會決定傳遞是否成功。


致謝

特別感謝 Martin Fowler、Damien Del Russo、Danilo Sato、Duncan Beaumont Cragg 和 Jennifer Smith 對本文的早期回饋。許多想法來自於我利用 Thoughtworks 同事作為不同專案的討論對象。雖然這裡無法一一列舉,但我想要特別點出 Ryan Murray、Mike Mason 和 Manoj Mahalingam,因為他們對本文提出的部分想法有特別的影響。

腳註

1: 康威定律

康威定律有許多不同的觀點,有些人認為它純粹是描述性的同義反覆。我更傾向於使用維基百科將詹姆斯·科普林和尼爾·哈里森聯繫在一起的變體。它確實可能是組織內軟體的描述性定律,但我相信這僅僅是因為試圖違背康威定律的軟體註定會失敗。

重大修訂

2013 年 11 月 18 日:新增選項以擴充 URL 空間以避免版本控制,並新增腳註以釐清康威定律的使用方式

2013 年 11 月 12 日:新增使用史詩的章節,從而完成初始發布

2013 年 11 月 8 日:新增反對讓系統壟斷資源的章節

2013 年 10 月 31 日:新增基於消費者的測試章節

2013 年 10 月 24 日:新增版本控制章節

2013 年 10 月 21 日:發布第一版,包含邏輯環境章節