抽象出 api 服務層的好處

0 偽代碼示例

0.1 demo結構

-| src
 -| editCenter
    -| A.js  // 獲取渠道列表,獲取媒體素材列表
 -| resourceCenter
    -| B.js  // 獲取媒體素材列表,獲取標簽列表
 -| mangeConfig
    -| C.js // 獲取渠道列表,獲取標簽列表
 -| service
    -| api
     -| tagApi.js
     -| mediaApi.js
     -| chanelApi.js

0.2 未抽象出api服務層的業務層代碼

A.js

……
    let ChanelListParams = {
      //……
    };
    let mediaListParams = {
        //...
    };
    // 獲取渠道列表
    function getChanelList(){
        params = dealParams(ChanelListParams);
        trshttpService.httpServer(trshttpService.getWCMROOT(),params,'post').
        then(function(data){
            dealData(data);
        });
    }

// 獲取素材列表
    function getMediaList(){
        params = dealParams(mediaListParams);
        trshttpService.httpServer(trshttpService.getBigdataROOT(),params,'get').
        then(function(data){
            dealData(data);
        });
    }
……

B.js

……  
    let TagListParams = {
        //...
    };
    let mediaListParams = {
        //...
    };

// 獲取標簽列表
    function getTagList(){
        params = dealParams(TagListParams);
        trshttpService.httpServer(trshttpService.getBigdataROOT(),params,'post').
        then(function(data){
            dealData(data);
        });
    }

// 獲取素材列表
    function getMediaList(){
        params = dealParams(mediaListParams);
        trshttpService.httpServer(trshttpService.getBigdataROOT(),params,'get').
        then(function(data){
            dealData(data);
        });
    }
……

C.js

……
    let TagListParams = {
        //...
    };
    let ChanelListParams = {
       
    };
// 獲取標簽列表
    function getTagList(){
        params = dealParams(TagListParams);
        trshttpService.httpServer(trshttpService.getBigdataROOT(),params,'post').
        then(function(data){
            dealData(data);
        });
    }

// 獲取渠道列表
    function getChanelList(){
        params = dealParams(ChanelListParams);
        trshttpService.httpServer(trshttpService.getWCMROOT(),params,'post').
        then(function(data){
            dealData(data);
        });
    }
……

0.3 抽象了api服務層的代碼

A.js

    let ChanelListParams = chanelApi.getListParams();
    let mediaListParams = mediaApi.getListParams();
// 獲取渠道列表
    function getChanelList(){
        params = dealParams(ChanelListParams);
        chanelApi.getChanelList(params).
        then(function(data){
            dealData(data);
        });
    }

// 獲取素材列表
    function getMediaList(){
        params = dealParams(mediaListParams);
        mediaApi.getMediaList(params).
        then(function(data){
            dealData(data);
        });
    }

B.js

let TagListParams = tagApi.getListParams();
let mediaListParams = mediaApi.getListParams();

// 獲取標簽列表
    function getTagList(){
        params = dealParams(TagListParams);
        tagApi.getTagList(params).
        then(function(data){
            dealData(data);
        });
    }

// 獲取素材列表
    function getMediaList(){
        params = dealParams(mediaListParams);
        mediaApi.getMediaList(params).
        then(function(data){
            dealData(data);
        });
    }

C.js

let TagListParams =  tagApi.getListParams();
let ChanelListParams = mediaApi.getListParams();
// 獲取標簽列表
    function getTagList(){
        params = dealParams(TagListParams);
        tagApi.getTagList(params).
        then(function(data){
            dealData(data);
        });
    }

// 獲取渠道列表
    function getChanelList(){
        params = dealParams(ChanelListParams);
        chanelApi.getChanelList(params).
        then(function(data){
            dealData(data);
        });
    }

tagApi.js

……
    return {
        getList: getTagList,
        getListParams: getListParams,
        ……
    };
    
    function getTagList(params) {
        return mockdata;
        return new Promise(function(resolve,reject){
            let params = dealParams(params);
            trshttpService.httpServer(trshttpService.getBigdataROOT(),params,'post')
            .then(function(data){
                let result = dealData(data);
                resolve(result);
            },function(){reject});
        });
    }
    
    function getListParams(){
        return {
            ……
        }
    }
……

mediaApi.js

……
    return {
        getMediaList: getMediaList,
        getListParams: getListParams,
        ……
    };
    
    function getMediaList(params) {
        return new Promise(function(resolve,reject){
            let params = dealParams(params);
            trshttpService.httpServer(trshttpService.getBigdataROOT(),params,'post')
            .then(function(data){
                let result = dealData(data);
                resolve(result);
            },function(){reject});
        });
    }
    
    function getListParams(){
        return {
            ……
        }
    }
……

chanelApi.js

……
    return {
        getChanelList: getChanelList,
        getListParams: getListParams,
        ……
    };
    
    function getChanelList(params) {
        return new Promise(function(resolve,reject){
            let params = dealParams(params);
            trshttpService.httpServer(trshttpService.getBigdataROOT(),params,'post')
            .then(function(data){
                let result = dealData(data);
                resolve(result);
            },function(){reject});
        });
    }
    
    function getListParams(){
        return {
        }
    }
……

1. 復用和語義化

將相同的請求提取到公共服務中,可以明顯提高代碼復用率。

我們從上面的未抽象api服務層的代碼看到,現在我們有三種請求,分別是:

  • 請求標簽列表
  • 請求素材列表
  • 請求渠道列表

盡管我們在底層已經層狀了一層trsHttpService.httpServer服務來實現http請求的細節,做到了很高的復用性,但這里還是存在著重復性代碼,比如在這三個文件中,每次都要重新定義請求參數對象,如果這個參數對象只有少數幾個屬性,倒也還可以,但是如果這個參數對象包含十幾二十個參數,每次要用到這個請求前,都需要定義一遍,那重復率就太高了。另外,像trshttpService.httpServer(trshttpService.getBigdataROOT(),params,'post') 這樣的代碼也出現了多次,不僅冗長,而且從這個方法本身并無法看出它是做什么事的,無法做到語義化。

在抽象了api服務層后,我們明顯看到兩個變化:

  • 參數對象不再在各自的控制器里定義,而是統一從api服務層獲取。定義只在api層發生一次,而在控制器中可以重用無數次,并且只需要一個方法調用。
  • 獲取數據的方法更加語義化,消除了無語義的功能性代碼

2. 應對變化

無論后端接口的參數改動,還是地址改動,或者返回值改變等,都可以在該層做適配,不需要影響業務層。

我們來假設三個場景。

場景一:請求參數變化。

加入我們未抽象api服務層時,控制器里定義的獲取標簽列表的請求參數如下:

let TagListParams = {
    tagModel:'aaa',
    tagTime: '...',
    pageSize: 8,
    curpage: 1
    ……
}

在進行請求前,我們肯定要根據用戶交互對參數進行賦值,比如:

function dealParams(params){
    params.tagModel = userSelectModel;
    params.tagTime = userInputTime;
    ...
    return params;
}

好了,功能開發完成后,后端告訴你,要把tagModel參數名稱改成 ModelName, 現在你會怎么做呢?當然,你可能想到了用編輯器的全局查找替換功能,可是,在某個其它模塊的某個前輩開發的代碼中,有下面這樣的代碼:

function someFun(){
    params.tagModel = abc;
    ...
}

而這個params可能和你要改的接口毫無關系,但全局替換可能會將它也替換掉,所以,為了避免這種問題,你不得不一個一個去替換,如果整個系統中請求標簽列表的地方有四五十處,那將是非常惱人的。

如果抽象出了api服務層,那就好辦了,我們看在api層里的代碼:

tagApi.js

 //……
 function dealParams(params) {
    //……
    params.ModelName = params.tagModel;
    delete params.tagModel;
    //……
    return params;
 }

只需要在api服務層發送強求前的參數處理函數里統一處理一次即可,業務層的代碼完全不用做任何改動。

場景二: 接口地址變化

在未抽象api層的代碼中我們看到,請求標簽列表和請求素材列表的接口地址是同一個,都是trshttpService.getBigdataROOT() 方法返回的,現在如果請求素材接口的地址不變還是用原來的,而請求標簽的接口使用了新的接口。那么按照老的做法,我們有如下步驟:

  1. trshttpService 中新增一個地址獲取方法 getNewRoot() 方法,并返回新的地址;
  2. 找到項目中所有請求標簽列表的地方,一個一個將getBigdataRoot方法替換為getNewRoot方法,當然,由于素材列表也使用的是getBigdataRoot方法,我們同樣無法使用編輯器的全局搜索替換功能。

然而,這種方法不僅效率低,還有最大的問題是,程序中那么多請求標簽的地方,你在執行替換的過程中很可能會遺漏掉那么一兩處,一旦發生這種情況,那就是生產事故。

那么,在抽象了api服務層后,我們怎么做的呢?

只有兩步:

  1. trshttpService 中新增一個地址獲取方法 getNewRoot() 方法,并返回新的地址。
  2. 進入tagApi.js ,將 getBigdataRoot 方法替換為 ``getNewRoot` 方法。

并且由于程序中所有請求標簽的地方都是調用的tagApi服務,所以不用擔心有遺漏。

場景三: 響應數據變化

假設原來的素材列表返回的相應數據是這樣的:

{
    DATA:[
    {
        relationid: 1,
        resoucetitle: '測試素材'
        //……
    },
    //……
    ],
    PAGER: {……}
}

而我們的A.jsC.js中,獲取到數據后,有大概四五十處都是使用 item.resourcetitle 來使用這個值的。

現在后端響應的數據改了,不再使用resourcetitle 來顯示素材名稱,而是改為了materialName。那我們怎么辦呢?

當然,我們一樣不能使用全局搜索替換,因為在系統的四十萬行代碼中你根本無法確定是否有個地方有一個與素材業務無關但也用了resourcetitle這個命名。我們當然也不可能一個一個去找到進行替換,一是效率低,二是有可能遺漏。

如果未抽象api服務層,我們最好的做法無非是:

在每一個控制器里,請求完素材的dealData()方法了做如下處理:

function dealData(data){
    data.foreach(function(item){
        item.resourcetitle  = item.materialName;
        delete item.materialName
    });
    this.items = data;
}

這樣做還得有一個前提是所有請求素材列表的地方獲得數據后都得有一個dealData方法,如果有些地方沒有,還必須再寫一個。

讓我們再次使用抽象api服務層的方式來解決這個問題吧。

我們只需要將上面的遍歷和替換在mediaApi.jsdealData 里實現一次即可,并且我們不要求所有調用者都去在調用接口后執行我們規定的dealData操作。

3. 面向接口編程

調用者不用關心請求地址如何獲取,也不關心請求是用POST方法還是GET方法,他唯一應該關心的是業務邏輯需要的數據如何獲取。

大家肯定也聽過面向接口編程,面向接口的核心思想有兩點:

  • 調用者不關心接口內部實現細節

  • 將定義與實現分離。

業務層需要使用數據時,只關心你給他一個獲取數據的方法,他并不需要去關心數據去哪個接口地址拿,也不需要關心這個數據是使用POST方法還是GET方法去獲取,他唯一需要關心的就是傳遞參數。就像我們去飯館吃飯點餐,我們并不需要關心飯店從哪里取進菜,也不需要關心廚師是用鐵鍋炒還是不銹鋼鍋炒,我們唯一需要的就是告訴服務員我們想吃什么。

在未抽象出api服務層時,我們將getWCMRootPOST 這樣涉及到實現的細節暴露到了業務層,讓業務調用者參與了具體的實現方式和細節,這是不合理的。

而我們抽象出了api服務層后,調用者只需要知道有個叫tagApi的服務有一個getTagList()方法可以給我們想要的標簽列表,就夠了。

上面我們解釋了面向接口編程的第一個特性:調用者不關心接口內部實現細節。下面我們看下第二個特性: 將定義與實現分離。

我們看到,在抽象出的api服務層,我們是這樣返回的:

    return {
        getTagList: getTagList,
        getListParams: getListParams,
        ……
    };
    function getTagList(){//...}
    function getListParams(){//...}

而不是像這樣:

    return {
        getTagList: function(params){
            //...
        },
        getListParams: function(params){
            //...
        }
    }

或者這樣:

this.getTagList = function(params){//...}
this.getListParams = function(params){//...}

這種寫法上的細微差異在實際開發中帶來的效果是完全不一樣的。后兩種寫法都是將定義和實現耦合在了一起,而第一種方法實現了定義與實現的分離。這兩種思想的差異在這段代碼里看不出明顯差異,我們換個例子。

我們前端很多時候都會涉及到存儲,假設現在的存儲方案有cookielocalStorage兩種,一開始我們采用了cookie方案,api 像下面這樣:

  • storeApi.js
return {
    this.getCookie = function(){...}
    this.setCookie = function(){...}
    this.removeCookie = function(){...}
}

調用者像下面這樣使用:

  • D.js

    storeApi.getCookie(...);
    storeApi.setCookie(...);
    storeApi.removeCookie(...);
    

后來發現cookie 方案不再滿足我們的需求了,更換為 localStorage方案了,那有這么幾種方式:

方式一:偷懶改原來的實現

  • store.js
this.getCookie = function(){//這里是localStorage實現}
this.setCookie = function(){//這里是localStorage實現}
this.removeCookie = function(){//這里是localStorage實現}

這樣做的好處是業務代碼不用更改,壞處很明顯,方法名和實現不一樣,造成后期維護成本上升.

方式二:新建一個服務

  • stroeForLocal.js
this.getLocalStorage = function(){...};
this.setLocalStorage = function(){...};
this.removeLocalStorage = function(){...};

然后修改業務代碼:

  • D.js
stroeForLocalApi.getLocalStorage(...)
stroeForLocalApi.setLocalStorage(...)
stroeForLocalApi.removeLocalStorage(...)

這樣做壞處顯而易見:需要大量改動業務代碼。

如果我們一開始就采用定義與實現分離的方式,看看代碼:

  • store.js

    return {
      get: getCookie,
      set: setCookie,
      remove: removeCookie
    };
    
    function getCookie(){//...}
    function setCookie(){//...}
    function removeCookie(){//...}
    
  • D.js

    storeApi.storemethod = 'local'
    storeApi.get(...);
    storeApi.set(...);
    storeApi.remove(...);
    

變更為localStorage方案后:

  • store.js

    this.storeMethod ='';
    if(this.stroeMethod === 'cookie'){
    return {
      get: getCookie,
      set: setCookie,
      remove: removeCookie
    };
    }else {
    return {
      get: getLocalStorage,
      set: setLocalStorage,
      remove: removeLocalStorage
      
    };
    }
    
    
    function getCookie(){//...}
    function setCookie(){//...}
    function removeCookie(){//...}
    function getLocalStorage(){...}
    function setLocalStorage(){...}
    function removeLocalStorage(){...}
    

而業務層則是無感知的。

這就是定義與實現分離所帶來的好處,業務層只需要知道有個存儲服務提供了set,get,remove方法就行了,它并不關心該服務采取哪種存儲方案,存儲方案的變化只需要體現在api服務層,不影響業務層。

4. 減少依賴

在請求服務還沒開發的情況下(也有可能是后端接口還沒開發完),只要給調用者提供一份接口清單,調用者就可以進行業務開發了,等請求服務將這些接口全部實現后,調用者不用做任何修改就可以直接使用。(可以配合mock方案)

在未抽離出api服務層的情況下,我們一般的開發過程是

  • 拿到一個需求
  • 等后端給接口文檔的同時先寫靜態頁面(可能會在業務代碼中造假數據)
  • 等到后端給我們接口文檔后,我們開始寫數據請求部分
    • 這時候如果后端接口已經完成,我們就直接對接數據,并在業務代碼中寫請求了;
    • 這時候如果后端接口還沒寫,我們就只能先按照接口文檔先寫拼參數發請求的部分,至于數據,還是自己造的假數據。
    • 后面如果后端通知我們接口有變動,我們又得挨個在業務代碼里做相應的改動。

這個過程中,我們對后端其實是存在著很強的依賴性的。

比如按照未抽象api服務層的代碼邏輯,在后端只有文檔尚未開發的時候,連后端都不知道接口要部署到哪個地址,那么前端就沒法去寫trshttpService.getBigdataRoot 這樣的代碼,因為你根本不知道地址。

而在抽象出api服務層后,我們的過程可能變成了下面這樣:

  • 拿到一個需求,根據需求在api服務層定義數據請求接口,可以先不實現請求,但可以根據頁面展示的元素定義并返回假數據(mock),這些假數據的字段名稱完全可以自己根據業務需求來定義。
  • 等后端給接口文檔的同時做開發,同時利用假數據來進行頁面功能測試,
  • 等后端給我們接口文檔后,我們在api層對參數和響應數據進行適配,而業務層代碼只需要因為參數的不同做少量適配就好。
  • 等后端完成后,在api 服務層去掉假數據,對接真實接口。

以上面的場景為例,即使后端還未開發,我們在業務代碼中也只是調用tagApi.getTagList()這樣的方法,在api層我們直接返回假數據,這樣既不影響前期開發節奏,后期即使對接了真實接口后,這一塊的代碼也不用變動。

我們看到,這個過程最大程度地減少了前端開發對后端接口的依賴,使得我們的開發更加順暢。

5. 單一職責

數據請求工作應該由專門的數據請求服務來完成,包括發送請求前的參數處理和接收到數據后的數據適配工作

以上說的都是實際開發中api服務層帶給我們的好處,而之所以有那些好處,是因為它本身遵循了良好的設計原則,尤其是編程領域最重要的一條設計原則:單一職責原則。

單一職責的核心點就是各司其職,業務的歸業務,服務的歸服務。

就像我們去飯館吃飯,我們不會自己把點菜單送去廚房,而是由服務員送去廚房一樣,涉及請求的東西也不應該由調用者來參與(比如請求地址獲取、http方法指定、參數處理、響應數據適配)等等。

所有涉及請求的工作(包括請求的前置工作和后置工作)都應該統一交由api服務層來進行,這才符合我們說的單一職責原則。

6.便于統一管理

在業務邏輯還未開發前就可以根據后端提供的接口文檔把所有和數據請求相關的事務集中完成

基于以上的工作,進行了api服務的抽象后,所有和請求相關的工作都可以統一放置在api服務層進行了,這樣便于統一管理所有接口請求,并且還帶來一個好處: 如果你在開發前已經拿到了后端給的接口文檔,那你就可以直接根據接口文檔把所有和請求相關的事務先集中完成,然后再去做業務,這樣不用在接口和業務之間來回切換,提升開發效率。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,844評論 2 372

推薦閱讀更多精彩內容