關于接口設計的一些思考
引子
做維護型工作,最大的收獲也許就是知道什么叫做丑陋了。本文針對我遇到的一些接口設計問題,總結了如下一些經驗分享給大家,希望我們能夠吸取經驗,對外提供最美的一面,即使我們的實現可能很丑,但是用戶不關心也看不到,這就是封裝的好處,哈哈。
1. 關于接口的粒度——應該提供應用無關的細粒度接口和應用相關的粗粒度接口
接口的粒度其實很大程度上是接口的職責問題。一般來說越細粒度的接口職責越內聚(偏向于Service),越粗粒度的接口職責越寬泛(偏向于Facade)。
筆者認為下面的做法是比較合理的:
- 對上層應用應該盡量提供粗粒度的接口,可以而且很多情況下是應用相關的。好處在于
- 提供更方便簡介的接口,屏蔽多接口的協作細節。
- 避免細粒度接口可能導致的多次讀取所帶來的不必要的性能消耗。比如,第一個方法調用查詢了數據庫得到的對象,在接下來的接口中都可以被使用。
- 減少不必要的多次參數檢查。
- 同時應該提供與具體應用無關的細粒度的接口,允許應用自己組裝應用邏輯。好處在于
- 業務無關,粒度夠細,有利于重用。
- 粒度細也就意味著職責清晰和內聚,利于解耦和維護。
- 利于單元測試
筆者認為接口一定要職責清晰,經常看到這樣的代碼,在一個for循環中"順帶"做了一些其他事情,原因是在這里作比較方便,否則需要重新遍歷一次,影響性能。但是由于這順帶作的事情,往往會導致接口職責不單一,污染了接口,導致接口的復用性變差。所有應該盡量抵制誘惑,接口與類都應該是職責單一的。
再說接口的粒度,提供細粒度的接口有利于重用,就像積木塊一樣,應用可以根據需要自行組裝。如果一開始就提供太粗粒度的接口,往往會有這樣的情況,有些應用場景下我只需要做其中的幾個步驟,這時候就容易導致重復類似的代碼出現了。這種情況下,應該將粗粒度的接口進行分解,將通用的步驟封裝成細粒度的接口(應該是應用無關),將粗粒度的接口暴露給上層應用(應該是應用相關的)。
使用粗粒度的接口可能帶來的一個問題是返回情偏多,具體處理詳見下面的關于返回值的討論。
2. 關于接口的命名——使用面向場景的接口簽名
接口名稱應該盡量面向場景,這不僅是接口友好性的表現,另一方面也是避免內部邏輯外泄的重要措施。比如:現在是否通過AV認證,是否提交AV認證都沒有相應的接口,而是提供了一個對AV_INFO_NEW表的查詢操作接口,這樣用戶如果要檢查是否已經提交AV認證信息,就必須這么做:
public static boolean hasUserSubmitAV(Integer companyId) {
AvInfoNewDO avInfoNewDO = avInfoService.findAvInfoNewByCompanyId(companyId);
if (avInfoNewDO == null || AvInfoStatusHelper.isAvinfoTransient(avInfoNewDO.getStatus())) {
return false;
} else {
return true;
}
}
這就是內部業務邏輯的外泄,如果以后是否提交AV認證不是這么一個判斷邏輯,就會導致大量的應用需要修改。另一方面,也是導致客戶端很多重復代碼。
目前為了方便使用spring的聲明式事務處理,我們的service在配置上都是繼承自intl-biz-datasource二方庫中的transactionDefinition。
這本來是一件好事,但是帶來的一個問題是Spring是根據方面簽名進行AOP的,而父類定義的是CUD數據庫操作類型的接口才攔截,這也導致了我們的接口看起來就象是一個CRUD接口。AOP不應該成為我們的一個限制,如果默認的AOP模式不能滿足我們的需求,可以重載父類定義。這其實是很有必要的,在多service接口協作的過程中,可能需要不同的service接口有不用的事務傳播類型。
另外,也可以考慮使用Anotation進行事務標注。
3. 關于接口的參數——最小粒度原則
現在很多接口都是這樣動不動就是一個DO或者DTO對象作為參數,但是到了實現一看,發現其實就是用到DO/DTO的兩三個字段而已。這種大對象作為參數實際上是一種非常不好的作法,特別是如果你只用到了大數據對象的一小部分字段而已。舉個例子大家就比較容易理解我為什么對它如此深惡痛絕了:在com.alibaba.intl.biz.product.service.interfaces.ProductService獲取產品獨立detail頁面URL的接口定義如下:
com.alibaba.intl.biz.product.service.interfaces.ProductService String getProductDetailUrl(URIBroker uriBroker, ProductSearchDTO product);
而在com.alibaba.intl.biz.product.service.impl.ProductServiceImpl中其實現只是用到了ProductSearchDTO的三個字段:getProductId(),getSubject(),getServiceType()。但是ProductSearchDTO這個類呢有十幾個屬性,并且還關聯了一些DO對象。你說這樣的一個接口給用戶,它怎么知道應該填充這個DTO的那些字段呢?!這對內存空間也是一種浪費。 直接定義成這樣的接口多簡單: com.alibaba.intl.biz.product.service.interfaces.ProductService
String getProductDetailUrl(URIBroker uriBroker, Integer productId, String subject, ServiceType serviceType);
其實傳遞URIBroker給下層也是一個不合理的做法,但這不在我們這一次討論范圍內
什么情況下傳遞整個DO/DTO對象是合理有用的呢?筆者認為以下三種情況可以考慮:
- 參數太多(超過6個),可以考慮將這些參數封裝成一個DO/DTO對象,
- 務必確保DO/DTO中的所有屬性都被該接口使用到了。
- 作為內部實現,以pipeline處理方式填充DO/DTO對象。
4. 關于接口的返回值——如何處理多種返回情況
接口的處理結果可能有多個返回情況,特別是接口粒度越粗,返回情況就越多。如何將處理結果返回給客戶,是一個需要好好考慮的問題。比如發布產品,如果將所有邏輯放在Service接口,那么接口必須支持多個返回結果,因為頁面需要根據不同的結果進行不同的提示。比如:如果用戶未提交AV認證,引導其先填寫AV信息。如果用戶發布的產品數超過了限度,提示之。如果用戶類目失效,提示之。如果。。。
一般來說有以下兩種方式:
-
一種定義一個ResultCode,即接口不只是返回true或者false,而是返回具體錯誤信息。頁面層根據調用結果代碼做相應的處理。但這會導致接口變得復雜,不好理解。
-
另一種方式是不通過返回值,而是定義自己的業務異常,通過拋出異常來告訴調用者結果。這會導致客戶端好多try catch,不過我們的聲明式事務控制也是要求Service接口拋出異常的。
建議是兩者的結合。但是返回結果碼是需要事先訂下的,后來再加上相當于改變了接口簽名。
5. 關于接口的可測性
好的接口不僅僅是對用戶友好,還應該是對自己友好。其中很大方面表現在可測性上。簡單來說,POJO對象可測性最高,所以盡量提供POJO參數接口。
在我們現在很多web層的util類中,web層對象到處走(甚至是web層框架特有對象),這導致可移植性,可測性和可復用性大大變差。比如WebUser雖然放在了ThreadLocal中,但是在biz和dal中獲取一個webUser做相應的檢查顯然是有問題的,因為他的定義就是web層使用的。還有在web層的很多util和helper類中,大量傳遞webx特有對象——rundata和context,直接從rundata中過去參數,驗證或者處理完又直接將結果塞入context中。特別是后者,看起來是方便,但是可測性就差好多了。因為你必須啟動整個容器,才能測試。 這里舉的例子主要都是針對web層的,其實biz層也是一樣的道理。
6. 總結
面向對象設計最大的原則就是針對接口設計。可見接口的重要性,如果接口能夠定義好,不僅便于自身維護,而且也導致上層應用不需要太多變動。想想Unix/Linux這么大的內核,也就200多個系統調用,非常穩定,沒有太多變化,人家是面向過程的編程思想,能做到這樣,確實有值得我們思考和學習的地方。希望我們以后在定義新接口的時候能夠多思考一下。另外,我們的intl-biz-product實在太單薄了,考慮我們像會員線一樣遷移到新的二方庫中,新增的接口一律放在新二方庫中,并且新增接口務必保證單元測試的完整性和可冪性。目前構想新的二方庫結構應該是api和impl分開(為服務化作準備),dal和biz分開(經常看到很多forBOPS,forMoree的SQL...),biz層還要細分通用邏輯層(細粒度的,應用無關的接口)和業務門面層(粗粒度的,應用相關的),每一層可能都有相應的common/share包。當然。只是一個我個人的一些粗步的構想(跟海滔討論過),歡迎大家提供建議。文檔地址如下:http://b2b-doc.alibaba-inc.com/pages/viewpage.action?pageId=45513924
關于UT的冪等性
所謂UT的冪等性,即只要被測函數沒有變化,跑N次這個UT應該都是一樣的結果。要保證這一點,需要做到如下幾點:
-
首先數據不能依賴于老數據,這意味著需要針對這個UT作相應的數據準備,可以使用DBUnit從數據庫中導出數據,根據需要簡單編輯一下。
-
其次還要保證數據的獨立性,幾個人同時操作一個數據庫或者操作一個存在大量未知數據的數據庫,將不能保證數據操作的正確性。要保證數據的獨立性和隔離性,可以使用先清空,執行測試,最后再回滾的方式。另一種方式是每個人跑自己的測試數據庫(可以使用內存數據庫)。
-
第三個是減少外部函數的依賴。如果你的被測函數的結果依賴于另一個函數,那么你很難保證被測函數的冪等性。解決這個問題的一個方式是使用Mock對象。
有了上面這三點保證,我們就可以保證這個UT在如下的輸入下必然有如下的輸出,這就是冪等性。
浙公網安備 33010602011771號