胶水代码(Glue Code)

# 缘起

最近在项目中遇到这样一个问题:项目规模很大,由多家厂商合作开发,其他厂商修改了接口的数据结构,导致原有已经开发好的功能无法使用,不得不修改代码来修复问题。

修改代码是再正常不过的事情,这个避免不了,但如何降低改动的成本呢?或者反向思考,什么样的代码修改起来成本是比较高的?

通常情况是,当提供服务的接口数据结构发生了改变,且直接依赖这些接口的模块又非常多的时候,调整代码的成本是比较大的,如果代码本身非常复杂,那么这种修改很可能会制造额外的 bug。

所以很明显,应该把这些依赖外部接口的代码提取出来,抽象成独立的一层,这一层屏蔽外部接口(或依赖的)的变化(或不兼容性),对系统内的其他模块提供稳定的数据结构(或行为)。

这让我想到了 胶水代码(Glue Code)

# 胶水代码

下面是 Wikipedia (opens new window) 关于胶水代码的定义:

In computer programming, glue code is executable code (often source code) that serves solely to "adapt" different parts of code that would otherwise be incompatible. Glue code does not contribute any functionality towards meeting program requirements. Instead, it often appears in code that lets existing libraries or programs interoperate, as in language bindings or foreign function interfaces such as the Java native interface, when mapping objects to a database using object-relational mapping, or when integrating two or more commercial off-the-shelf programs. Glue code may be written in the same language as the code it is gluing together, or in a separate glue language. Glue code is very efficient in rapid prototyping environments, where several components are quickly put together into a single language or framework.

从上面这段话中可以看到,胶水代码有如下三个特点:

  1. 用于适配互不兼容的系统(或模块)
  2. 能够提高开发速度
  3. 不涉及实际的业务逻辑

下面就围绕上述三点并结合一个前端 Demo 来看一下如何使用胶水代码(或者说是这种思想)来解决上述的问题。

# 用例

假设有一个 <Home/> 组件,在组件内部获取用户列表。可以按照下图的结构组织代码:

如图所示,UI 组件直接调用 API 模块,API 模块调用 HTTP Client 模块发送 HTTP 请求获取数据,从服务端返回的数据再逐层传递至 UI 组件,组件内部解析返回的数据,并将数据渲染出来。

下面是 <Home /> 组件的代码:

<template>
  <div v-if="isLoading">Loading...</div>
  <ul v-else>
    <!-- 易受到后端接口变化而出错的地方:1 -->
    <li v-for="user in users" :key="user.name">
      <!-- 易受到后端接口变化而出错的地方:2 -->
      {{ user.name }}
    </li>
  </ul>
</template>

<script setup>
import { ref } from '@vue/reactivity';
// User 模块为 API 模块
import { getUsers } from '../../apis/User';

const isLoading = ref(false);
const users = ref([]);

async function initUsers() {
  isLoading.value = true;

  try {
    // 直接调用 getUsers 方法,如果后端返回结果的字段 “name”
    // 发生变化,那么就需要修改组件模版内的取值
    const res = await getUsers();
    users.value = res.data;
  } catch (error) {
    console.error(error);
  }

  isLoading.value = false;
}

initUsers();
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

但是,如果后端同学此时修改了接口的数据结构,比如把用户名称字段由 name 改为 username,那么这个组件就需要修改两处代码(在上面的代码中已经标注)。如果组件交互或业务逻辑非常复杂,那么这种修改就需要花费一些时间和精力。

在这个例子中,后端服务属于 Service,而 <Home/> 属于 UI 组件,可以说修改后的 Service 与 UI 组件互不兼容,这满足使用胶水代码的条件。

可以在这两个模块之间加入胶水代码,这里的胶水代码服务于 UI 组件,对其提供稳定的数据结构,在内部“消化” Service 的“变化”,并且它只负责数据结构的转化。可以按照如下的结构来改进代码:

由于 API 模块和 HTTP Client 模块一般都是被多个组件共用的,而胶水代码是服务于 UI 组件的,所以可以把胶水代码放置在了 API 和 UI 组件之中间。根据上面的结构来改进代码:
















 











 










<template>
  <div v-if="isLoading">Loading...</div>
  <ul v-else>
    <!-- name 字段无需因后端服务接口的改变而改变 -->
    <li v-for="user in users" :key="user.name">
      {{ user.name }}
    </li>
  </ul>
</template>

<script setup>
import { ref } from '@vue/reactivity';
// fetchUsers 为胶水代码,但对于 Home 组件来说,它就是 API
// 所以将它放在 API 目录当中,注意这个文件夹的位置与 Users 文件
// 所在的 API 文件夹是不同的
import { fetchUsers } from './apis/fetchUsers';

const isLoading = ref(false);
const users = ref([]);

async function initUsers() {
  isLoading.value = true;

  try {
    // UI 组件并没有直接使用 src/apis/Users.js 来获取数据
    // 而是将任务委托给了胶水代码 ./apis/fetchUsers.js,由
    // 它来为组件处理好数据
    users.value = await fetchUsers();
  } catch (error) {
    console.error(error);
  }

  isLoading.value = false;
}

initUsers();
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

如上面代码所示,<Home/> 组件依赖 fetchUsers 模块来获取数据,而不再直接依赖于 Users 模块。

下面为 fetchUsers 模块(胶水代码):

import { getUsers } from '../../../apis/User';

/**
 * Fetch Users
 *
 * Glue Code
 *
 * 在这里可以使用 JsDoc 来帮助规范前端代码内部的数据结构,
 * 这样利于 “面向接口开发”,让代码易于维护
 *
 * @returns {Promise<{ name: String }[]>}
 */
export async function fetchUsers() {
  const { data: users } = await getUsers();

  return users.map((user) => {
    return {
      // Home 组件内部使用 name 表示用户名,无论服务端如何变化(
      // 字段由小写改为大写,或是因为后端修改了字段的名称为 username),
      // 只需要修改这里的代码就可以了
      name: user.username,
    };
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

也可以将胶水层单独封装到一个独立的服务当中(Backends for frontends,BFF),这样这一层就可以根据需要独立更新,而不用去更新前端系统。

# 总结

接口是不同模块、系统间遵守的“协议”,稳定的接口能够使得系统稳定的运行。在实际开发过程当中,因版本迭代或其它因素的影响,接口会发生变化。为了维持系统的的稳定运行,可以引入胶水代码隔离接口的变化。根据项目的规模和投入成本的不同,胶水代码的表现形式也不尽相同,可以作为一个系统的一部分,也可以作为一个独立的系统来服务于其它的系统。

# 资源