Appearance
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);
}现在,我们的新项目代码全面现代化,我们希望:
- 所有数据请求都返回
Promise,以便使用async/await。 - 所有数据格式都是
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 的逻辑,但又不能直接在业务代码里使用它。怎么办?
三、使用适配器模式重构
是时候制造一个“代码转接头”了。我们的适配器需要做两件事:
- 把
callback的方式适配成Promise的方式。 - 把
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 等浏览器在事件监听(
attachEventvsaddEventListener)等 API 上的差异。 - 框架版本过渡:在大型项目从 Vue 2 迁移到 Vue 3 时,可能会使用适配器模式来包装一些旧的 Vue 2 组件或插件,让它们能够在新版框架下暂时继续工作。
五、优缺点与适用场景
优点
- 解耦与复用:将客户端代码与被适配的、不兼容的接口解耦,让你可以复用一些旧的、但功能强大的类。
- 符合开放/封闭原则:你无需修改原有的旧代码(Adaptee),只需增加一个适配器类,即可满足新的接口需求。
- 提高透明度:对客户端来说,它调用的就是一个标准的目标接口,完全感觉不到适配过程的存在。
缺点
- 增加复杂性:引入额外的适配器类或函数,会增加系统的代码量和复杂性。如果只是为了适配一个非常简单的功能,可能会得不偿失。
适用场景
- 当你希望使用一个已经存在的类,但它的接口不符合你的需求时。
- 当你希望创建一个可以复用的类,用来与一些彼此之间没有太大关联的、未来可能出现的类协同工作时。
- 在需要集成第三方 SDK 或服务的场景中,这几乎是必备模式。