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

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