關於接口設計的一些思考

引子

作維護型工做,最大的收穫也許就是知道什麼叫作醜陋了。本文針對我遇到的一些接口設計問題,總結了以下一些經驗分享給你們,但願咱們可以吸收經驗,對外提供最美的一面,即便咱們的實現可能很醜,可是用戶不關心也看不到,這就是封裝的好處,哈哈。java

1. 關於接口的粒度——應該提供應用無關的細粒度接口和應用相關的粗粒度接口

接口的粒度其實很大程度上是接口的職責問題。通常來講越細粒度的接口職責越內聚(偏向於Service),越粗粒度的接口職責越寬泛(偏向於Facade)。web

筆者認爲下面的作法是比較合理的:spring

  • 對上層應用應該儘可能提供粗粒度的接口,能夠並且不少狀況下是應用相關的。好處在於
    1. 提供更方便簡介的接口,屏蔽多接口的協做細節。
    2. 避免細粒度接口可能致使的屢次讀取所帶來的沒必要要的性能消耗。好比,第一個方法調用查詢了數據庫獲得的對象,在接下來的接口中均可以被使用。
    3. 減小沒必要要的屢次參數檢查。
  • 同時應該提供與具體應用無關的細粒度的接口,容許應用本身組裝應用邏輯。好處在於
    1. 業務無關,粒度夠細,有利於重用。
    2. 粒度細也就意味着職責清晰和內聚,利於解耦和維護。
    3. 利於單元測試

筆者認爲接口必定要職責清晰,常常看到這樣的代碼,在一個for循環中"順帶"作了一些其餘事情,緣由是在這裏做比較方便,不然須要從新遍歷一次,影響性能。可是因爲這順帶做的事情,每每會致使接口職責不單一,污染了接口,致使接口的複用性變差。全部應該儘可能抵制誘惑,接口與類都應該是職責單一的。數據庫

再說接口的粒度,提供細粒度的接口有利於重用,就像積木塊同樣,應用能夠根據須要自行組裝。若是一開始就提供太粗粒度的接口,每每會有這樣的狀況,有些應用場景下我只須要作其中的幾個步驟,這時候就容易致使重複相似的代碼出現了。這種狀況下,應該將粗粒度的接口進行分解,將通用的步驟封裝成細粒度的接口(應該是應用無關),將粗粒度的接口暴露給上層應用(應該是應用相關的)。編程

使用粗粒度的接口可能帶來的一個問題是返回情偏多,具體處理詳見下面的關於返回值的討論。api

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對象是合理有用的呢?筆者認爲如下三種狀況能夠考慮:

  1. 參數太多(超過6個),能夠考慮將這些參數封裝成一個DO/DTO對象,
  2. 務必確保DO/DTO中的全部屬性都被該接口使用到了。
  3. 做爲內部實現,以pipeline處理方式填充DO/DTO對象。

4. 關於接口的返回值——如何處理多種返回狀況

接口的處理結果可能有多個返回狀況,特別是接口粒度越粗,返回狀況就越多。如何將處理結果返回給客戶,是一個須要好好考慮的問題。好比發佈產品,若是將全部邏輯放在Service接口,那麼接口必須支持多個返回結果,由於頁面須要根據不一樣的結果進行不一樣的提示。好比:若是用戶未提交AV認證,引導其先填寫AV信息。若是用戶發佈的產品數超過了限度,提示之。若是用戶類目失效,提示之。若是。。。

通常來講有如下兩種方式:

  1. 一種定義一個ResultCode,即接口不僅是返回true或者false,而是返回具體錯誤信息。頁面層根據調用結果代碼作相應的處理。但這會致使接口變得複雜,很差理解。

  2. 另外一種方式是不經過返回值,而是定義本身的業務異常,經過拋出異常來告訴調用者結果。這會致使客戶端好多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應該都是同樣的結果。要保證這一點,須要作到以下幾點:

  1. 首先數據不能依賴於老數據,這意味着須要針對這個UT做相應的數據準備,可使用DBUnit從數據庫中導出數據,根據須要簡單編輯一下。

  2. 其次還要保證數據的獨立性,幾我的同時操做一個數據庫或者操做一個存在大量未知數據的數據庫,將不能保證數據操做的正確性。要保證數據的獨立性和隔離性,可使用先清空,執行測試,最後再回滾的方式。另外一種方式是每一個人跑本身的測試數據庫(可使用內存數據庫)。

  3. 第三個是減小外部函數的依賴。若是你的被測函數的結果依賴於另外一個函數,那麼你很難保證被測函數的冪等性。解決這個問題的一個方式是使用Mock對象。

有了上面這三點保證,咱們就能夠保證這個UT在以下的輸入下必然有以下的輸出,這就是冪等性。

相關文章
相關標籤/搜索