您现在的位置是:网站首页 > 适配器模式(Adapter)的接口转换实践文章详情
适配器模式(Adapter)的接口转换实践
陈川
【
JavaScript
】
31111人已围观
7042字
适配器模式(Adapter)的接口转换实践
适配器模式是一种结构型设计模式,它允许不兼容的接口之间进行协作。就像现实世界中的电源适配器可以让不同标准的插头工作一样,在编程中,适配器模式充当两个不同接口之间的桥梁。
适配器模式的核心概念
适配器模式包含三个主要角色:
- 目标接口(Target):客户端期望使用的接口
- 适配器(Adapter):将源接口转换为目标接口的包装器
- 适配者(Adaptee):需要被适配的现有接口
在JavaScript中,适配器通常是一个函数或对象,它接收适配者的接口,然后转换为客户端期望的接口形式。
简单示例:数据格式转换
假设我们有一个第三方库,它返回的数据格式是XML,但我们的应用期望使用JSON格式:
// 适配者 - 返回XML数据的函数
function getXMLData() {
return `
<user>
<name>张三</name>
<age>30</age>
<email>zhangsan@example.com</email>
</user>
`;
}
// 适配器 - 将XML转换为JSON
function xmlToJsonAdapter(xmlString) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlString, "text/xml");
return {
name: xmlDoc.querySelector('name').textContent,
age: parseInt(xmlDoc.querySelector('age').textContent),
email: xmlDoc.querySelector('email').textContent
};
}
// 客户端代码
const jsonData = xmlToJsonAdapter(getXMLData());
console.log(jsonData);
// 输出: {name: "张三", age: 30, email: "zhangsan@example.com"}
复杂示例:接口版本适配
在实际开发中,我们经常需要处理API版本升级带来的接口变化。适配器模式可以帮助我们平滑过渡:
// 旧版API接口
class OldAPI {
fetchUser(id) {
return {
user_id: id,
full_name: '李四',
contact_email: 'lisi@example.com'
};
}
}
// 新版API期望的接口格式
class NewAPIClient {
getUser(id) {
// 期望返回格式: {id: string, name: string, email: string}
}
}
// 创建适配器
class APIAdapter {
constructor(oldAPI) {
this.oldAPI = oldAPI;
}
getUser(id) {
const oldUser = this.oldAPI.fetchUser(id);
return {
id: oldUser.user_id,
name: oldUser.full_name,
email: oldUser.contact_email
};
}
}
// 使用适配器
const oldAPI = new OldAPI();
const adapter = new APIAdapter(oldAPI);
const newAPIClient = new NewAPIClient();
// 现在可以使用适配后的接口
const user = adapter.getUser(123);
newAPIClient.getUser = () => user;
函数式适配器实现
在函数式编程风格中,适配器可以简单地实现为高阶函数:
// 源函数 - 接受回调
function legacyFetchData(callback) {
setTimeout(() => {
callback({ status: 'ok', data: [1, 2, 3] });
}, 100);
}
// 适配器 - 将回调风格转为Promise
function promisify(fn) {
return function() {
return new Promise((resolve, reject) => {
fn((result) => {
if (result.status === 'ok') {
resolve(result.data);
} else {
reject(new Error('Request failed'));
}
});
});
};
}
// 使用适配后的函数
const modernFetchData = promisify(legacyFetchData);
modernFetchData()
.then(data => console.log(data)) // [1, 2, 3]
.catch(err => console.error(err));
适配器模式在UI组件中的应用
前端开发中,我们经常需要适配不同的UI组件库。例如,将Element UI的表格适配为Ant Design的表格:
// Element UI表格配置
const elementTableConfig = {
data: [],
columns: [
{ prop: 'name', label: '姓名' },
{ prop: 'age', label: '年龄' }
],
rowKey: 'id'
};
// 适配器函数
function adaptToAntdTable(config) {
return {
dataSource: config.data,
columns: config.columns.map(col => ({
title: col.label,
dataIndex: col.prop,
key: col.prop
})),
rowKey: config.rowKey
};
}
// 使用适配后的配置
const antdTableConfig = adaptToAntdTable(elementTableConfig);
处理多个适配者的情况
有时我们需要同时适配多个不同的接口到一个统一的接口:
// 不同的数据源
const mysqlDataSource = {
query(sql) {
return { rows: [{ id: 1, name: 'MySQL数据' }] };
}
};
const mongoDataSource = {
find(collection, query) {
return [{ _id: 1, name: 'MongoDB数据' }];
}
};
// 统一数据库接口适配器
class DatabaseAdapter {
constructor(db) {
this.db = db;
}
execute(query) {
if ('query' in this.db) {
// MySQL适配
const result = this.db.query(query);
return result.rows.map(row => ({ id: row.id, name: row.name }));
} else if ('find' in this.db) {
// MongoDB适配
return this.db.find('users', {}).map(doc => ({
id: doc._id,
name: doc.name
}));
}
throw new Error('Unsupported database');
}
}
// 使用适配器
const mysqlAdapter = new DatabaseAdapter(mysqlDataSource);
console.log(mysqlAdapter.execute('SELECT * FROM users'));
const mongoAdapter = new DatabaseAdapter(mongoDataSource);
console.log(mongoAdapter.execute('users.find({})'));
适配器模式与代理模式的区别
虽然适配器和代理都包装了另一个对象,但它们的目的是不同的:
- 适配器:改变被包装接口的形式,使其与其他代码兼容
- 代理:保持相同的接口,控制对被包装对象的访问
例如,一个缓存代理会保持与原对象相同的接口:
// 原始服务
class UserService {
getUser(id) {
console.log('从数据库获取用户');
return { id, name: '用户' + id };
}
}
// 缓存代理
class UserServiceProxy {
constructor(service) {
this.service = service;
this.cache = new Map();
}
getUser(id) {
if (this.cache.has(id)) {
console.log('从缓存获取用户');
return this.cache.get(id);
}
const user = this.service.getUser(id);
this.cache.set(id, user);
return user;
}
}
// 适配器则是改变接口
class UserServiceAdapter {
constructor(service) {
this.service = service;
}
fetchUserDetails(userId) {
const user = this.service.getUser(userId);
return {
userId: user.id,
userName: user.name,
registeredAt: new Date().toISOString()
};
}
}
实际项目中的适配器模式应用
在大型项目中,适配器模式可以帮助我们:
- 集成第三方库:当第三方库的API不符合我们的需求时
- 处理遗留代码:逐步重构时保持新旧代码兼容
- 统一接口:当系统需要支持多种实现时提供一致接口
例如,统一不同浏览器的存储接口:
// 统一存储接口
class StorageAdapter {
constructor(storage) {
this.storage = storage;
}
setItem(key, value) {
if ('setItem' in this.storage) {
this.storage.setItem(key, JSON.stringify(value));
} else if ('set' in this.storage) {
this.storage.set(key, value);
} else {
this.storage[key] = value;
}
}
getItem(key) {
if ('getItem' in this.storage) {
const value = this.storage.getItem(key);
return value ? JSON.parse(value) : null;
} else if ('get' in this.storage) {
return this.storage.get(key);
}
return this.storage[key] || null;
}
}
// 使用适配器
const localStorageAdapter = new StorageAdapter(localStorage);
const sessionStorageAdapter = new StorageAdapter(sessionStorage);
const customStorageAdapter = new StorageAdapter({});
// 现在可以使用统一的接口操作不同的存储
适配器模式的优缺点
优点:
- 让不兼容的接口能够一起工作
- 复用现有类而不需要修改其代码
- 符合单一职责原则,将接口转换逻辑分离
- 符合开闭原则,可以引入新适配器而不改变现有代码
缺点:
- 增加代码复杂度,有时直接修改接口可能更简单
- 过多使用适配器会使系统难以理解和维护
- 性能开销,每次调用都需要额外的处理
何时使用适配器模式
在以下情况下考虑使用适配器模式:
- 需要使用现有类,但其接口与其他代码不兼容
- 需要复用几个现有的子类,但这些子类缺少一些公共功能
- 需要为多个不同的接口提供统一的抽象
- 在系统升级或重构期间需要保持向后兼容
与其他模式的结合
适配器模式常与其他模式一起使用:
- 与工厂模式结合:创建适当的适配器实例
- 与装饰器模式结合:在适配的同时添加新功能
- 与外观模式结合:简化复杂子系统的接口
例如,结合工厂模式创建适配器:
class AdapterFactory {
static createAdapter(source) {
if (source.query) {
return new SQLAdapter(source);
} else if (source.find) {
return new NoSQLAdapter(source);
} else if (source.get) {
return new RESTAdapter(source);
}
throw new Error('Unsupported data source');
}
}
class SQLAdapter {
constructor(db) {
this.db = db;
}
fetch() {
return this.db.query('SELECT * FROM data');
}
}
// 使用工厂创建适配器
const db = { query: () => ['SQL data'] };
const adapter = AdapterFactory.createAdapter(db);
console.log(adapter.fetch());