抽象出 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阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容