后端接口返回的数据格式不兼容怎么办?无需修改业务代码帮你搞定!

你有没有遇到过这样的情况:随着项目版本的迭代,你发现有时候后端同事提供的接口和当前项目中正在用的接口数据/字段不一致,导致你的业务代码与新的 api 接口返回的数据不兼容,让你总是花费海量的时间和精力去修改、调整你现有的代码逻辑才能适配新的接口。

不知道你碰到这样的情况心情会怎么样,发生在我身上的时候,就非常烦 ~

这是我们在开发工作中肯定会遇到的问题,尤其是在大版本迭代中,基本上避免不了这个问题。这里也不是搞前后端对立,后端也有后端的难处,比如因为需求变动而导致某些接口和字段必须要做出调整。

所以,我们要互相理解对方的困难。但是,有没有一种方法可以解决类似这样的问题呢?

当然有!这就是我们今天要分享的内容——适配器模式。

  • 今天的内容主要包括:
    • 适配器模式定义;
    • 适配器模式组成;
    • 适配器模式实现;
    • 适配器模式应用场景;
    • 适配器模式优缺点。

好了,话不多说,我们正式开始吧 ~

适配器模式定义

什么是适配器模式呢?

在我们前端开发中,适配器模式可以帮助我们解决 JS 中接口不兼容的问题。尤其是在项目升级或重构时保持代码兼容性方面,适配器模式有着广泛应用。

我们举个例子:比如你要用笔记本读取一张相机内存卡中的照片,但是你发现你的笔记本没有内存卡插槽。

但是,常见的笔记本都会提供一个读卡器插槽,可以将你的内存卡先插到读卡器中,然后再将读卡器插入电脑插槽中。

同样的,适配器在我们开发中也起着类似的作用。它可以作为两个接口不兼容的类的桥梁,让这两个类一起正常工作。

那么,适配器模式是如何做到这一点的呢?

适配器模式通过引入一个新的适配器类,让这个类实现目标接口,并在内部封装一个被适配者的实例。这样,适配器类就可以将目标接口的调用转换为被适配者的调用,从而使两个接口不兼容的类能够一起工作。

适配器模式构成

适配器模式主要由三个部分组成:目标接口、适配器和被适配者。

当 目标接口 的 request 方法被调用时,适配器 会将这个调用转换为 被适配者 的 specificRequest 调用。

  • 下面我们来分别看一下适配器模式的这三个组成部分:
    • 目标接口:这是我们期望的接口。也就是说,它是我们的代码能够直接使用的接口。
    • 适配器:这是适配器模式的核心。适配器的主要任务就是将目标接口的调用转换为被适配者的调用。
    • 被适配者:这是我们需要适配的对象,也就是我们不能直接使用,但又需要使用的接口。

我们在代码中应用适配器模式时,完全不需要关心被适配者的接口是什么,只需要关心目标接口就可以。

适配器模式实现

前面我们提到,适配器模式主要用于对新旧 api 返回的数据做兼容。假设,我们有一个旧的 api 返回的数据,如下:

{
  "status": "success",
  "data": {
    "name": "John",
    "age": 30
  }
}

我们在业务中的异步请求代码可能是下面这样的:

// 旧 api 数据处理函数
function handleOldApiResponse(response) {
  if (response.status === 'success') {
    console.log(`Name: ${response.data.name}, Age: ${response.data.age}`);
    // 对旧数据做处理,以便在业务中使用...
  }
}

随着项目版本的迭代,在一次新版本中,因业务调整,后端同事不得不对返回的数据字段做调整。比如返回的数据字段改成了下面的样子:

{
  "statusCode": 200,
  "result": {
    "firstName": "John",
    "years": 30
  }
}

由于我们在前端页面中渲染的字段是从旧 api 返回的数据中获取的,但是新的 api 返回值中,原先的字段都做了变更。如果只是少量的接口返回值有变动,我们改一下业务代码中的取值即可。但是,如果变更非常多,比如一次大的版本迭代,或者是一次重构版本,那我们的工作量可想而知了。

而且,真实的业务中,每个接口或页面上渲染的字段值都不是孤立的,它们之间极有可能会有依赖关系。如果我们修改大量的业务代码,很有可能会导致不可预期的问题。这极大地增加了我们的工作负担。

这时,如果合理应用适配器模式的话,能极大程度上减少我们的工作量,还可以尽尽可能的避免因大量修改业务代码可能出现的问题。

所以,我们在不修改业务代码的前提下,只需要给相应的异步请求提供一个适配器,也就是在获取新数据后对返回的新数据做一层适配,将新的数据格式转换为旧的数据格式。

// 新数据经过适配器的处理后,可兼容旧接口返回的数据格式
function apiAdapter(newApiResponse) {
  return {
    status: newApiResponse.statusCode === 200 ? "success" : "fail",
    data: {
      name: newApiResponse.result.firstName,
      age: newApiResponse.result.years,
    },
  };
}

// 这是新 api 返回的新数据
const newApiResponse = {
  statusCode: 200,
  result: {
    firstName: "John",
    years: 30,
  },
};

// 已经过适配器兼容过的数据,与旧数据格式兼容
const adaptedResponse = apiAdapter(newApiResponse);

// 新数据格式转换成旧数据格式后,仍然调用原先的方法处理数据,以便在业务中使用
handleOldApiResponse(adaptedResponse); // 输出:Name: John, Age: 30

有没有发现,我们既没有修改旧的 api 方法,也没有添加新的 api 方法,只需要在拿到新的数据格式后,对其做一次兼容即可,把新的数据格式转换成旧的数据格式。这样,我们完全不需要改动业务上的字段取值。

所以说,适配器模式能让我们在不修改现有代码的前提下,可以让业务使用新的 api 数据结构。

适配器模式优缺点

适配器模式最大的优点就是让我们免去了调整业务逻辑代码的过程。为新接口修改相关的业务代码,有时候工作量大的能让你怀疑人生。但是,有了适配器模式,我们只需要在获取到新数据后写一个相应的适配器,就可以让旧的业务代码和新的数据兼容。

而且,适配器模式也能让我们的代码更灵活,我们在需要做兼容的地方只需要添加一个适配器即可,其余部分完全不需要改动。

但是,过多的使用适配器模式也会增加项目的复杂度,也有可能会隐藏一些问题,导致对出现的问题排查起来比较困难。

如果我们的项目中确实存在大量的接口不兼容问题,而且这些问题是无法避免的,那么使用适配器模式无疑是一个很好的选择。但是,如果我们的项目中只有少量的接口不兼容问题,或者这些问题可以通过其他方式解决,那么我们就需要谨慎考虑是否需要使用适配器模式了。

而我呢,通常会把适配器模式作为一个“渐进式”的解决方案来应用。简单来说,比如在一次大版本的迭代中,由于开发周期不充裕,这时候我们就可以先通过适配器模式系统性的完成这次版本的迭代工作。之后,再根据具体的任务排期,把适配器适配的新数据格式依次替换到具体的业务中。这样能一定程度上减少适配器的数量,直到最后把所有相关的业务代码修改到最新版本。这样的话,既能确保开发任务的推进,又能保证项目质量。

当然,这是在系统还未完全稳定的前提下,为了“逃避”加班不得已而为之的。所以,下次你遇到大规模迭代时,可以考虑应用一下适配器模式,能让你少加班哦 ~

贡献者: mankueng