在過去幾年里,REST成為了一種被廣泛使用,甚至被濫用的架構流行語。和SOA一樣,不同的人對於REST有不同的理解。有人認為REST就是使用HTTP來直接發送XML的,但並不采用SOAP規范;還有人采用相似的方法來解釋道:REST就是用HTTP來發送JSON數據的(剛開始我也這樣認為);還有人則認為在使用REST時我們需要將URI查詢參數傳遞給方法。以上所有對於REST的解釋都是錯誤的。但和SOA不同的的是,Roy T.Fielding在他的博士論文中對REST做出了權威的概念定義。
REST是一種架構風格
在使用REST之前,我們首先需要理解什么是架構風格。架構風格之於架構就像設計模式之於設計一樣。它將不同架構實現所共有的東西抽象出來,使得我們在談及到架構時不至於陷入技術細節中。分布式系統架構存在多種架構風格,包括客戶端-服務器架構風格和分布式對象風格。Field論文的前幾個章節對有些架構風格做了解釋,包括強加在每種風格上的各種約束。你可能會認為該論文中對於架構風格的解釋和約束有些理論化。在這一點上,你可能是正確的。這些理論構成了Field所提出的REST架構風格的基礎。REST本來就應該是屬於Web架構的一種架構風格。
當然,Web——體現為URI、HTTP和HTML——先於Field的博士論文而出現。但是,Field是制定HTTP1.1標准的主要貢獻者之一,他對Web在發展過程中的設計決策也產生過巨大的影響。這樣看來,REST是對Web架構的理論擴展。
那么現在,我們為什么將REST作為構建系統的另一種方式呢?或者更嚴格地說,是構建Web服務的一種方式。原因在於,和其他技術一樣,我們可以通過不同的方式來使用Web協議。有些使用方式符合設計者的初衷,而有些就不見得了。比如,關系型數據庫管理系統便是一例。你可以根據原本的架構風格來使用RDBMS,即定義不同的數據庫表,再定義不同的列、外鍵關聯、視圖和約束等。你也可以只創建一張表,其中只含有兩列,一列為表示“鍵”,一列表示“值”,然后將序列化之后的對象保存在值列中。此時,你依然在使用RDBMS,但是你卻使用不到多少RDMBS提供的功能,比如查詢、組合、排序和分組等。
同樣的道理,Web協議既可以按照它原先的設計初衷為人所用——此時便是一種遵循REST架構風格的方式——也可以通過一種不遵循其設計初衷的方式為人所用。因此,在我們沒有獲得(或可以說成是“不需要”)由使用“REST”風格的HTTP所帶來的好處時,另一種不同的分布式系統架構可能是合適的,就像在保存擁有唯一鍵的數值時,NoSQL/鍵值對存儲方式是一種更好的選擇一樣。
RESTful HTTP服務器的關鍵方面
那么,對於采用“RESTful HTTP”的分布式系統來說,它具有哪些關鍵方面呢?讓我們先來看看服務器端。請注意,在我們討論服務器端時,無論客戶是操作Web瀏覽器的某個人,還是由編程言開發的客戶端程序,對它們都是同等處理的,沒有什么區別。
首先,就像其名字所指出的,資源是關鍵的概念。作為一個系統設計者,你決定哪些有意義的“東西”可以暴露給外界,並且給這些“東西”一個唯一的身份標識。通常來說,每種資源都擁有一個URI,更重要的是,每個URI都需要指向某個資源——即你向外界暴露的“東西”。比如,你可能會做出這樣的決定:每一個客戶、產品、產品列表、搜索結果和每次對產品目錄的修改都應該分別作為一種資源。資源是具有展現和狀態的,這些展現的格式可能不同。客戶通過資源的展現與服務器交互,格式可以為XML、JSON、HTML或二進制數據。
另一個關鍵方面是無狀態通信,此時我們將采用具有自描述功能的消息。比如,HTTP請求便包含了服務器所需的所有信息。當然,服務器也可以使用其本身的狀態來輔助通信,但是重要的是:我們不能依靠請求本身來創建一個隱式上下文環境(對話)。無狀態通信保證了不同請求之間的相互獨立性,這在很大程度上提高了系統的可伸縮性。
如果你將資源看作對象——這是合理的——那么你應該問問它們應該擁有什么樣的接口。這個問題的答案是REST的另一個關鍵面,它將REST與其他架構風格區別開來。你可以調用的方法集合是固定的。每一個對象都支持相同的接口。在RESTful HTTP中,對象方法便可以表示為可以操作資源的HTTP動詞,其中最重要的有GET、PUT、POST和DELETE。
雖然乍一看這些方法將會轉化成CRUD操作,但是事實卻並非如此。通常,我們所創建的資源並不表示任何持久化實體,而是封裝了某種行為,當我們將HTTP動詞應用在這些資源上時,我們實際上是在調用這些行為。在HTTP規范中,每種HTTP方法都有一個明確的定義。比如,GET方法只能用於“安全”的操作:(1)它不修改數據;(2)它總是讀取數據;(3)它讀取到的數據可能被緩存起來。
SOAP風格Web服務的主要推動者之一Don Box曾經說,HTTP的GET方法是分布式系統中最優化的方法。由此可知,Web之所以具有這么好的性能和可伸縮性,恰恰是得益於這種常見的HTTP GET方法。
有些HTTP方法是冪等(意味着對同一URL的多個請求應該返回同樣的結果)的,即我們可以安全地對失敗的請求進行重試。這些方法包括GET、PUT和DELETE等。
最后,通過使用超媒體,REST服務器的客戶端可以沿着某種路徑發現應用程序可能的狀態變化。這就是Field在他的博士論文中所提到的HATEOAS(Hypermedia as the Engine of Application State)。簡單來講,就是單個資源並不獨立存在。不同資源是相互鏈接在一起的。這並不意外,畢竟,這就是Web被稱為Web的原因。對於服務器來說,這意味着在返回中包含對其他資源的鏈接,由些客戶便可以通過這些鏈接訪問到相應的資源。
RESTful HTTP客戶端的關鍵方面
RESTful HTTP客戶端可能通過兩種方式在不同資源之間進行轉移,一種是上面所提到的超媒體,一種是服務器端的重定向。服務器端和客戶端將協同工作以動態地影響客戶端的分布式行為。由於URI包含了對地址進行解引用的所有信息——包括主機名和端口——客戶端可以根據超媒體鏈接訪問到不同的應用程序,不同的主機,甚至不同公司的資源。
在理想情況下,REST客戶端將從單個眾所周知的URI開始訪問,然后通過超媒體鏈接繼續訪問不同的資源。這和Web瀏覽器顯示HTML頁面是一樣的,HTML中包含了各種鏈接和表單,瀏覽器根據用戶輸入與不同的Web應用程序交互,此時它並不需要知道Web應用程序的接口或實現。
然而,瀏覽器並不能算是一個自給自足的客戶端,它需要由人來做出實際決定。但是一個程序客戶端卻可以模擬人來做出決定的,其中甚至包含了一些硬編碼邏輯。它可以跟隨不同的鏈接訪問不同的資源,同時它將根據不同的媒體類型發出不同的請求。
REST和DDD
RESTful HTTP是具有誘惑力的,但是我們並不建議將領域模型直接暴露給外界,因為這樣會使系統接口變得非常脆弱,原因在於對領域模型的每次改變都會導致對系統接口的改變。要將DDD與RESTful HTTP合並起來使用,我們有兩種方式:
第一種方法是為系統接口層單獨創建一個限界上下文,再在此上下文中通過適當的策略來訪問實際的核心模型。這是一種經典的方法,它將系統接口看作一個整體,通過資源抽象將系統功能暴露給外界,而不是通過服務或者遠程接口。
讓我們看一個實際的例子。我們創建一個系統來管理工作組,其中包括任務、計划/預約和子工作組管理等。我們將創建一個純凈的、不受架構影響的領域模型,該模型能正確地反映通用語言,並准確地實現業務邏輯。如果要為這個領域模型發布一個接口,我們便可以通過REST資源的形式向外提供一個遠程接口。這些資源反映了客戶所需的用例,它們和領域模型是存在區別的。但是,每一種資源歸根結底都創建自核心域,比如核心域中的聚合等。
當然,我們也可以簡單地使用領域對象來作為WebAPI的方法參數,比如我們可以將/user/task路由到getTask()方法,該方法返回一個Task對象。這樣看起來是簡單的,但卻隱藏着一個很大的問題。對Task對象的任何修改(指修改對Task類的定義)都將立即反映到遠程接口上,結果有可能使客戶端調用失敗。而即便我們所做的修改與外界沒有任何關系,我們依然不能排除客戶端調用失敗的可能性。
因此,上一種方式是應該被優先考慮的,因為它在核心域和系統接口模型之間完成了解耦,這使得我們可以先對領域模型進行修改,然后再決定哪些修改應該反映到系統接口模型上。請注意,在這種方法中,系統接口模型通常是根據領域模型來設計的,但是更好、更自然的方法應該是根據用例來設計。另外,我們還可以為這種方式自定義一種媒體類型。
另一種方法用於需要使用標准媒體類型的時候。如果某種媒體類型並不專用於支持單個(某個)系統接口,而是用於一組相似的客戶端-服務器交互場景,此時我們可以創建一個領域模型來處理每一種媒體類型。這樣的領域模型甚至可以在服務器和客戶端之間進行重用,雖然有些REST和SOA的擁護者認為這是一種反模式。請注意:這種方法本質上即為DDD中的共享內核或者發布語言。
這也是一種由外向里的、橫切式的方法。在上面提到的工作組例子中,有多種常用的格式可以用於領域模型。比如ical格式(iCal 又稱 iCalendar,是一種標准的互聯網日歷格式,讓用戶能夠在各種計算機和各種程序之間創建和共享電子日歷),這是一種通用格式。在本例中,我們首先選擇一種媒體類型,即ical,然后再根據這種格式創建領域模型。該模型可以用於任何能夠理解ical格式的系統,比如服務器程序,或者Android客戶端。在采用這種方法時,服務器需要處理多種類型的媒體類型,而同一種媒體類型又可以用於多個服務器。
如何在以上兩種方法之間進行選取呢?這在很大程度上取決於系統設計者對可重用性上的需求。第一種方法比較適合更加專屬的系統,而第二種方法更適合那些通用的系統。
為什么是REST?
從經驗看來,符合REST原則的系統將具有更好的松耦合性。通常來講,添加新資源並在已有資源中創建到新資源的鏈接是非常簡單的。要添加新的格式同樣如此。另外,基於REST的系統也是非常容易理解的,因為此時系統被分為很多較小的資源塊,每一個資源塊都可以獨立地測試和調試,並且每一個資源塊都表示了一個可重用的入口點。HTTP設計本身以及URI成熟的重寫與緩存機制使得RESTful HTTP成為一種不錯的架構選擇,該架構具有很好的松耦合性和可伸縮性。
