官术网_书友最值得收藏!

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升級時將會變得非常簡單。

主站蜘蛛池模板: 新建县| 崇州市| 磴口县| 祥云县| 建昌县| 望城县| 资源县| 大渡口区| 名山县| 马龙县| 仁寿县| 新绛县| 武清区| 凤冈县| 永州市| 泰安市| 莆田市| 朔州市| 昆明市| 霍邱县| 盐源县| 闽侯县| 自贡市| 玛沁县| 明溪县| 名山县| 保德县| 会泽县| 黄梅县| 浑源县| 汝南县| 尼木县| 女性| 民县| 璧山县| 广宗县| 炎陵县| 灵台县| 阳曲县| 贵德县| 永德县|