這陣子正打算用Rails做個(gè)東東,所以開(kāi)始系統地學(xué)習起了Rails。巧合的是,大概兩周前,dlee邀請我加入Fielding博士關(guān)于REST的那篇論文的翻譯團隊??梢哉f(shuō)Rails和REST這兩個(gè)最熱門(mén)的詞匯幾乎同時(shí)擠入了我的生活。隨著(zhù)我對Rails的學(xué)習和對[Fielding]的翻譯,我也開(kāi)始對REST產(chǎn)生了一些不太成熟的想法,寫(xiě)在這里與大家分享,同時(shí)也起到拋磚引玉的作用,歡迎大家討論。
先復習一下REST的基本思想。[Fielding]把REST形式化地定義為一種架構風(fēng)格(architecture style),它有架構元素(element)和架構約束(constraint)組成。這些概念比較晦澀難懂,而且我們做工程的往往并不需要形而上的理解。我們只知道,REST是一種針對網(wǎng)絡(luò )應用的設計和開(kāi)發(fā)方式,可以降低開(kāi)發(fā)的復雜性,提高系統的可伸縮性。REST提出了一些設計概念和準則:
- 網(wǎng)絡(luò )上的所有事物都被抽象為資源(resource);
- 每個(gè)資源對應一個(gè)唯一的資源標識(resource identifier);
- 通過(guò)通用的連接器接口(generic connector interface)對資源進(jìn)行操作;
- 對資源的各種操作不會(huì )改變資源標識;
- 所有的操作都是無(wú)狀態(tài)的(stateless)。
對于當今最常見(jiàn)的網(wǎng)絡(luò )應用來(lái)說(shuō),resource identifier是url,generic connector interface是HTTP,第4條準則就是我們常說(shuō)的url不變性。這些概念中的resouce最容易使人產(chǎn)生誤解。resouce所指的并不是數據,而是數據+特定的表現形式(representation),這也是為什么REST的全名是Representational State Transfer的原因。舉個(gè)例子來(lái)說(shuō),“本月賣(mài)得最好的10本書(shū)”和“你最喜歡的10本書(shū)”在數據上可能有重疊(有一本書(shū)即賣(mài)得好,你又喜歡),甚至完全相同。但是它們的representation不同,因此是不同的resource。
REST之所以能夠簡(jiǎn)化開(kāi)發(fā),是因為其引入的架構約束,比如Rails 1.2中對REST的實(shí)現默認把controller中的方法限制在7個(gè):index、show、new、edit、create、update和destory,這實(shí)際上就是對CURD的實(shí)現。更進(jìn)一步講,Rails(也是當今大部分網(wǎng)絡(luò )應用)使用HTTP作為generic connector interface,HTTP則把對一個(gè)url的操作限制在了4個(gè)之內:GET、POST、PUT和DELETE。
REST之所以能夠提高系統的可伸縮性,是因為它強制所有操作都是stateless的,這樣就沒(méi)有context的約束,如果要做分布式、做集群,就不需要考慮context的問(wèn)題了。同時(shí),它令系統可以有效地使用pool。REST對性能的另一個(gè)提升來(lái)自其對client和server任務(wù)的分配:server只負責提供resource以及操作resource的服務(wù),而client要根據resource中的data和representation自己做render。這就減少了服務(wù)器的開(kāi)銷(xiāo)。
既然REST有這樣的好處,那我們應該義無(wú)反顧地擁抱它??!目前一些大牛(像DHH)都已經(jīng)開(kāi)始投入到了REST的世界,那我們這些人應該做什么或者說(shuō)思考寫(xiě)什么你呢?我覺(jué)得我們應該思考兩個(gè)問(wèn)題:
- 如何使用REST;
- REST和MVC的關(guān)系。
第一個(gè)問(wèn)題假設REST是我們應該采用的架構,然后討論如何使用;第二個(gè)問(wèn)題則要說(shuō)明REST和當前最普遍應用的MVC是什么關(guān)系,互補還是取代?
我們先來(lái)談?wù)劦谝粋€(gè)問(wèn)題,如何使用REST。我感覺(jué),REST除了給我們帶來(lái)了一個(gè)嶄新的架構以外,還有一個(gè)重要的貢獻是在開(kāi)發(fā)系統過(guò)程中的一種新的思維方式:通過(guò)url來(lái)設計系統的結構。根據REST,每個(gè)url都代表一個(gè)resource,而整個(gè)系統就是由這些resource組成的。因此,如果url是設計良好的,那么系統的結構就也應該是設計良好的。對于非高手級的開(kāi)發(fā)人員來(lái)說(shuō),考慮一個(gè)系統如何架構總是一個(gè)很抽象的問(wèn)題。敏捷開(kāi)發(fā)所提倡的Test Driven Development,其好處之一(我覺(jué)得是最大的好處)就是可以通過(guò)testcase直觀(guān)地設計系統的接口。比如在還沒(méi)有創(chuàng )建一個(gè)class的時(shí)候就編寫(xiě)一個(gè)testcase,雖然設置不能通過(guò)編譯,但是testcase中的方法調用可以很好地從class使用者的角度反映出需要的接口,從而為class的設計提供了直觀(guān)的表現。這與在REST架構中通過(guò)url設計系統結構非常類(lèi)似。雖然我們連一個(gè)功能都沒(méi)有實(shí)現,但是我們可以先設計出我們認為合理的url,這些url甚至不能連接到任何page或action,但是它們直觀(guān)地告訴我們:系統對用戶(hù)的訪(fǎng)問(wèn)接口就應該是這樣。根據這些url,我們可以很方便地設計系統的結構。
讓我在這里重申一遍:
REST允許我們通過(guò)url設計系統,就像Test Driven Development允許我們使用testcase設計class接口一樣。
OK,既然url有這樣的好處,那我們就著(zhù)重討論一下如何設計url。網(wǎng)絡(luò )應用通常都是有hierarchy的,像棵大樹(shù)。我們通常希望url也能反映出資源的層次性。比如對于一個(gè)blog應用:/articles表示所有的文章,/articles/1表示id為1的文章,這都比較直觀(guān)。遺憾的是,網(wǎng)絡(luò )應用的資源結構永遠不會(huì )如此簡(jiǎn)單。因此人們常常會(huì )問(wèn)這樣一個(gè)問(wèn)題:RESTful的url能覆蓋所有的用戶(hù)請求嗎?比如,login如何RESTful?search如何RESTful?
從REST的概念上來(lái)看,所有可以被抽象為資源的東東都可以使用RESTful的url。因此對于上面的兩個(gè)問(wèn)題,如果login和search可以被抽象為資源,那么就可以使用RESTful的url。search比較簡(jiǎn)單,因為它會(huì )返回搜索結果,因此可以被抽象為資源,并且只實(shí)現index方法就可以了(只需要顯示搜索結果,沒(méi)有create、destory之類(lèi)的東西)。然而這里面也有一個(gè)問(wèn)題:search的關(guān)鍵字如何傳給server?index方法顯然應該使用HTTP GET,這會(huì )把關(guān)鍵字加到url后面,當然不符合REST的風(fēng)格。要解決這個(gè)問(wèn)題,可以把每次search看作一個(gè)資源,因此要創(chuàng )建create和index方法,create用來(lái)在用戶(hù)點(diǎn)擊“搜索”按鈕是通過(guò)HTTP POST把關(guān)鍵字傳給server,然后index則用來(lái)顯示搜索結果。這樣一來(lái),我們還可以記錄用戶(hù)的搜索歷史。使用同樣的方法,我們也可以對login應用REST,即每次login動(dòng)作是一個(gè)資源。
現在,我們來(lái)復雜一些的東東。如何用url表達“category為ruby的article”?一開(kāi)始可能想到的是/category/ruby/articles,這種想法很直觀(guān)。但是我覺(jué)得里面的category是不需要的,我們可以直接把“/ruby”理解為“category是ruby”,也就是說(shuō)“ruby”出現的位置說(shuō)明了它指的就是category。OK,/ruby/articles,單單從這個(gè)url上看,我們能獲得多少關(guān)于category的信息呢?顯然category隱藏在了url后面,這樣做到底好不好,應該是仁者見(jiàn)仁,智者見(jiàn)智了。對于如何表達category這樣的東西,我還沒(méi)想出很好的方式,大家有什么好idea,可以一起討論。
另外還有一種url形式,它對應到程序中的繼承關(guān)系。比如product是一個(gè)父類(lèi),book和computer是其子類(lèi)。那么所有產(chǎn)品的url應該是/products,所有書(shū)籍的url應該是/books,所有電腦的url應該是/computers。這一想法就比較直觀(guān)了,而且再次驗證了url可以幫助我們進(jìn)行設計的論點(diǎn)。
讓我再說(shuō)明一下我的想法:
如果每個(gè)用戶(hù)需求都可以抽象為資源,那么就可以完全使用REST。由此看來(lái),使用REST的關(guān)鍵是如何抽象資源,抽象得越精確,對REST的應用就越好。因此,如何改變我們目前根深蒂固的基于action的思想是最重要的。
有了對第一個(gè)問(wèn)題的討論,第二個(gè)問(wèn)題就容易討論多了。REST會(huì )取代MVC嗎?還是彼此是互補關(guān)系(就像AOP對于OOP)?答案是It depends!如果我們可以把所有的用戶(hù)需求都可以抽象為資源,那么MVC就可以推出歷史的舞臺了。如果情況相反,那么我們就需要混合使用REST和MVC。
當然,這是非常理想的論斷??赡芪覀儫o(wú)法找到一種方法可以把所有的用戶(hù)需求都抽象為資源,因為保證這種抽象的完整性(即真的是所有需求都可以)需要形式化的證明。而且即使被證明出來(lái)了,由于開(kāi)發(fā)人員的能力和喜好不同,MVC肯定也會(huì )成為不少人的首選。但是對于希望擁抱REST的人來(lái)說(shuō),這些都沒(méi)有關(guān)系。只要你開(kāi)發(fā)的系統所設計的問(wèn)題域可以被合理地抽象為資源,那么REST就會(huì )成為你的開(kāi)發(fā)利器。
所以,所有希望擁抱REST的朋友們,趕快訓練自己如何帶上資源的眼鏡看世界吧,這才是REST的核心所在。