從Moco談程序庫設(shè)計
Moco在程序庫設(shè)計包括兩個方面,如何設(shè)置服務(wù)器和如何讓服務(wù)器運行起來。
先說簡單的,如何讓服務(wù)器運行。最簡單的選擇是讓用戶自己啟動服務(wù)器,然后,在測試結(jié)束之后關(guān)閉服務(wù)器。這么一來,每個測試都大概會是這樣:
- public void shouldWork() {
- ... setup server ...
- try {
- server.start();
- ... your test here ...
- } finally {
- server.stop();
- }
- }
作為一個框架,留給客戶使用的接口一定簡單,把實現(xiàn)細節(jié)封裝在框架以內(nèi)。如果每個測試都這么寫,用戶很快就會罵娘了。現(xiàn)在的Moco的做法是使用了running方法,把Server啟停的細節(jié)封裝了起來。
- running(server, new Runnable() {
- @Override
- public void run() throws Exception {
- assertThat(helper.get(root()), is("foo"));
- }
- });
我知道你不喜歡匿名內(nèi)部類,我也不喜歡,但是,這是Java的局限。等到Java 8駕臨,相信一切會有所改觀。
設(shè)置服務(wù)器,也就是在什么樣的請求,給什么樣的應(yīng)答。Moco有一個很關(guān)鍵的起點,那便是DSL,要知道,之前的一段時間我剛剛完成了《領(lǐng)域特定語言》的翻譯。于是,API的可讀性成了一件非常重要的事。
說起來很簡單,但是,要知道匹配請求的條件有很多,而且還可能任意組合。如果這些條件是彼此獨立的(“或”),我可以用變參來解決,但有時,這些條件是相關(guān)的(“與”),那該怎么辦呢?拜函數(shù)式編程思路所賜,我想到了函數(shù)組合的方式。
我知道,Java沒有一等公民的函數(shù),但是,Java里有一等公民的對象,我們可以用對象模擬函數(shù),事實上,這也是很多面向?qū)ο蟪绦蛟O(shè)計語言解決函數(shù)組合的一種方式,而這種對象稱為函數(shù)對象,也叫functor。
把需要的條件封裝成一個個函數(shù)對象,然后通過幾個簡單的運算符就可以將它們組合起來。目前Moco里提供了and和or兩個運算符用于組合。
- server.request(and(by(uri("/foo")), by(method("put")))).response("bar");
- server.request(or(by("foo"), by(uri("/foo")))).response("bar");
隨著面向?qū)ο笏悸返钠占?,函?shù)組合的寫法會越來越普遍的,它會成為程序員工具箱中的必備品。
DSL是一個重要的考量,所以,這里沒有直接new對象,而是用函數(shù)(比如uri、method)做了一層封裝?;蛟S你會說,那為了可讀性,我可以把類名設(shè)計成一個可讀的樣子,但new一個對象的問題在哪呢?就在new上。一方面,new是為了創(chuàng)建一個對象,這是實現(xiàn)細節(jié),與我們要表達的內(nèi)容不在一個層次,另一方面,你會發(fā)現(xiàn)多個new會讓這句話顯得不連貫。這句話?是的,我們的寫法就像是用一句話聲明一個東西一樣,而我們期望自己講的內(nèi)容有一定的連貫性,而這才是DSL。
一旦設(shè)計成DSL,單個函數(shù)的文檔也就失去了意義,更重要的是一個用法的文檔。所以,在Moco里,我寫了文檔,卻沒有寫JavaDoc。
在Moco里,請求和應(yīng)答里可能有同樣的東西,比如file,而request和response的參數(shù)類型是不一樣的。如果file能夠返回不同類型是***的,可惜在Java里面,我們不可能根據(jù)返回類型做重載。一種解決方案是為request和response分別設(shè)計一個函數(shù),比如requestFile和responseFile,但顯然,這種做法會把同樣的邏輯分散到兩個類實現(xiàn)里,而且這樣需要共享的東西不只是file,還有其它一些東西。
在計算機問題里,加上一個中間層永遠是一個重要的解決方案。這也是by的目的所在。這樣一來,這些共享的東西就可以做成一個抽象(Resource),對請求來說,只要有一個by,就可以適配成Matcher。
在Moco設(shè)計中,還涉及到了一個框架設(shè)計中很重要的點:類型。Java是靜態(tài)類型程序設(shè)計語言,利用好類型,就可以在編譯期把錯誤報出來,而不會留到運行時,fail fast是很重要的一個程序設(shè)計實踐。
Moco目前支持一些Content Type檢測。如果把這個Content Type放到Resource里面,你會發(fā)現(xiàn)不是所有Resource都需要這個,比如method,我們當然可以在Resource接口里支持Content Type,在不需要的地方拋出異常。
在Moco里,我的做法是引入了一個ContentResource,支持Content Type,它繼承于Resource,這樣依賴,需要Content Type的接口(比如content),直接支持ContentResource,而其它部分繼續(xù)支持Resource,這樣,如果一不小心用錯的話,編譯就會報錯。
再有一點是關(guān)于Publish接口,在Moco的實現(xiàn)里有一個Setting,還有一個BaseSetting,如果觀察實現(xiàn),在設(shè)置Request會創(chuàng)建出一個BaseSetting,但其返回的接口是Setting。這么做的一個原因是,因為Setting出現(xiàn)在用戶可以使用的接口上,而BaseSetting是留給內(nèi)部實現(xiàn)的,它提供了一些用于內(nèi)部實現(xiàn)的方法,而用戶是不應(yīng)該使用這些方法的,所以,將二者做了一個分離,這樣一來,保證了用戶不會誤操作。同樣處理的還有HttpServer和ActualHttpServer。
小結(jié)一番,簡化用戶接口,設(shè)計DSL,利用好類型,區(qū)分Publish接口。