ServiceStack是一個開源的、支持.NET與Mono平臺的REST Web Services框架。InfoQ有幸與Demis Bellot深刻地討論了這個項目。在這篇兩部分報道的第1部分中,咱們主要談論了ServiceStack項目創建的原動力,以及項目中的各類設計方案選擇。html
InfoQ: 你是否定爲微軟對服務的實現方式有什麼問題?ServiceStack又是怎樣解決這些問題的呢?git
Demis: 有一些問題是因爲微軟一直以來自認爲良好的框架設計的方式所形成的常見問題,另外一些問題的根源是因爲微軟的設計老是傾向於知足設計器優先的工具,而且試圖爲使用設計器做爲嚮導的開發者提供一套熟悉的API,而這種設計傾向形成了一些反作用:github
因爲微軟過於追求爲開發者提供一套熟悉的RPC API,並在VS.NET中提供豐富的UI工具(例如「添加服務引用」對話框)以保證上手的簡易性,其代價就是推廣了遠程服務中的各類反模式,從長期來看將致使沒必要要的不協調與脆弱性。不幸的是,微軟長期以來在其發佈的每一個web service框架中都持續使用這種設計方式,這種方式提倡建立RPC服務API的設計方式,形成了開發者在乎識中將遠程服務也看成本地方法同樣調用。這種結果的害處是多方面的,遠程服務意味着它包裝了一個外部依賴,對它的調用要比本地方法慢上幾百萬倍,而且更容易受外界的影響而發生錯誤。將隱式的服務契約與服務端RPC方法簽名綁定在一塊兒,就意味着你的API服務契約和服務端的實現緊密耦合在一塊兒了。因爲沒有規定一個定義良好的邊界,致使了糟糕的關注分離,而且出現了將繁重的ORM數據模型經過網絡返回給客戶端的糟糕實踐。從定義上來講,這種數據模型的關係型結構與循環式關係對於數據遷移對象(DTO)來講並不是一種好的選擇,它使調用可能偶爾會失敗。這種方式還將隱式的服務契約與你的底層關係型數據庫(RDBMS)數據模型耦合在一塊兒,這樣在進行修改時就會產生額外的衝突。並且遠程API與調用它們的服務端網站是無鏈接的,而這些調用的方法在客戶端代理部署後也會不斷地演化。理想的框架應該可以提倡可演化的、靈活的API設計,以免在服務端改變時產生運行時錯誤。redis
這些有一張關於WCF提倡的細粒度的RPC API方式與ServiceStack所鼓勵的基於消息的API方式的不一樣之處的對比圖。此外,這個用ServiceStack重寫Web API的入門教程的示例則展現了基於消息的API如何減小細粒度調用,增長了服務的可重用性。數據庫
ServiceStack採用了Martin Fowler所推薦的遠程服務最佳實踐,所以避免了以前困擾着.NET web service開發者的不少潛在問題:apache
遠程門面提倡使用基於消息的、粗粒度的批量調用接口。它可以將通訊次數減至最低,並促進了建立更少可是可用性更好、而且支持版本化的服務接口。ServiceStack採起了基於消息的設計確保開發者始終建立粗粒度的API設計。編程
數據遷移對象要求使用特定用途的POCO以建立web service響應的數據格式。ServiceStack一直鼓勵使用DTO,它自己與它的實現徹底解耦,而且存在於一個沒有外部依賴也不須要具體實現的程序集中。這種策略容許將定義服務端的服務類型共享給全部.NET客戶端,於是提供了一個端到端的類型化API,而且無需使用代碼生成。json
網關的做用是將全部服務端通訊包裝起來,隱藏在一個顯式定義而且可重用的客戶端網關以後。這種最佳實踐和代碼生成的代理(例如WCF)有所不一樣,後者將代碼生成的類型與底層的服務客戶端揉合在一塊兒,使得生成結果難以模擬(mock)、測試以及替換爲不一樣的實現,也常常致使對現有服務的改變會形成客戶端代碼的編譯錯誤。這一點對ServiceStack來講不多會成爲問題,由於它可以重用通用的服務客戶端,所以在接口方面惟一須要改變的東西僅限於類型自己而已。而且因爲基於消息的設計的優點,不管新增或移除任何功能都不會影響到現有的客戶端。設計模式
ServiceStack在它的通用而且可重用的類型化.NET服務客戶端採用了網關模式。咱們同時支持Silverlight、JavaScript、Dart甚至是MQ(消息隊列)的客戶端。讓全部.NET服務客戶端都共享相同的接口,這使測試(經過注入或者寫日誌方式)變得簡單,並且可以方便地在JSON、XML、JSV、MessagePack以及ProtoBuf等現有的客戶端之間進行切換,而無需改動應用程序的代碼。這就容許你在開發時使用相似於JSON這樣便於調試的文本格式,隨後在部署構建時切換爲使用.NET上速度最快的二進制格式,例如Message Pack或ProtoBuf,以下降數據傳輸量並實現最高的性能。
以當前的狀況來看,SOAP這種技術是不該該繼續存在的,或者說應該僅僅限制在那些它還可以帶來一些好處的地方使用。它的存在自己就是基於一個錯誤的假設,即爲了實現數據的互交換性,必須表現出複雜性,要儘量的嚴格與明確,這使得基於它的全部現實都必須實現一個複雜的schema以進行通訊。而事實上,之與相對設計方式反而被證實是正確的,若是使用了相似於JSON、CVS、Protocol Buffers、MessagePack、及BSON等最小化而且靈活的格式,就會大大減小實現的障礙與複雜性,結果的生成會更快,互操做性與版本能力也會更強。
我老是以爲這件事很難想象,儘管SOAP是創建在HTTP基礎之上的,但它的設計者們在開發出WSDL時彷佛徹底沒有吸收那些寶貴的經驗。WSDL所引入的類型、消息、操做、端口、綁定以及服務這些概念真的可以取代HTTP中簡單的URL標識符,以及Accept/Content-Type這些頭信息嗎?我以爲這一點也不難判斷啊。
雖然它名爲SOAP(簡單對象訪問協議),但它既不簡單,也不是什麼對象訪問協議,並且對於開發服務來講,它在許多方面都顯示出它是個糟糕的選擇,例如:
因爲SOAP明顯背離了服務的核心優點,因此它可以流行起來實在是一個使人吃驚的事實。我曾有許多年爲政府項目與大型企業開發服務的經驗,很不幸的是其中的許多項目都有一個必需實現的要求,即把他們的服務暴露爲SOAP終結點。不過許多互聯網公司與專一於創造價值的創業公司都不太會使用這門技術了,他們更多地選擇建立簡單的HTTP API,返回單純的XML或者JSON格式的響應。
雖然在多數狀況下SOAP是一個糟糕的選擇,但ServiceStack仍是爲你建立的服務提供了對SOAP終結點的支持,由於現在仍然有許多現存的企業系統只可以與SOAP終結點進行通訊。這一點也和ServiceStack的核心目標相一致,即爲你的服務提供最大化的功能性、可訪問性以及鏈接方式。
SOAP在設計上是一種很是脆弱的格式,若是某個服務稍稍偏離了WSDL,而客戶端又是由這個WSDL生成的,那你一般就會看到運行時錯誤的發生。雖然在靜態類型系統中,快速失敗被視爲一種優點,但對於遠程服務來講就不是一種理想的方式了,由於你一般但願可以實現向前兼容與向後兼容,所以服務端的API改變不會破壞現有的客戶端。這一目標顯然被WCF忽略了,它提倡RPC方法簽名、SOAP格式以及代碼生成,這能夠說是當今的全部web service實現技術中最爲脆弱的組合了。
將SOAP與Google的Protocol Buffers進行一下對比是頗有趣的,它是Google推出的一種簡單的接口描述語言(IDL),Google在其內部的全部RPC協議與文件格式中幾乎都使用了該技術:
MessagePack RPC以及用於開發Facebook的Apache Thrift是另外兩個流行的開源選擇,它們都提供了快速的,簡單的IDL,而且爲多數主流平臺提供了綁定。
雖然是由不一樣的公司開發,但以上每一個IDL都比SOAP更簡單、快速而且易於使用。從SOAP的規格說明書的厚度來看,它應該是由委員會所開發的,但顯然徹底沒有考慮過性能,大小以及易於實現。
雖然在大小與性能方面有優點,但Protocol Buffers與MessagePack都有一個問題,即它們使用了二進制格式。因爲基於文本的格式與協議更易於建立、維護及調試,它們常常成爲整個開放web的更流行的選擇。近年來,在基於文本的數據互交換格式上的贏家顯而易見是JSON,這是一個簡單的、緊湊的自描述文本格式,它的主要優點是可以在全部支持JavaScript的瀏覽器上進行直接應用,只需使用瀏覽器內置的「eval()」語句就可以當即將其轉換爲JavaScript對象了。因爲它的流行性,主流瀏覽器現在都加入了對JSON的原生支持,它們爲eval()方法提供了一個更安全的替代方案,能夠在老瀏覽器上提供一個基於JavaScript的備選實現方式。若是想更多的瞭解JSON以及它的好處,我推薦你去看看Douglas Crockford這個有趣的演講——「Heresy & Heretical Open Source」。
因爲它的廣泛性、功能多樣而且易於使用,JSON在多數平臺上都獲得了原生的支持,而且很快地成爲了開發Web API的首選數據格式,尤爲是使用在Ajax客戶端的時候。
從我自身的經驗來看,我認爲JSON對於實現服務是一種優秀的格式。它是一種更精簡、靈活、容忍度高而且適應性更強的格式,而且它易於建立、調用以及調試,比起SOAP減小了衝突並提升了生產力。當咱們開始ServiceStack項目時,JSON的惟一問題就是.NET Framework自帶的序列化器比XML序列化器還要慢。出於對性能上的嚴謹態度,咱們並不肯意接受這點不足,所以咱們發佈了咱們本身實現的序列化器,它比任何其它.NET JSON序列化器都要快上3倍以上,而且比.NET Framework中的任何序列化器(包括二進制序列化器)都要快上2.5倍以上。
繼承了JSON保持體積最小的精神,咱們也開發了JSV格式,它相似於JSON,但使用了CVS風格的轉義功能,使得它比JSON略微加快,更精簡而且更便於人類閱讀,它不只可以用於.NET與.NET服務的交互,並且可以用以解析查詢字符串,在ServiceStack的GET請求中是可以容許傳遞複雜的對象圖的。
雖然WCF與ServiceStack在實現方式上是徹底不一樣的服務框架,但它們卻有着很是相似的目標。ServiceStack在建立服務上採起了一種很是面向服務的方式,它的設計通過了作好,可以以最大程度重用的方式達到服務的實現。經過代碼優先、類型化設計的方式,咱們可以爲你的服務提供更強大的智能推斷,以容許你自動地生成XSD、WSDL文件,自動生成元數據頁,而且自動暴露預約義的路徑。每一個新加入的功能、特性、終結點與Content-Type都是圍繞着你的現有模型建立的,而且不須要任何額外的操做以及對應用程序代碼的任何改動就能夠當即得到新功能。咱們將這種方式理解爲從一個代碼優先的模型開始,做爲最權威的部分,隨後再將其功能暴露出來。
WCF的目標也是提供一個服務框架,以支持在多種終結點上運行服務,但他們站在一個不一樣的視角,而且爲全部網絡終結點提供了一個統一的抽象,你必須對服務進行配置以綁定到終結點。做爲WCF的主要目標之一,他們是少數幾個在最終實現中對WS-*系列技術提供了深刻支持的框架。
咱們最終的目標都是提供一個易於使用的服務框架,咱們只是在如何取得簡便性上有着徹底不一樣的想法。
WCF看起來比較喜歡繁重的抽象,用複雜的運行時XML配置進行管理,以及用大型工具爲開發者提供端到端的鏈接能力。這個抽象層包含了許多複雜的技術:通過抽象的人工服務端對象模型很複雜、配置很複雜、WSDL很複雜、SOAP/WS-*也很複雜。最終結果是,爲了對這些主題有一個良好的理解,對每一個主題都須要看好幾本書才行。
有一種陳舊的觀念認爲處理複雜性的解決之道就是添加更高層的抽象。這種方式的問題在於,只有這種抽象的設計很完美,它纔可以顯出效果。(例如編程語言比機器代碼更合適)不然當你遇到了一些預料以外的行爲,或者是配置、整合與互操做性上的問題是,你須要去理解在這些抽象層之下到底發生了什麼。在這種狀況中添加更多的抽象實際上只會增長開發者必須去熟悉的概念,以及當他們針對高級別抽象層進行開發時額外的認知。這也使得理解你的代碼庫更加困難,由於你必須理解每個之下都發生了些什麼。WCF的服務端對象模型是全新的、而且不夠天然,這進一步放大了問題,新的開發者們對此一無所知,由於這種模型是獨一無二的,而且它徹底沒有對服務或HTTP領域中的概念知識做出任何指導。所以當開發者們以後轉而使用最好的服務框架時,他們從WCF中學到的東西徹底沒有用武之地。與其投入時間去閱讀那些如何使用WCF的書籍,還不如把時間花在學習HTTP與TCP/IP上,由於即便你轉而使用其它web service框架,這些知識依然能起到做用,它們是徹底獨立於編程語言或平臺的。
從技術實現的層面來講,過多的抽象會致使沒必要要的性能負擔,而且這種性能問題很難優化。由於當你嘗試直接與底層API打交道時,這些抽象會產生阻礙以及各類強制性的阻力。而咱們的方式是隻在絕對必要時纔會添加抽象,例如咱們的IHttpRequest與IHttpResponse封裝對於爲ASP.NET與自託管的HttpListener提供一個通用的API就是必需的。相比起添加抽象層的方式,咱們更傾向於添加一些非強制性的特性,它們只是將一些底層接口與一些常見功能包裝起來,以實現DRY。這種方式帶來的好處就是容許終端用戶自由地調用各類方式,並在他們本身開發的類庫中加入本身定義的功能加強,以促進他們實現精益的、DRY以及可讀性良好的代碼庫。暴露底層API可以確保咱們保持框架的靈活性,用戶能夠經過多種方式進行訪問而得已完整地控制系統的輸出,也由於用戶能夠定製返回的響應,所以他們不會受到任何限制,而得已控制返回的每個字節。
WCF爲服務採起了一種統一的方式,它促進所支持的全部終結點都標準化爲一個單一的人爲抽象模型。這種方式也遇到了抽象的通用原則所產生的反作用,即一個統一的抽象模型要爲每一種試圖抽象化的實現承擔全部的複雜性,於是它們只可以處理各類終結點的特性的交集部分,並且爲每一個結終點實現最低程度的通用功能。結果就是生成了一個不完整的外部API,而且因爲抽象的存在阻礙了對底層終結點的訪問,使得這種API的可訪問性與可配置能力都大大削弱了。
要完全瞭解WCF的功能須要強大的技術能力,須要你投入巨大的精力研究各類技術資源。但本質上來看這種精力投入是一種浪費行爲,由於WCF與其它平臺上的一些採用了標準方式開發的框架相比,生產力與功能都有所欠缺。對於終端用戶來講,哪怕是爲了稍稍掌握一些WCF的使用經驗,也須要投入大得多的精力。出於以上緣由,我不但願看到有人去使用相似於WCF之類的框架,或者是它那種統一的終結點的抽象方式。因爲它的複雜性與龐大的技術實現細節,我也很懷疑它是否可以保持演化,以適應新的開發範式或是服務開發模式。我懷疑WCF會遭受到與WebForms相同的命運,退化爲一個過期的技術,最終被微軟下一代框架取而代之。
除了現有的REST、SOAP、MQ以及RCON以外,咱們也計劃爲ServiceStack加入更多的終結點。但因爲咱們採起了一種基於約定的方式,咱們會避免使用繁重的配置。咱們所採用的基於消息的設計方式會避免使用大型工具,並簡化必須實現的外部接口部分。首先從C#開始,逐漸移植其它語言,這使得終端用戶能夠當即使用新功能,而無需任何額外的投入。咱們的鬆耦合架構不存在統一的抽象模型,這能夠容許咱們加入互不衝突的新終結點與特性,而不會爲現有的組件帶來強制的複雜性。
總而言之,咱們提供了一個特性完整、性能更好而且更易於理解的服務框架,它的代碼庫也更加精益和靈活,而且使用它進行開發和維護所需的技術投入要小的多。
WCF所推崇的另外一個概念是XML配置,而這也是咱們沒法苟同的,它妨礙了測試,而且須要投入更多精力進行維護。只有你的應用程序中真正可配置的部分才應該放在你的應用配置文件中,定義及組織你的服務依賴應該交給代碼去作,這種方式的額外優勢在於它便於調試,並且能夠在編譯時靜態地進行驗證。WCF須要大量的配置,在ServiceStack中建立一個完整的應用程序所需的代碼量比起一個的WCF Service所需的XML WCF 配置還要小。若是把配置信息放在IOC(控制反轉)這一怪,那代碼會更乾淨與整潔,由於它直接就能夠引用你所需的特性的.NET API了。
大型工具的問題在於,當你打算在其中嘗試些新功能,或者是某些不在其設計初衷裏的功能時,它就無能爲力了。也就是說你成爲了這個工具的奴隸,只能提供一些它所支持的功能。並且框架一旦進行了重寫,大型工具就沒法繼續使用,由於它們所提倡的繁重的、複雜的代碼庫是很難演化或者是重構的。我推測這就是微軟爲何老是重寫新的服務框架,而不是改進現有的框架的緣由。也是爲何Web API不可以重用MVC現有的抽象方式,而且不支持SOAP的緣故,儘管它們都採用了相同的RPC API設計方式,而且它的自託管功能選項也是基於WCF建立的。
因爲ServiceStack是基於ASP.NET的最底層建立的,所以它提供了對ASP.NET MVC良好的集成能力,也可以方便地重用MVC中許多組件現有的功能實現,例如Authentication filter attribute、Caching與Session provider等等。你也能夠在MVC中方便地調用ServiceStack服務,比起C#方法直接調用只是稍稍麻煩一點。
ServiceStack與WCF相反,它看起來要簡單許多。例如ServiceStack的架構只要一頁紙就能畫下。啓用它只需在web.config中加入一行,告訴ASP.NET將全部請求轉發給ServiceStack便可。
ServiceStack與微軟開發類庫與框架所採用的方式,其觀念上的最大不一樣或許就是在如何最好地處理複雜性這一點上了。微軟傾向於明確性,XML方式的可配置能力,引入繁重的抽象層(爲了前瞻性而支持全部潛在的用例),依賴於抽象層將簡單的面向用戶的門面通過改頭換面後暴露出去,爲了方便使用設計器工具及新手開發者進行優化。微軟的主要動力是爲開發者提供一個熟悉的編程模型,而且經過使用大型工具來簡化模型的上手難度。從WebForms中就能夠看出這一點,它爲WinForm開發者提供了一個基於事件的編程模型來開發網站,WCF則讓開發者使用傳統的C#方法與接口方式去建立遠程服務。它們採起的方式一般會致使完成功能須要投入大量的技術精力去研究巨大的代碼庫。
與之相反,咱們將大型的代碼庫看成代碼的最大敵人,而且避免引入人爲的複雜性,這是咱們的主要目標之一。比方說咱們強烈反對引入新概念、抽象或編程模型。咱們崇尚小巧的低級別接口,它與底層領域1對1進行映射,以實現靈活性的最大化,減小衝突,而且在須要進一步定製化時將轉換的複雜性降至最低。經過可重用的工具類與擴展方法保證了DRY與高級別的功能。咱們採起一種代碼優先的開發模型,經過代碼去捕捉用戶意願的本質,這促進了一種更優雅、不妥協的設計方式,避免了使用設計器工具時所帶來的轉換的複雜性。咱們的全部類庫都使用POCO,以得到最大的可重用性,而且咱們內置的轉換器能夠簡化在領域特定模型中進行轉換所需的精力。
咱們爲用戶展示了一種基於消息設計的最佳實踐,爲從頭開始開發的服務提供了一種最優的方式。服務就是普通的C#類,無需關注任何終結點的問題,由於這種依賴是能夠自動解析的,這種方式會促使用戶採用良好的代碼實踐。
約定以及減小對外部知識的瞭解是咱們處理複雜性最好的武器。相比明確的指定實現方式,更好的辦法是提供一個基於約定的默認行爲,使它按預計的方式運行。這就讓用戶沒必要必定要學習你的框架API,由於咱們能夠假定預計的標準行爲的結果,並容許咱們隨時提供更多的能力,比方說,它可以自動支持外部所調用的全部內置終結點與格式。你也可讓你的服務返回任意的響應,它會自動序列化爲所請求的Content-Type。
咱們相信,達成簡單性的最好方式就是在第一時間避免各類複雜性,咱們採起了如下方式:
咱們並不喜歡代碼生成,由於咱們相信它爲項目帶來了沒必要要的麻煩,咱們也反對在開發工做的核心部分依賴於任何大型工具。
因爲你的ServiceStack服務的服務契約在設計上是由你的請求與響應DTO維護的,咱們就可以提供一個類型化的端到端的API,只須要你用來建立服務的那些類型以及那些通用的、可重用的.NET客戶端就能夠了。這容許咱們爲任何.NET上的服務框架提供一個最簡潔的、類型化的以及端到端的API。
基於WCF的設計方式以及它對配置的嚴重依賴,看起來WCF在設計時徹底沒有考慮到可測試性。而這一點是ServiceStack的核心目標之一,咱們默認提供了一個內置的(而且可重寫的)IOC容器,鼓勵在一開始就遵循良好的開發實踐。開發一個服務只需實現一個不包括任何方法的IService接口,或者從其它現有的Service類、或ServiceBase類繼承便可,這些類都是可獨立測試的,而且徹底能夠mock。這一系列努力的成果就是,你所實現的單元測試能夠用做XML、JSON、JSV以及SOAP的集成測試。咱們所提供的自託管HttpListener類型使得進行內存中的集成測試很是方便。
性能是ServiceStack的首要目標之一,咱們將性能視爲最重要的特性。基於消息的設計提倡更少的網絡調用,咱們也很當心地保證只暴露那些運行快速的面向用戶的API。通常狀況下咱們不接受來自外部的貢獻代碼,由於它們會包含速度較慢的代碼。咱們對本身的實現是盡心盡力的,咱們沒有使用任何運行時的反射或者正規表達式匹配,而是採用其它更快的解決方案。
咱們開發並維護着.NET上最快的JSON、JSV以及CSV文本序列化器,同時容許以插件的方式使用Message Pack與Protocol Buffers這兩個.NET上最快的二進制序列化器。
因爲緩存是建立高性能服務不可或缺的技術,咱們提供了一個豐富的Caching Provider模型,包含了對各類內存緩存、Redis、Memcached、Azure以及Amazon後臺的實現。緩存API會保證使用最優化的格式,比方說,若是客戶端支持,咱們會將壓縮後的JSON輸出緩存起來,並在以後發生調用時直接將緩存中的內容輸出到響應流中,這就保證了託管代碼能產生最快的響應速度。
每次咱們找出微軟的類庫中的瓶頸時,咱們都會使用更快速的實現去替代它。咱們已經實現了本身的JSON序列化器,做爲插件的Gzip和Deflate壓縮類庫,而且提供了本身的Session實現,它可以與以上任意一種Caching provider相配合使用,它的出現也避免了困擾ASP.NET開發者已久的嚴重性能問題。
出於咱們對開發高性能服務的技術追求,咱們爲最快的分佈式NoSQL數據庫Redis開發並維護着一個.NET上最好的C# Redis客戶端。
Demis Bellot是來自Stack Exchange的一位開發者,他維護着StackOverflow Careers 2.0的後臺網站以及基於ServiceStack建立的MQ服務。他同時也是ServiceStack的創始人以及項目領導。
查看英文原文:Interview With Demis Bellot, Project Lead of ServiceStack - Part 1