from:http://www.infoq.com/cn/articles/micro-soa-2
在上一篇文章中,我說到SOA是一個特別大的話題,不但沒有絕對統(tǒng)一的原則,而且很多原則本身的內(nèi)容也具備相當(dāng)模糊性和寬泛性。雖然我們可以說SOA ≈ 模塊化開發(fā) + 分布式計算,但由于其原則的模糊性,我們?nèi)匀缓茈y說什么應(yīng)用是絕對符合SOA的,只能識別出哪些是不符合SOA的。
本篇將對8種可操作的服務(wù)設(shè)計原則進(jìn)行細(xì)化的分析,作為SOA實踐的參考。
服務(wù)設(shè)計原則1:優(yōu)化遠(yuǎn)程調(diào)用
這里的遠(yuǎn)程調(diào)用特指RPC(Remote Procedure Call)。當(dāng)然更面向?qū)ο蟮恼f法應(yīng)該是遠(yuǎn)程方法調(diào)用或者遠(yuǎn)程服務(wù)調(diào)用等等。
由于SO接口通常要被遠(yuǎn)程訪問,而網(wǎng)絡(luò)傳輸,對象序列化/反序列化等開銷都遠(yuǎn)遠(yuǎn)超過本地Object訪問幾個數(shù)量級,所以要加快系統(tǒng)的響應(yīng)速度、減少帶寬占用和提高吞吐量,選擇高性能的遠(yuǎn)程調(diào)用方式經(jīng)常是很重要的。
但是遠(yuǎn)程調(diào)用方式往往又要受限于具體的業(yè)務(wù)和部署環(huán)境,比如內(nèi)網(wǎng)、外網(wǎng)、同構(gòu)平臺、異構(gòu)平臺等等。有時還要考慮它對諸如分布式事務(wù),消息級別簽名/加密,可靠異步傳輸?shù)确矫娴闹С殖潭龋ㄟ@些方面通常被稱為SLA:service level agreement),甚至還包括開發(fā)者的熟悉和接受程度等等。
因此,遠(yuǎn)程調(diào)用方式往往需要根據(jù)具體情況做出選擇和權(quán)衡。
以Java遠(yuǎn)程Service為例分析不同場景下,傳輸方式的某些可能較好選擇:
- 內(nèi)網(wǎng) + 同框架Java客戶端 + 大并發(fā):多路復(fù)用的TCP長連接 + kryo (二進(jìn)制序列化) (kryo也可以用Protostuff,F(xiàn)ST等代替)
- 內(nèi)網(wǎng) + 不同框架Java客戶端:TCP + Kryo
- 內(nèi)網(wǎng) + Java客戶端 + 2PC分布式事務(wù):RMI/IIOP (TCP + 二進(jìn)制)
- 內(nèi)網(wǎng) + Java客戶端 + 可靠異步調(diào)用:JMS + Kryo (TCP + 二進(jìn)制)
- 內(nèi)網(wǎng) + 不同語言客戶端:thrift(TCP + 二進(jìn)制序列化)
- 外網(wǎng) + 不同語言客戶端 + 企業(yè)級特性:HTTP + WSDL + SOAP (文本)
- 外網(wǎng) + 兼顧瀏覽器、手機(jī)等客戶端:HTTP + JSON (文本)
- 外網(wǎng) + 不同語言客戶端 + 高性能:HTTP + ProtocolBuffer (二進(jìn)制)
簡單來說,從性能上講,tcp協(xié)議 + 二進(jìn)制序列化更適合內(nèi)網(wǎng)應(yīng)用。從兼容性、簡單性上來說,http協(xié)議 + 文本序列化更適合外網(wǎng)應(yīng)用。當(dāng)然這并不是絕對的。另外,tcp協(xié)議在這里并不是限定遠(yuǎn)程調(diào)用協(xié)議一定只能是位于OSI網(wǎng)絡(luò)模型的第四層的原始tcp,它可以包含tcp之上的任何非http協(xié)議。
所以,回答上面提到的問題,WebServices (經(jīng)典的WSDL+SOAP+HTTP)雖然是最符合前述SOA設(shè)計原則的技術(shù),但并不等同于SOA,我認(rèn)為它只是滿足了SOA的底線,而未必是某個具體場景下的最佳選擇。這正如一個十項全能選手在每個單項上是很難和單項冠軍去競爭的。更理想的SOA Service最好能在可以支持WebServices的同時,支持多種遠(yuǎn)程調(diào)用方式,適應(yīng)不同場景,這也是Spring Remoting,SCA,Dubbo,F(xiàn)inagle等分布式服務(wù)框架的設(shè)計原則。
遠(yuǎn)程調(diào)用技術(shù)解釋:HTTP + JSON適合SOA嗎?
JSON簡單易讀,通用性極佳,甚至能很好支持瀏覽器客戶端,同時也常被手機(jī)APP使用,大有取代XML之勢。
但JSON本身缺乏像XML那樣被廣泛接受的標(biāo)準(zhǔn)schema,而一般的HTTP + JSON的遠(yuǎn)程調(diào)用方式也缺乏像Thrift,CORBA,WebServices等等那樣標(biāo)準(zhǔn)IDL(接口定義語言),導(dǎo)致服務(wù)端和客戶端之間不能形成強(qiáng)的服務(wù)契約,也就不能做比如自動代碼生成。所以HTTP + JSON在降低了學(xué)習(xí)門檻的同時,可能顯著的增加復(fù)雜應(yīng)用的開發(fā)工作量和出錯可能性。
例如,新浪微博提供了基于HTTP + JSON的Open API,但由于業(yè)務(wù)操作比較復(fù)雜,又在JSON上封裝實現(xiàn)了各種語言的客戶端類庫,來減少用戶的工作量。
為了解決這方面的問題,業(yè)界有很多不同方案來為HTTP + JSON補(bǔ)充添加IDL,如RSDL、JSON-WSP、WADL、WSDL 2.0等等,但事實上它們的接受度都不太理想。
另外值得一提的是,JSON格式和XML一樣有冗余,即使做GZIP壓縮之類的優(yōu)化,傳輸效率通常也不如很多二進(jìn)制格式,同時壓縮、解壓還會引入額外的性能開銷。
遠(yuǎn)程調(diào)用技術(shù)解釋:Apache Thrift多語言服務(wù)框架
Thrift是最初來自facebook的一套跨語言的service開發(fā)框架,支持C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, JavaScript, Node.js, Smalltalk, Delphi等幾乎所有主流編程語言,具有極好的通用性。
Thrift被facebook,twitter等巨頭以及開源社區(qū)都廣泛使用,是非常成熟的技術(shù)。
Thrift的服務(wù)契約通過類似如下形式的IDL定義:
struct User { 1: i32 id, 2: string name, 3: string password } service UserService { void store(1: User user), UserProfile retrieve(1: i32 id) }
非常類似于C語言,易讀易寫,比WSDL簡單明了得多。比用java之類的編程語言也更方便,有時候可以把所有相關(guān)的接口和數(shù)據(jù)結(jié)構(gòu)定義放到同一個文件,發(fā)布出去的時候不用再打一個壓縮包之類,甚至可以直接粘貼到文檔中
Thrift還提供工具,可以基于IDL自動生成各種語言對應(yīng)的服務(wù)端和客戶端代碼:
[lishen@dangdang thrift]thrift --gen java user.thrift [lishen@dangdang thrift]$ thrift --gen cpp user.thrift [lishen@dangdang thrift]$ thrift --gen php user.thrift [lishen@dangdang thrift]$ thrift --gen csharp user.thrift
我認(rèn)為thrift是比WebServices更簡單高效的技術(shù),是在SOA中對WebServices最具有替代性的技術(shù)之一。
遠(yuǎn)程調(diào)用技術(shù)解釋:多路復(fù)用的TCP長連接
這是一種追求極致高性能高伸縮的方式,這里只做簡要介紹。
比較典型的是twitter的Mux RPC協(xié)議以及google的SPDY協(xié)議,在其中多個請求同時共用同一個長連接,即一個連接交替?zhèn)鬏敳煌埱蟮淖止?jié)塊。它既避免了反復(fù)建立連接開銷,也避免了連接的等待閑置從而減少了系統(tǒng)連接總數(shù),同時還避免了TCP順序傳輸中的線頭阻塞(head-of-line blocking)問題。
另外,國內(nèi)比較著名的開源dubbo框架的默認(rèn)RPC協(xié)議,以及業(yè)界許多小型開源RPC框架也都是類似的思路。
采用多路復(fù)用機(jī)制后,一般就要求服務(wù)器端和客戶端都支持額外的類似于會話層(即OSI網(wǎng)絡(luò)模型第六層)的語義,導(dǎo)致它們必須要依賴于同一套RPC框架。
其他很多RPC機(jī)制都是使用TCP短連接。即使有些RPC使用了長連接,但一個連接同一時間只能發(fā)送一個請求,然后連接就處于閑置狀態(tài),來等待接收該請求的響應(yīng),待響應(yīng)完畢,該連接才能被釋放或者復(fù)用。
HTTP 1.1也支持一種基于pipeline模式的長連接,其中多個HTTP請求也可共用一個連接,但它要求響應(yīng)(response)也必須按照請求(request)的順序傳輸返回,即FIFO先進(jìn)先出。而在完全多路復(fù)用的連接中,哪個的響應(yīng)先ready就可以先傳輸哪個,不用排隊。
當(dāng)然,短連接、長連接和多路復(fù)用長連接之間不存在絕對的好壞,需要取決于具體業(yè)務(wù)和技術(shù)場景,在此不詳細(xì)展開了。
遠(yuǎn)程調(diào)用技術(shù)解釋:Java高效序列化
最近幾年,各種新的Java高效序列化方式層出不窮,不斷刷新序列化性能的上限,例如Kryo,F(xiàn)ST等開源框架。它們提供了非常高效的Java對象的序列化和反序列化實現(xiàn),相比JDK標(biāo)準(zhǔn)的序列化方式(即基于Serializable接口的標(biāo)準(zhǔn)序列化,暫不考慮用諸如Externalizable接口的定制序列化),在典型場景中,其序列化時間開銷可能縮短20倍以上,生成二進(jìn)制字節(jié)碼的大小可能縮減4倍以上。
另外,這些高效Java序列化方式的開銷也顯著少于跨語言的序列化方式如thrift的二進(jìn)制序列化,或者JSON等等
遠(yuǎn)程調(diào)用技術(shù)解釋:RMI/IIOP和分布式事務(wù)
RMI/IIOP是Java EE中標(biāo)準(zhǔn)的遠(yuǎn)程調(diào)用方式,IIOP是CORBA的協(xié)議,只有IIOP上的RMI才支持兩階段提交的分布式事務(wù),同時提供和CORBA的互操作。
當(dāng)然,嚴(yán)格的兩階段提交事務(wù)并不高效,還可能嚴(yán)重影響系統(tǒng)伸縮性甚至可用性等等,一般只應(yīng)用在非常關(guān)鍵的業(yè)務(wù)中。
遠(yuǎn)程調(diào)用技術(shù)解釋:Google ProtocolBuffer跨語言序列化
ProtocolBuffer是google開發(fā)的跨語言的高效二進(jìn)制序列化方式,其序列化性能和thrift比較類似。事實上thrift最初就是ProtocolBuffer的仿制品。但它和thrift最大的不同是他沒有自帶的RPC實現(xiàn)(因為google沒有將RPC部分開源,但有大量第三方實現(xiàn))。
由于不和RPC方式耦合,反而使得ProtocolBuffer能被方便的集成進(jìn)大量已有的系統(tǒng)和框架中。在國內(nèi)它也被百度、淘寶等廣泛的應(yīng)用在Open API中,和HTTP搭配作為一種高效的跨平臺跨組織的集成方式。
服務(wù)設(shè)計原則2:消除冗余數(shù)據(jù)
同樣由于service的遠(yuǎn)程調(diào)用開銷很高,所以在它的輸入?yún)?shù)和返回結(jié)果中,還要盡量避免攜帶當(dāng)前業(yè)務(wù)用例不需要的冗余的字段,來減少序列化和傳輸?shù)拈_銷。同時,去掉冗余字段也可以簡化接口,避免給外部用戶帶來不必要的業(yè)務(wù)困惑。
比如article service中有個返回article list的方法
List<Article> getArticles(...)
如果業(yè)務(wù)需求僅僅是要列出文章的標(biāo)題,那么在返回的article中就要避免攜帶它的contents等等字段。
這里經(jīng)典解決方案就是引入OO中常用的Data Transfer Object (DTO)模式,專門針對特定service的用例來定制要傳輸?shù)臄?shù)據(jù)字段。這里就是添加一個AriticleSummary的額外數(shù)據(jù)傳輸對象:
List<ArticleSummary> getArticleSummaries(...)
額外的DTO確實是個麻煩,而一般OO程序通常則可直接返回自己的包含冗余的業(yè)務(wù)模型。
服務(wù)設(shè)計原則3:粗粒度契約
同樣由于遠(yuǎn)程調(diào)用開銷高,同時service的外部使用者對特定業(yè)務(wù)流程的了解也比不上組織內(nèi)部的人,所以service的契約(接口)通常需要是粗粒度的,其中的一個操作就可能對應(yīng)到一個完整的業(yè)務(wù)用例或者業(yè)務(wù)流程,這樣既能減少遠(yuǎn)程調(diào)用次數(shù),同時又降低學(xué)習(xí)成本和耦合度。
而OO接口通常可以是非常細(xì)粒度的,提供最好的靈活性和重用性。
例如,article service支持批量刪除文章,OO接口中可以提供
deleteArticle(long id)
供用戶自己做循環(huán)調(diào)用(暫不考慮后端SQL之類優(yōu)化),但SO接口中,則最好提供
deleteArticles(Set<Long> ids)
供客戶端調(diào)用,將可能的N次遠(yuǎn)程調(diào)用減少為一次。
例如,下訂單的用例,要有一系列操作
addItem -> addTax -> calculateTotalPrice -> placeOrder
OO中我們完全可以讓用戶自己來靈活選擇,分別調(diào)用這些細(xì)粒度的可復(fù)用的方法。但在SO中,我們需要將他們封裝到一個粗粒度的方法供用戶做一次性遠(yuǎn)程調(diào)用,同時也隱藏了內(nèi)部業(yè)務(wù)的很多復(fù)雜性。另外,客戶端也從依賴4個方法變成了依賴1個方法,從而大大降低了程序耦合度。
順便值得一提的是,如果上面訂單用例中每個操作本身也是遠(yuǎn)程的service(通常在內(nèi)網(wǎng)之中),這種粗粒度封裝就變成了經(jīng)典的service composition(服務(wù)組合)甚至service orchestration(服務(wù)編排)了。這種情況下粗粒度service同樣可能提高了性能,因為對外網(wǎng)客戶來說,多次跨網(wǎng)的遠(yuǎn)程調(diào)用變成了一次跨網(wǎng)調(diào)用 + 多次內(nèi)網(wǎng)調(diào)用。
對這種粗粒度service封裝和組合,經(jīng)典解決方案就是引入OO中常用的Facade模式,將原來的對象屏蔽到專門的“外觀”接口之后。同時,這里也很可能要求我們引入新的service參數(shù)/返回值的數(shù)據(jù)結(jié)構(gòu)來組合原來多個操作的對象模型,這就同樣用到前述的DTO模式。
一個簡單Facade示例(FooService和BarService是兩個假想的本地OO service,façade將它們的結(jié)果值組合返回):
class FooBarFacadeImpl implements FooBarFacade { private FooService fooService; private BarService barService; public FooBarDto getFooBar() { FooBarDto fb = new FooBarDto(); fb.setFoo(fooService.getFoo()); fb.setBar(barService.getBar()); return fb; } }
當(dāng)然,有的時候也可以不用facade和DTO,而在是FooService和BarService之外添加另一個本地service和domain model,這要和具體業(yè)務(wù)場景有關(guān)。
服務(wù)設(shè)計原則4:通用契約
由于service不假設(shè)用戶的范圍,所以一般要支持不同語言和平臺的客戶端。但各種語言和平臺在功能豐富性上有很大差異,這就決定了服務(wù)契約必須取常見語言、平臺以及序列化方式的最大公約數(shù),才能保證service廣泛兼容性。由此,服務(wù)契約中不能有某些語言才具備的高級特性,參數(shù)和返回值也必須是被廣泛支持的較簡單的數(shù)據(jù)類型(比如不能有對象循環(huán)引用)。
如果原有的OO接口不能滿足以上要求,則在此我們同樣需要上述的Facade和DTO,將OO接口轉(zhuǎn)換為通用的SO契約。
例如原有對象模型
class Foo { private Pattern regex; }
Pattern是Java特有的預(yù)編譯好的,可序列化的正則表達(dá)式(可提高性能),但在沒有特定框架支持下,可能不好直接被其他語言識別,所以可添加DTO:
class FooDto { private String regex; }
服務(wù)設(shè)計原則5:隔離變化
雖然OO和SO都追求低耦合,但SO由于使用者范圍極廣,就要求了更高程度的低耦合性。
比如前述的article service,OO中可以直接返回article對象,而這個article對象在OO程序內(nèi)部可能做為核心的建模的domain model,甚至作為O/R mapping等等。而在SO如果還直接返回這個article,即使沒有前面所說的冗余字段,復(fù)雜類型等問題,也可能讓外部用戶與內(nèi)部系統(tǒng)的核心對象模型,甚至O/R mapping機(jī)制,數(shù)據(jù)表結(jié)構(gòu)等等產(chǎn)生了一定關(guān)聯(lián)度,這樣一來,內(nèi)部的重構(gòu)經(jīng)常都會可能影響到外部的用戶。
所以,這里再次對Facade和DTO產(chǎn)生了需求,用它們作為中介者和緩沖帶,隔離內(nèi)外系統(tǒng),把內(nèi)部系統(tǒng)變化對外部的沖擊減少到最小程度。
服務(wù)設(shè)計原則6:契約先行
Service是往往涉及不同組織之間的合作,而按照正常邏輯,兩個組織之間合作的首要任務(wù),就是先簽訂明確的契約,詳細(xì)規(guī)定雙方合作的內(nèi)容,合作的形式等等,這樣才能對雙方形成強(qiáng)有力的約束和保障,同時大家的工作也能夠并行不悖,不用相互等待。因此SOA中,最佳的實踐方式也是契約先行,即先做契約的設(shè)計,可以有商務(wù),管理和技術(shù)等不同方面的人員共同參與,并定義出相應(yīng)的WSDL或者IDL,然后在開發(fā)的時候再通過工具自動生成目標(biāo)語言的對應(yīng)代碼。
對于WSDL來說,做契約先行的門檻略高,如果沒有好的XML工具很難手工編制。但對于Thrift IDL或者ProtocolBuffer等來說,由于它們和普通編程語言類似,所以契約設(shè)計相對是比較容易的。另外,對于簡單的HTTP + JSON來說(假設(shè)不補(bǔ)充使用其他描述語言),由于JSON沒有標(biāo)準(zhǔn)的schema,所以是沒法設(shè)計具有強(qiáng)約束力的契約的,只能用另外的文檔做描述或者用JSON做輸入輸出的舉例。
但是,契約先行,然后再生成服務(wù)提供端的代碼,畢竟給service開發(fā)工作帶來了較大的不便,特別是修改契約的時候?qū)е麓a需要重寫。因此,這里同樣可能需要引入Facade和DTO,即用契約產(chǎn)生的都是Facade和DTO代碼,它們負(fù)責(zé)將請求適配和轉(zhuǎn)發(fā)到其他內(nèi)部程序,而內(nèi)部程序則可以保持自己的主導(dǎo)性和穩(wěn)定性。
另外,契約先行可能會給前面提到的多遠(yuǎn)程調(diào)用支持帶來一些麻煩。
當(dāng)然契約先行也許并不是能被廣泛接受的實踐方式,就像敏捷開發(fā)中“測試先行”(也就是測試驅(qū)動開發(fā))通常都是最佳實踐,但真正施行的團(tuán)隊卻非常之少,這方面還需要不斷摸索和總結(jié)。但我們至少可以認(rèn)為Echo中Java2WSDL并不被認(rèn)為是SOA的最佳實踐。
服務(wù)設(shè)計原則7:穩(wěn)定和兼容的契約
由于用戶范圍的廣泛性,所以SO的服務(wù)契約和Java標(biāo)準(zhǔn)API類似,在公開發(fā)布之后就要保證相當(dāng)?shù)姆€(wěn)定性,不能隨便被重構(gòu),即使升級也要考慮盡可能的向下兼容性。同時,如果用契約先行的方式,以后頻繁更改契約也導(dǎo)致開發(fā)人員要不斷重做契約到目標(biāo)語言映射,非常麻煩。
這就是說SO對契約的質(zhì)量要求可能大大高于一般的OO接口,理想的情況下,甚至可能需要專人(包括商務(wù)人員)來設(shè)計和評估SO契約(不管是否用契約先行的方式),而把內(nèi)部的程序?qū)崿F(xiàn)交給不同的人,而兩者用Facade和DTO做橋梁。
服務(wù)設(shè)計原則8:契約包裝
前述原則基本都是針對service提供端來講的,而對service消費端而言,通過契約生成對應(yīng)的客戶端代碼,經(jīng)常就可以直接使用了。當(dāng)然,如果契約本身就是Java接口之類(比如在Dubbo,Spring Remoting等框架中),可以略過代碼生成的步驟。
但是,service的返回值(DTO)和service接口(Facade),可能被消費端的程序到處引用到。
這樣消費端程序就較強(qiáng)的耦合在服務(wù)契約上了,如果服務(wù)契約不是消費端定義的,消費端就等于把自己程序的部分主導(dǎo)權(quán)完全讓渡給了別人。
一旦契約做更改,或者消費端要選擇完全不同的service提供方(有不同的契約),甚至改由本地程序自己來實現(xiàn)相關(guān)功能,修改工作量就可能非常大了。
另外,通過契約生成的客戶端代碼,經(jīng)常和特定傳輸方式是相關(guān)的(比如webservices stub),這樣給切換遠(yuǎn)程調(diào)用方式也會帶來障礙。
因此,就像在通常應(yīng)用中,我們要包裝數(shù)據(jù)訪問邏輯(OO中的DAO或者Repository模式),或者包裝基礎(chǔ)服務(wù)訪問邏輯(OO中的Gateway模式)一樣,在較理想的SOA設(shè)計中,我們也可以考慮包裝遠(yuǎn)程service訪問邏輯,由于沒有恰當(dāng)?shù)拿Q,暫時稱之為Delegate Service模式,它由消費端自己主導(dǎo)定義接口和參數(shù)類型,并將調(diào)用轉(zhuǎn)發(fā)給真正的service客戶端生成代碼,從而對它的使用者完全屏蔽了服務(wù)契約,這些使用者甚至不知道這個服務(wù)到底是遠(yuǎn)程提供的的還是本地提供的。
此外,即使我們在消費端是采用某些手工調(diào)用機(jī)制(如直接構(gòu)建和解析json等內(nèi)容,直接收發(fā)JMS消息等等),我們同樣可以用delegate service來包裝相應(yīng)的邏輯。
delegate service示例1:
// ArticlesService是消費端自定義的接口 class ArticleServiceDelegate implements ArticlesService { // 假設(shè)是某種自動生成的service客戶端stub類 private ArticleFacadeStub stub; public void deleteArticles(List<Long> ids) { stub.deleteArticles(ids); } }
delegate service示例2:
// ArticlesService是消費端自定義的接口 class ArticleServiceDelegate implements ArticlesService { public void deleteArticles(List<Long> ids) { // 用JMS和FastJson手工調(diào)用遠(yuǎn)程service messageClient.sendMessage(queue, JSON.toJSONString(ids)); } }
從面向?qū)ο蟮矫嫦蚍?wù),再從面向服務(wù)到面向?qū)ο?/h2>
總結(jié)上面的幾個原則,雖然只是談及有限的幾個方面,但大致也可看出OO和SO在實際的設(shè)計開發(fā)中還是有不少顯著的不同之處,而且我們沒有打算用SO的原則來取代過去的OO設(shè)計,而是引入額外的層次、對象和OO設(shè)計模式,來補(bǔ)充傳統(tǒng)的OO設(shè)計。
其實就是形成了這種調(diào)用流程:
Facade、DTO和Delegate Service負(fù)責(zé)做OO到SO和SO到OO的中間轉(zhuǎn)換。
現(xiàn)在,可以回答Echo示例中的問題:通過“透明的”配置方式,將OO程序發(fā)布為遠(yuǎn)程Service,雖然可能較好的完成了從本地對象到遠(yuǎn)程對象的跨越,但通常并不能較好的完成OO到SO的真正跨越。
同時,透明配置方式也通常無法直接幫助遺留應(yīng)用(如ERP等)轉(zhuǎn)向SOA。
當(dāng)然,在較為簡單和使用范圍確定很有限應(yīng)用(比如傳統(tǒng)和局部的RPC)中,透明式遠(yuǎn)程service發(fā)布會帶來極大的便利。
另外,上面對SO的所有討論都集中在RPC的方式,其實SO中也用message的方式做集成,它也是個大話題,暫時不在此詳論了。
為什么不能放棄面向?qū)ο螅?/h2>
SO是有它的特定場景的,比如遠(yuǎn)程的,范圍不定的客戶端。所以它的那些設(shè)計原則并不能被借用來指導(dǎo)一般性的程序開發(fā),比如很多OO程序和SO原則完全相反,經(jīng)常都要提供細(xì)粒度接口和復(fù)雜參數(shù)類型以追求使用的使用靈活性和功能的強(qiáng)大性。
就具體架構(gòu)而言,我認(rèn)為SOA層應(yīng)該是一個很薄的層次(thin layer),將OO應(yīng)用或者其他遺留性應(yīng)用加以包裝和適配以幫助它們面向服務(wù)。其實在通常的web開發(fā)中,我們也是用一個薄的展現(xiàn)層(或者叫Web UI層之類)來包裝OO應(yīng)用,以幫助它們面向瀏覽器用戶。因此,F(xiàn)açade、DTO等不會取代OO應(yīng)用中核心的Domain Model、Service等等 (這里的service是OO中service,未必是SO的)。
綜合起來,形成類似下面的體系結(jié)構(gòu):

理想和現(xiàn)實
需要特別指出的是,上面提到的諸多SO設(shè)計原則是在追求一種相對理想化的設(shè)計,以達(dá)到架構(gòu)的優(yōu)雅性,高效性,可重用性,可維護(hù)性,可擴(kuò)展性等等。
而在現(xiàn)實中任何理論和原則都可能是需要作出適當(dāng)妥協(xié)的,因為現(xiàn)實是千差萬別的,其情況遠(yuǎn)比理論復(fù)雜,很難存在放之四海而皆準(zhǔn)的真理。
而且很多方面似乎本來也沒有必要追求完美和極致,比如如果有足夠能力擴(kuò)充硬件基礎(chǔ)設(shè)施,就可以考慮傳輸一些冗余數(shù)據(jù),選擇最簡單傳輸方式,并多來幾次遠(yuǎn)程調(diào)用等等,以減輕設(shè)計開發(fā)的工作量。
那么理想化的原則就沒有意義了嗎?比如領(lǐng)域驅(qū)動設(shè)計(Domain-Driven Design)被廣泛認(rèn)為是最理想的OO設(shè)計方式,但極少有項目能完全采用它;測試驅(qū)動開發(fā)也被認(rèn)為是最佳的敏捷開發(fā)方式,但同樣極少有團(tuán)隊能徹底采用它。但是,恐怕沒有多少人在了解它們之后會否認(rèn)它們巨大的意義。
理想化的原則可以更好的幫助人們理解某類問題的本質(zhì),并做為好的出發(fā)點或者標(biāo)桿,幫助那些可以靈活運用,恰當(dāng)取舍的人取得更大的成績,應(yīng)付關(guān)鍵的挑戰(zhàn)。這正如孔子說的“取乎其上,得乎其中;取乎其中,得乎其下;取乎其下,則無所得矣”。
另外,值得一提的是,SOA從它的理念本身來說,就帶有一些的理想主義的傾向,比如向“全世界”開放,不限定客戶端等等。如果真愿意按SOA的路徑走,即使你是個土豪,偷個懶比浪費網(wǎng)絡(luò)帶寬重要,但說不定你的很多用戶是土鱉公司,浪費幾倍的帶寬就大大的影響他們的利潤率。
延伸討論:SOA和敏捷軟件開發(fā)矛盾嗎?
SOA的服務(wù)契約要求相當(dāng)?shù)姆€(wěn)定性,一旦公開發(fā)布(或者雙方合同商定)就不應(yīng)該有經(jīng)常的變更,它需要對很多方面有極高的預(yù)判。而敏捷軟件開發(fā)則是擁抱變化,持續(xù)重構(gòu)的。軟件設(shè)計大師Martin Fowler把它們歸結(jié)為計劃式設(shè)計和演進(jìn)式設(shè)計的不同。
計劃理論(或者叫建構(gòu)理論)和演進(jìn)理論是近代哲學(xué)的兩股思潮,影響深遠(yuǎn),派生出了比如計劃經(jīng)濟(jì)和市場經(jīng)濟(jì),社會主義和自由主義等等各種理論。
但是,計劃式設(shè)計和演進(jìn)式設(shè)計并不絕對矛盾,就像計劃經(jīng)濟(jì)和市場經(jīng)濟(jì)也不絕對矛盾,非此即彼,這方面需要在實踐中不斷摸索。前面我們討論的設(shè)計原則和架構(gòu)體系,就是將SOA層和OO應(yīng)用相對隔離,分而治之,在SOA層需要更多計劃式設(shè)計,而OO應(yīng)用可以相對獨立的演進(jìn),從而在一定程度緩解SOA和敏捷開發(fā)的矛盾。
延伸討論:SOA和REST是一回事嗎?
從最本質(zhì)的意義上講,REST(Representational State Transfer)實際是一種面向資源架構(gòu)(ROA),和面向服務(wù)架構(gòu)(SOA)是有根本區(qū)別的。
例如,REST是基于HTTP協(xié)議,對特定資源做增(HTTP POST)、刪(HTTP DELETE)、改(HTTP PUT)、查(HTTP GET)等操作,類似于SQL中針對數(shù)據(jù)表的INSERT、DELETE、UPDATE、SELECT操作,故REST是以資源(資源可以類比為數(shù)據(jù))為中心的。而SOA中的service通常不包含這種針對資源(數(shù)據(jù))的細(xì)粒度操作,而是面向業(yè)務(wù)用例、業(yè)務(wù)流程的粗粒度操作,所以SOA是以業(yè)務(wù)邏輯為中心的。
但是在實際使用中,隨著許多REST基本原則被不斷突破,REST的概念被大大的泛化了,它往往成為很多基于HTTP的輕量級遠(yuǎn)程調(diào)用的代名詞(例如前面提到過的HTTP + JSON)。比如,即使是著名的Twitter REST API也違反不少原始REST的基本原則。
在這個泛化的意義上講,REST也可以說是有助于實現(xiàn)SOA的一種輕量級遠(yuǎn)程調(diào)用方式。
SOA架構(gòu)的進(jìn)化
前面討論的SOA的所有問題,基本都集中在service本身的設(shè)計開發(fā)。但SOA要真正發(fā)揮最大作用,還需要不斷演進(jìn)成更大的架構(gòu)(也就是從微觀SOA過渡到宏觀SOA),在此略作說明:
第一個層次是service架構(gòu):開發(fā)各種獨立的service并滿足前面的一些設(shè)計原則,我們前面基本都集中在討論這種架構(gòu)。這些獨立的service有點類似于小孩的積木。
第二個層次是service composition(組合)架構(gòu):獨立的service通過不同組合來構(gòu)成新的業(yè)務(wù)或者新的service。在理想情況下,可以用一種類似小孩搭積木的方式,充分發(fā)揮想象力,將獨立的積木(service)靈活的拼裝組合成新的形態(tài),還能夠自由的替換其中的某個構(gòu)件。這體現(xiàn)出SOA高度便捷的重用性,大大提高企業(yè)的業(yè)務(wù)敏捷度。
第三個層次是service inventory(清單)架構(gòu):通過標(biāo)準(zhǔn)化企業(yè)服務(wù)清單(或者叫注冊中心)統(tǒng)一的組織和規(guī)劃service的復(fù)用和組合。當(dāng)積木越來越多了,如果還滿地亂放而沒有良好的歸類整理,顯然就玩不轉(zhuǎn)了。
第四個層次是service-oriented enterprise架構(gòu)……
總結(jié)
至此,我們只是簡要的探討了微觀層面的SOA,特別是一些基本設(shè)計原則及其實踐方式,以期能夠略微展示SOA在實踐中的本質(zhì),以有助于SOA更好的落地,進(jìn)入日常操作層面。
最后,打個比方:SOA不分貴賤(不分語言、平臺、組織),不遠(yuǎn)萬里(通過遠(yuǎn)程調(diào)用)的提供服務(wù)(service),這要求的就是一種全心全意為人民服務(wù)的精神……
作者簡介
沈理,當(dāng)當(dāng)網(wǎng)架構(gòu)師和技術(shù)委員會成員,主要負(fù)責(zé)當(dāng)當(dāng)網(wǎng)的SOA實施(即服務(wù)化)以及分布式服務(wù)框架的開發(fā)。以前也有在BEA、Oracle、Redhat等外企的長期工作經(jīng)歷,從事過多個不同SOA相關(guān)框架和容器的開發(fā)。他的郵箱:shenli@dangdang.com
感謝馬國耀對本文的審校。
給InfoQ中文站投稿或者參與內(nèi)容翻譯工作,請郵件至editors@cn.infoq.com。也歡迎大家通過新浪微博(@InfoQ)或者騰訊微博(@InfoQ)關(guān)注我們,并與我們的編輯和其他讀者朋友交流。