理查森成熟度模型
邁向 REST 榮耀的步驟
一個由 Leonard Richardson 開發的模型,將 REST 方法的主要元素分解為三個步驟。這些步驟引入了資源、HTTP 動詞和超媒體控制。
2010 年 3 月 18 日
最近我一直在閱讀 Rest In Practice 的草稿:這是我的幾位同事一直在編寫的書。他們的目標是說明如何使用 Restful 網路服務來處理企業面臨的許多整合問題。這本書的核心概念是,網路是一個規模龐大、運作良好的分散式系統的實證,我們可以從中汲取想法,更輕鬆地建置整合系統。

圖 1:邁向 REST 的步驟
為了說明網路樣式系統的特定屬性,作者使用了一個由 Leonard Richardson 開發並在 QCon 演講中 說明 的 restful 成熟度模型。這個模型是很棒的思考方式,可以運用這些技術,所以我決定自己試著說明一下。(這裡的通訊協定範例僅供說明,我認為編寫程式碼並測試它們沒有價值,因此細節部分可能會有問題。)
第 0 級
模型的起點是使用 HTTP 作為遠端互動的傳輸系統,但不會使用任何網路機制。基本上,您在此所做的就是使用 HTTP 作為您自己的遠端互動機制的隧道機制,通常基於 遠端程序呼叫。

圖 2:第 0 層級範例互動
假設我想預約看醫生。我的預約軟體首先需要知道我的醫生在特定日期有哪些空檔,因此它會向醫院預約系統提出要求以取得該資訊。在第 0 層級情況下,醫院會在某些 URI 公開服務端點。然後,我將包含我的要求詳細資料的文件貼到該端點。
POST /appointmentService HTTP/1.1 [various other headers] <openSlotRequest date = "2010-01-04" doctor = "mjones"/>
然後伺服器會傳回提供此資訊的文件
HTTP/1.1 200 OK [various headers] <openSlotList> <slot start = "1400" end = "1450"> <doctor id = "mjones"/> </slot> <slot start = "1600" end = "1650"> <doctor id = "mjones"/> </slot> </openSlotList>
我在這裡使用 XML 作為範例,但內容實際上可以是任何東西:JSON、YAML、鍵值對或任何自訂格式。
我的下一步是預約,我也可以透過將文件貼到端點來執行此動作。
POST /appointmentService HTTP/1.1 [various other headers] <appointmentRequest> <slot doctor = "mjones" start = "1400" end = "1450"/> <patient id = "jsmith"/> </appointmentRequest>
如果一切順利,我會收到回應,表示我的預約已預訂。
HTTP/1.1 200 OK [various headers] <appointment> <slot doctor = "mjones" start = "1400" end = "1450"/> <patient id = "jsmith"/> </appointment>
如果發生問題,例如有人在我之前預約,則我會在回覆主體中收到某種類型的錯誤訊息。
HTTP/1.1 200 OK [various headers] <appointmentRequestFailure> <slot doctor = "mjones" start = "1400" end = "1450"/> <patient id = "jsmith"/> <reason>Slot not available</reason> </appointmentRequestFailure>
到目前為止,這是一個簡單的 RPC 型式系統。它很簡單,因為它只是來回傳遞純舊 XML (POX)。如果您使用 SOAP 或 XML-RPC,它基本上是相同的機制,唯一的區別是您將 XML 訊息包裝在某種類型的信封中。
第 1 級 - 資源
邁向 RMM 中 Rest 榮耀的第一步是引入資源。因此,現在我們不再對單一服務端點提出所有要求,而是開始與個別資源對話。

圖 3:第 1 層級加入資源
因此,對於我們的初始查詢,我們可能有一個給定醫生的資源。
POST /doctors/mjones HTTP/1.1 [various other headers] <openSlotRequest date = "2010-01-04"/>
回覆傳遞相同的基本資訊,但每個時段現在都是可以個別處理的資源。
HTTP/1.1 200 OK [various headers] <openSlotList> <slot id = "1234" doctor = "mjones" start = "1400" end = "1450"/> <slot id = "5678" doctor = "mjones" start = "1600" end = "1650"/> </openSlotList>
使用特定資源預約表示貼到特定時段。
POST /slots/1234 HTTP/1.1 [various other headers] <appointmentRequest> <patient id = "jsmith"/> </appointmentRequest>
如果一切順利,我會收到類似之前的回覆。
HTTP/1.1 200 OK [various headers] <appointment> <slot id = "1234" doctor = "mjones" start = "1400" end = "1450"/> <patient id = "jsmith"/> </appointment>
現在不同的是,如果有人需要對預約做任何事,例如預約一些檢查,他們會先取得預約資源,其 URI 可能類似於 http://royalhope.nhs.uk/slots/1234/appointment
,並將資料傳送到該資源。
對像我這樣的物件導向愛好者來說,這就像物件識別的概念。我們不是呼叫以太中的某些函式並傳遞引數,而是呼叫特定物件上的方法,並提供其他資訊的引數。
第 2 級 - HTTP 動詞
我在第 0 和第 1 層的所有互動中都使用 HTTP POST 動詞,但有些人會使用 GET 或同時使用。在這些層級中,這沒有太大的差別,它們都被用作通道機制,允許您透過 HTTP 通道您的互動。第 2 層會遠離此機制,盡可能貼近 HTTP 本身的使用方式來使用 HTTP 動詞。

圖 4:第 2 層新增 HTTP 動詞
對於時段清單,這表示我們要使用 GET。
GET /doctors/mjones/slots?date=20100104&status=open HTTP/1.1 Host: royalhope.nhs.uk
回覆與 POST 時相同
HTTP/1.1 200 OK [various headers] <openSlotList> <slot id = "1234" doctor = "mjones" start = "1400" end = "1450"/> <slot id = "5678" doctor = "mjones" start = "1600" end = "1650"/> </openSlotList>
在第 2 層,對於此類要求使用 GET 至關重要。HTTP 將 GET 定義為安全操作,也就是說它不會對任何事物的狀態做出任何重大變更。這讓我們可以安全地多次呼叫 GET,順序不限,每次都能獲得相同的結果。這項重要的結果是,它允許路由要求中的任何參與者使用快取,這是讓網路效能表現良好的關鍵要素。HTTP 包含各種支援快取的措施,所有通訊參與者都可以使用這些措施。透過遵循 HTTP 的規則,我們可以利用該功能。
要預約,我們需要一個會變更狀態的 HTTP 動詞,POST 或 PUT。我會使用我之前使用的相同 POST。
POST /slots/1234 HTTP/1.1 [various other headers] <appointmentRequest> <patient id = "jsmith"/> </appointmentRequest>
在此使用 POST 和 PUT 之間的權衡利弊是我不想深入探討的,也許我哪天會針對它們撰寫一篇獨立的文章。但我確實想指出,有些人錯誤地將 POST/PUT 與建立/更新對應起來。它們之間的選擇與此截然不同。
即使我使用與層級 1 相同的貼文,遠端服務回應的方式仍有另一個顯著差異。如果一切順利,服務會回覆回應代碼 201,以表示世界上有一個新的資源。
HTTP/1.1 201 Created Location: slots/1234/appointment [various headers] <appointment> <slot id = "1234" doctor = "mjones" start = "1400" end = "1450"/> <patient id = "jsmith"/> </appointment>
201 回應包含一個位置屬性,其中包含一個 URI,供用戶端在未來用於 GET 該資源的目前狀態。此處的回應也包含該資源的表示,以節省用戶端現在額外的呼叫。
如果發生問題,例如其他人預訂了時段,則會有另一個差異。
HTTP/1.1 409 Conflict [various headers] <openSlotList> <slot id = "5678" doctor = "mjones" start = "1600" end = "1650"/> </openSlotList>
此回應的重要部分是使用 HTTP 回應代碼來表示發生問題。在這種情況下,409 似乎是一個不錯的選擇,表示其他人已經以不相容的方式更新了資源。與其使用 200 回應代碼,但包含錯誤回應,在層級 2 中,我們明確使用類似這樣的某種錯誤回應。由通訊協定設計者決定要使用哪些代碼,但如果發生錯誤,應該要有非 2xx 回應。層級 2 介紹使用 HTTP 動詞和 HTTP 回應代碼。
這裡出現了一個不一致的情況。REST 倡導者談論使用所有 HTTP 動詞。他們也透過表示 REST 嘗試從網際網路的實際成功中學習,來證明他們的做法。但實際上,世界網際網路很少使用 PUT 或 DELETE。有合理的理由可以更常使用 PUT 和 DELETE,但網際網路的存在證明並非其中之一。
網際網路的存在所支援的主要元素是安全(例如 GET)和非安全操作之間的強大區分,以及使用狀態碼來協助傳達您遇到的錯誤類型。
第 3 級 - 超媒體控制
最後一層介紹了一個您經常聽到的東西,其醜陋的縮寫為 HATEOAS(超文字作為應用程式狀態的引擎)。它解決了如何從開放時段清單得知預約所需執行的動作的問題。

圖 5:層級 3 新增超媒體控制項
我們從與第 2 層中傳送的相同初始 GET 開始
GET /doctors/mjones/slots?date=20100104&status=open HTTP/1.1 Host: royalhope.nhs.uk
但回應有一個新元素
HTTP/1.1 200 OK [various headers] <openSlotList> <slot id = "1234" doctor = "mjones" start = "1400" end = "1450"> <link rel = "/linkrels/slot/book" uri = "/slots/1234"/> </slot> <slot id = "5678" doctor = "mjones" start = "1600" end = "1650"> <link rel = "/linkrels/slot/book" uri = "/slots/5678"/> </slot> </openSlotList>
每個時段現在都有連結元素,其中包含 URI,用來告訴我們如何預約。
超媒體控制的重點在於,它們會告訴我們接下來可以做什麼,以及我們需要操作的資源 URI。我們不必知道要將預約要求張貼到何處,因為回應中的超媒體控制會告訴我們如何執行。
POST 會再次複製第 2 層的 POST
POST /slots/1234 HTTP/1.1 [various other headers] <appointmentRequest> <patient id = "jsmith"/> </appointmentRequest>
而回覆會包含多個超媒體控制,用於執行接下來的不同事項。
HTTP/1.1 201 Created Location: http://royalhope.nhs.uk/slots/1234/appointment [various headers] <appointment> <slot id = "1234" doctor = "mjones" start = "1400" end = "1450"/> <patient id = "jsmith"/> <link rel = "/linkrels/appointment/cancel" uri = "/slots/1234/appointment"/> <link rel = "/linkrels/appointment/addTest" uri = "/slots/1234/appointment/tests"/> <link rel = "self" uri = "/slots/1234/appointment"/> <link rel = "/linkrels/appointment/changeTime" uri = "/doctors/mjones/slots?date=20100104&status=open"/> <link rel = "/linkrels/appointment/updateContactInfo" uri = "/patients/jsmith/contactInfo"/> <link rel = "/linkrels/help" uri = "/help/appointment"/> </appointment>
超媒體控制的一個顯著好處是,它允許伺服器變更其 URI 架構,而不會中斷用戶端。只要用戶端查詢「addTest」連結 URI,伺服器團隊就可以處理除了初始進入點以外的所有 URI。
另一個好處是,它有助於用戶端開發人員探索通訊協定。連結會提供用戶端開發人員提示,讓他們知道接下來可能可以做什麼。它不會提供所有資訊:「self」和「cancel」控制都會指向相同的 URI - 他們需要找出其中一個是 GET,而另一個是 DELETE。但至少它會提供一個起點,讓他們知道要思考哪些資訊,以及在通訊協定文件尋找類似的 URI。
同樣地,它允許伺服器團隊透過在回應中放置新連結,來宣傳新功能。如果用戶端開發人員持續注意未知連結,這些連結可以成為進一步探索的觸發因素。
沒有絕對的標準來表示超媒體控制。我這裡所做的是使用 REST in Practice 團隊目前的建議,也就是遵循 ATOM (RFC 4287)。我使用 <link>
元素,其中 uri
屬性用於目標 URI,而 rel
屬性用於描述關係類型。眾所周知的關係(例如用於參考元素本身的 self
)是明確的,任何特定於該伺服器的關係都是完全限定的 URI。ATOM 指出,眾所周知連結關係的定義是 連結關係註冊。在我撰寫本文時,這些定義僅限於 ATOM 所做的定義,而 ATOM 通常被視為第 3 層 REST 的領導者。
各級別的意義
我應該強調,RMM 雖然是思考 REST 元素的好方法,但它本身並非 REST 層級的定義。Roy Fielding 明確表示 第 3 層 RMM 是 REST 的先決條件。與軟體中的許多術語一樣,REST 有許多定義,但由於 Roy Fielding 創造了這個術語,因此他的定義應該比大多數定義更具權威性。
我發現這個 RMM 有用的地方在於,它提供了一個循序漸進的方式來理解 restful 思維背後的基本概念。因此,我認為它是一個幫助我們瞭解這些概念的工具,而不是應該用於某種評估機制的東西。我認為我們目前還沒有足夠的範例來真正確定 restful 方法是整合系統的正確方式,但我確實認為這是一個非常有吸引力的方法,也是我在大多數情況下會推薦的方法。
與 Ian Robinson 討論這件事時,他強調了當 Leonard Richardson 最初提出這個模型時,他發現這個模型有吸引力的地方,在於它與常見的設計技術之間的關係。
- 第 1 級透過使用分而治之來處理複雜性的問題,將大型服務端點分解成多個資源。
- 第 2 級引入了一組標準動詞,以便我們以相同的方式處理類似的狀況,消除了不必要的變異。
- 第 3 級引入了可發現性,提供了一種讓協定更具自文件化的方式。
結果是一個模型,它幫助我們思考我們想要提供的 HTTP 服務類型,並架構出想要與之互動的人們的期望。
致謝
Savas Parastatidis、Ian Robinson 和 Jim Webber 對草稿提出了實質性的評論。Leonard Richardson 在回答我的問題時非常有幫助,讓我可以將對他想法的誤解降到最低。Aaron Swartz 修正了我第 3 級 URI 中的一些錯誤。
重大修訂
2010 年 3 月 18 日:最初發布