Skip to content

4.1 适配器模式 (Adapter Pattern)

欢迎来到结构型模式的第一站。我们将从一个非常实用、旨在解决“不兼容”问题的模式开始——适配器模式。

核心定义:将一个类的接口转换成客户端所期望的另一个接口。适配器模式让那些接口不兼容的类可以协同工作。

一、生活中的场景:电源转接头

这个模式最经典、最直观的比喻就是电源转接头

  • 你的电器 (Client):比如你的 MacBook,它期望插入一个两孔的国标插座。
  • 墙上的插座 (Target Interface / 目标接口):这是一个国标的两孔插座。
  • 港版充电器 (Adaptee / 被适配者):不幸的是,你带来的是一个港版充电器,它是一个英标的三脚大插头。
  • 转接头 (Adapter / 适配器):你拿出一个小小的转接头。它的一面可以插入你的三脚插头,另一面则可以完美地插入墙上的两孔插座。

这个“转接头”本身不产生电,也不消耗电。它唯一的作用就是转换接口,让原本无法一起工作的两样东西能够协同工作。

适配器模式在代码世界里,扮演的就是这个“转接头”的角色。

二、没有适配器模式的代码:格格不入的旧接口

假设我们的项目早期使用了一个自定义的 ajax 工具函数,它用于从后端获取数据。这个函数设计得比较老旧,它返回的是一个 XML 格式的字符串。

javascript
// --- old-ajax.js --- (一个我们无法修改的旧库)
// 这是一个老旧的 ajax 函数,它通过回调函数来处理 XML 字符串
function oldAjax(url, callback) {
  console.log(`正在通过旧 ajax 请求: ${url}`);
  // 模拟网络请求
  setTimeout(() => {
    // 模拟从服务器返回的 XML 数据
    const xmlData = "<user><name>张三</name><age>20</age></user>";
    callback(xmlData);
  }, 500);
}

现在,我们的新项目代码全面现代化,我们希望:

  1. 所有数据请求都返回 Promise,以便使用 async/await
  2. 所有数据格式都是 JSON 对象,而不是 XML 字符串。

我们的新业务代码期望的接口是这样的:

javascript
// --- new-business-logic.js ---
// 新的业务代码期望所有请求都是这样的风格
async function fetchData() {
  // 期望的 http 请求函数
  const response = await http.request("/api/user/1");
  // 期望 response 是一个 JSON 对象
  console.log(response.name);
}

问题来了:oldAjax 函数和我们的新期望完全不匹配!我们想复用 oldAjax 的逻辑,但又不能直接在业务代码里使用它。怎么办?

三、使用适配器模式重构

是时候制造一个“代码转接头”了。我们的适配器需要做两件事:

  1. callback 的方式适配Promise 的方式。
  2. XML 字符串适配JSON 对象。
javascript
// --- adapter.js ---
// 1. 我们需要一个 XML 转 JSON 的辅助函数
function xmlToJson(xmlStr) {
  // 这是一个非常简化的转换,真实场景会用库
  const name = xmlStr.match(/<name>(.*?)<\/name>/);
  const age = xmlStr.match(/<age>(.*?)<\/age>/);
  return { name, age: parseInt(age) };
}

// 2. 核心:适配器函数
function ajaxAdapter(url) {
  return new Promise((resolve, reject) => {
    // 在适配器内部,调用旧的、不兼容的接口
    oldAjax(url, (xmlData) => {
      try {
        // 在这里进行关键的“转换”工作
        const jsonData = xmlToJson(xmlData);
        // 对外暴露符合新规范的接口
        resolve(jsonData);
      } catch (err) {
        reject(err);
      }
    });
  });
}

// =======================================================
// --- new-business-logic.js ---
// 现在,我们的业务代码可以这样写:
const http = {
  // 将适配器挂载到我们期望的接口上
  request: ajaxAdapter,
};

async function fetchData() {
  console.log("开始请求数据...");
  // 业务代码调用的接口是全新的、统一的!
  // 它完全不知道背后其实是一个老旧的 ajax 在工作
  const response = await http.request("/api/user/1");
  console.log("成功获取数据:", response);
  console.log(response.name); // 输出: 张三
}

fetchData();

通过 ajaxAdapter 这个中间层,我们成功地让新旧代码协同工作,而新业务逻辑的代码完全不需要知道ajax 的存在。它面向的是一个统一、现代的 http.request 接口。

四、真实世界的应用

  • 数据格式统一:当你的应用需要从多个不同的 API 获取数据,而这些 API 返回的数据结构千奇百怪时,你可以为每个 API 写一个适配器,将它们返回的数据统一转换成你应用内部的标准数据模型。
  • 兼容浏览器差异:早期的前端开发中,经常需要写适配器来抹平 IE 和 Chrome 等浏览器在事件监听(attachEvent vs addEventListener)等 API 上的差异。
  • 框架版本过渡:在大型项目从 Vue 2 迁移到 Vue 3 时,可能会使用适配器模式来包装一些旧的 Vue 2 组件或插件,让它们能够在新版框架下暂时继续工作。

五、优缺点与适用场景

优点

  1. 解耦与复用:将客户端代码与被适配的、不兼容的接口解耦,让你可以复用一些旧的、但功能强大的类。
  2. 符合开放/封闭原则:你无需修改原有的旧代码(Adaptee),只需增加一个适配器类,即可满足新的接口需求。
  3. 提高透明度:对客户端来说,它调用的就是一个标准的目标接口,完全感觉不到适配过程的存在。

缺点

  1. 增加复杂性:引入额外的适配器类或函数,会增加系统的代码量和复杂性。如果只是为了适配一个非常简单的功能,可能会得不偿失。

适用场景

  • 当你希望使用一个已经存在的类,但它的接口不符合你的需求时。
  • 当你希望创建一个可以复用的类,用来与一些彼此之间没有太大关联的、未来可能出现的类协同工作时。
  • 在需要集成第三方 SDK 或服务的场景中,这几乎是必备模式。