- 鳳凰架構:構建可靠的大型分布式系統
- 周志明
- 2277字
- 2021-06-24 11:30:53
2.2.3 RMM
前面我們花費大量篇幅討論了REST的思想、概念和指導原則等理論方面的內容,在本節中,我們將把重心放在實踐上,把目光從整個軟件架構設計進一步聚焦到REST接口設計上,以切合2.2節的標題,也順帶填了前面埋下的“如何評價服務是否RESTful”的坑。
RESTful Web APIs和RESTful Web Services的作者Leonard Richardson曾提出一個衡量“服務有多么REST”的Richardson成熟度模型(Richardson Maturity Model,RMM),以便讓那些原本不使用REST的系統,能夠逐步地導入REST。Richardson將服務接口“REST的程度”從低到高,分為0至3級。
·第0級(The Swamp of Plain Old XML):完全不REST。
·第1級(Resources):開始引入資源的概念。
·第2級(HTTP Verbs):引入統一接口,映射到HTTP協議的方法上。
·第3級(Hypermedia Controls):超媒體控制,在本文里面的說法是“超文本驅動”,在Fielding論文里的說法是“Hypertext As The Engine Of Application State,HATEOAS”,其實都是指同一件事情。
下面筆者借用Martin Fowler撰寫的關于RMM的文章中的實際例子(原文是XML寫的,這里簡化為JSON表示),來具體展示一下四種不同程度的REST反映到實際接口中會是怎樣的。假設你是一名軟件工程師,接到的需求(原文中的需求復雜一些,這里簡化了)描述是這樣的:
醫生預約系統
作為一名病人,我想要從系統中得知指定日期內我熟悉的醫生是否具有空閑時間,以便于我向該醫生預約就診。
第0級
醫院開放了一個/appointmentService的Web API,傳入日期、醫生姓名等參數,可以得到該時間段內該名醫生的空閑時間,該API的一次HTTP調用如下所示:
POST /appointmentService?action=query HTTP/1.1 {date: "2020-03-04", doctor: "mjones"}
然后服務器會傳回一個包含了所需信息的回應:
HTTP/1.1 200 OK [ {start:"14:00", end: "14:50", doctor: "mjones"}, {start:"16:00", end: "16:50", doctor: "mjones"} ]
得到了醫生空閑的結果后,筆者覺得14:00比較合適,于是進行預約確認,并提交了個人基本信息:
POST /appointmentService?action=confirm HTTP/1.1 { appointment: {date: "2020-03-04", start:"14:00", doctor: "mjones"}, patient: {name: icyfenix, age: 30, ……} }
如果預約成功,那我能夠收到一個預約成功的響應:
HTTP/1.1 200 OK { code: 0, message: "Successful confirmation of appointment" }
如果出現問題,譬如有人在我前面搶先預約了,那么我會在響應中收到某種錯誤消息:
HTTP/1.1 200 OK { code: 1 message: "doctor not available" }
至此,整個預約服務宣告完成,直接明了,我們采用的是非常直觀的基于RPC風格的服務設計,似乎很容易就解決了所有問題,但真的是這樣嗎?
第1級
第0級是RPC的風格,如果需求永遠不會變化,那它完全可以良好地工作下去。但是,如果你不想為預約醫生之外的其他操作、為獲取空閑時間之外的其他信息去編寫額外的方法,或者改動現有方法的接口,那還是應該考慮一下如何使用REST來抽象資源。
通往REST的第一步是引入資源的概念,在API中的基本體現是圍繞資源而不是過程來設計服務,說得直白一點,可以理解為服務的Endpoint應該是一個名詞而不是動詞。此外,每次請求中都應包含資源的ID,所有操作均通過資源ID來進行,譬如,獲取醫生指定時間的空閑檔期:
POST /doctors/mjones HTTP/1.1 {date: "2020-03-04"}
然后服務器傳回一組包含了ID信息的檔期清單,注意,ID是資源的唯一編號,有ID即代表“醫生的檔期”被視為一種資源:
HTTP/1.1 200 OK [ {id: 1234, start:"14:00", end: "14:50", doctor: "mjones"}, {id: 5678, start:"16:00", end: "16:50", doctor: "mjones"} ]
筆者還是覺得14:00的時間比較合適,于是又進行預約確認,并提交了個人基本信息:
POST /schedules/1234 HTTP/1.1 {name: icyfenix, age: 30, ……}
后面預約成功或者失敗的響應消息在這個級別里面與之前一致,就不重復了。比起第0級,第1級的特征是引入了資源,通過資源ID作為主要線索與服務交互,但第1級至少還有三個問題沒有解決:一是只處理了查詢和預約,如果臨時想換個時間,要調整預約,或者病忽然好了,想刪除預約,這都需要提供新的服務接口;二是處理結果響應時,只能依靠結果中的code、message這些字段做分支判斷,每一套服務都要設計可能發生錯誤的code,這很難考慮全面,而且也不利于對某些通用的錯誤做統一處理;三是沒有考慮認證授權等安全方面的內容,譬如要求只有登錄用戶才允許查詢醫生檔期時間,某些醫生可能只對VIP開放,需要特定級別的病人才能預約,等等。
第2級
第1級遺留的三個問題都可以通過引入統一接口來解決。HTTP協議的七個標準方法是經過精心設計的,只要架構師的抽象能力夠用,它們幾乎能涵蓋資源可能遇到的所有操作場景。REST的具體做法是:把不同業務需求抽象為對資源的增加、修改、刪除等操作來解決第一個問題;使用HTTP協議的Status Code,它可以涵蓋大多數資源操作可能出現的異常,也可以自定義擴展,以此解決第二個問題;依靠HTTP Header中攜帶的額外認證、授權信息來解決第三個問題,這個在實戰中并沒有體現,后文會在5.3節中介紹相關內容。
按這個思路,獲取醫生檔期,應采用具有查詢語義的GET操作進行:
GET /doctors/mjones/schedule?date=2020-03-04&status=open HTTP/1.1
然后服務器會傳回一個包含了所需信息的回應:
HTTP/1.1 200 OK [ {id: 1234, start:"14:00", end: "14:50", doctor: "mjones"}, {id: 5678, start:"16:00", end: "16:50", doctor: "mjones"} ]
筆者仍然覺得14:00的時間比較合適,于是進行預約確認,并提交了個人基本信息,用以創建預約,這是符合POST的語義的:
POST /schedules/1234 HTTP/1.1 {name: icyfenix, age: 30, ......}
如果預約成功,那筆者能夠收到一個預約成功的響應:
HTTP/1.1 201 Created Successful confirmation of appointment
如果出現問題,譬如有人搶先預約了,那么筆者會在響應中收到某種錯誤消息:
HTTP/1.1 409 Conflict doctor not available
第3級
第2級是目前絕大多數系統所到達的REST級別,但仍不是完美的,至少還存在一個問題:你是如何知道預約mjones醫生的檔期是需要訪問“/schedules/1234”這個服務Endpoint的?也許你第一時間甚至無法理解為何我會有這樣的疑問,這當然是程序代碼寫的呀!但REST并不認同這種已烙在程序員腦海中許久的想法。RMM中的超文本控制、Fielding論文中的HATEOAS和現在提的比較多的“超文本驅動”,所希望的是除了第一個請求是由你在瀏覽器地址欄輸入驅動之外,其他的請求都應該能夠自己描述清楚后續可能發生的狀態轉移,由超文本自身來驅動。所以,當你輸入了查詢的指令之后:
GET /doctors/mjones/schedule?date=2020-03-04&status=open HTTP/1.1
服務器傳回的響應信息應該包括諸如如何預約檔期、如何了解醫生信息等可能的后續操作:
HTTP/1.1 200 OK { schedules:[ { id: 1234, start:"14:00", end: "14:50", doctor: "mjones", links: [ {rel: "comfirm schedule", href: "/schedules/1234"} ] }, { id: 5678, start:"16:00", end: "16:50", doctor: "mjones", links: [ {rel: "comfirm schedule", href: "/schedules/5678"} ] } ], links: [ {rel: "doctor info", href: "/doctors/mjones/info"} ] }
如果做到了第3級REST,那服務端的API和客戶端也是完全解耦的,此時如果你要調整服務數量,或者對同一個服務做API升級時將會變得非常簡單。