设计模式-4(适配器模式与代理模式)
·
目录
3.适配器模式
1. 适配器模式的本质
适配器模式是结构型设计模式的一种,核心目标是解决「接口不兼容」问题:
将一个类/函数/接口的格式(输入参数、返回值、调用方式),转换为客户端(业务代码)期望的另一种格式,使得原本因格式不匹配而无法协作的组件,能够无缝衔接。
关键特点:客户端无需修改任何代码,只需通过适配器“中转”,即可使用新接口的能力。
2. 业务场景实践:新旧网络请求接口兼容
场景背景
- 旧接口:基于
XMLHttpRequest的Ajax函数(回调式),调用格式为:
Ajax(type, url, data, successCallback, failCallback); - 新接口:基于
fetch的HttpUtils类(Promise式),调用格式为:
await HttpUtils.get(url)或await HttpUtils.post(url, data); - 痛点:业务中大量使用旧接口调用,手动修改成本极高。
适配器接收旧接口的参数格式,内部调用新接口,再将新接口的Promise结果转换为旧接口的回调逻辑,实现“旧调用方式用新接口能力”:
// 适配器:接收旧接口参数,内部调用新接口
async function AjaxAdapter(type, url, data, success, failed) {
const method = type.toUpperCase();
try {
// 内部调用新接口(HttpUtils)
const result = method === 'GET'
? await HttpUtils.get(url)
: await HttpUtils.post(url, data);
// 适配旧接口的回调逻辑
result.statusCode === 1 ? success(result.data) : failed(result.statusCode);
} catch (error) {
failed(error.statusCode || '网络错误');
}
}
// 覆盖旧接口:业务代码无需修改,仍调用Ajax
function Ajax(...args) {
return AjaxAdapter(...args);
}
生产级应用:axios的跨环境适配器
axios的核心优势之一是「跨环境统一API」,其底层依赖适配器模式实现:
- 目标:让用户在浏览器和Node.js中,用同一套
axios.get/postAPI发起请求,无需关心底层差异; - 适配器逻辑:
-
- 通过
getDefaultAdapter函数判断执行环境:
- 通过
-
-
- 浏览器环境:使用
xhr适配器(基于XMLHttpRequest); - Node环境:使用
http适配器(基于Node内置http模块);
- 浏览器环境:使用
-
-
- 两个适配器对外暴露统一格式:入参为
config对象(含method、url、data等),出参为Promise;
- 两个适配器对外暴露统一格式:入参为
- 价值:用户无需学习两套API,适配器封装了所有底层差异,降低使用成本。
适配器模式的核心价值
- 解耦:客户端(业务代码)与新接口完全分离,无需感知新接口的存在;
- 复用:旧组件/代码无需改造,即可复用新接口的能力(如旧业务代码复用fetch/axios);
- 灵活:后续替换新接口(如从fetch切换到axios),只需修改适配器,无需改动业务代码。
3. 案例练习:新旧本地存储工具适配
场景描述
- 旧存储工具(OldStorage):仅支持localStorage,回调式调用,格式如下:
OldStorage.save(key, value, success, fail)(保存数据,成功/失败通过回调通知);
OldStorage.get(key, success, fail)(获取数据,结果通过回调返回); - 新存储工具(NewStorage):支持localStorage/sessionStorage,Promise式调用,格式如下:
NewStorage.save(storageType, key, value): Promise<void>(storageType可选'local'/'session');
NewStorage.get(storageType, key): Promise<string | null>; - 需求:不修改旧业务中对
OldStorage的调用代码,通过适配器兼容NewStorage,且默认使用localStorage。
// 1. 旧存储工具(模拟历史代码,不可修改)
const OldStorage = {
save: (key, value, success, fail) => {
try {
localStorage.setItem(key, value);
success?.('保存成功');
} catch (err) {
fail?.(err.message);
}
},
get: (key, success, fail) => {
try {
const value = localStorage.getItem(key);
success?.(value);
} catch (err) {
fail?.(err.message);
}
}
};
// 2. 新存储工具(新实现,支持多存储类型+Promise)
const NewStorage = {
// storageType: 'local' | 'session'
save: (storageType, key, value) => {
return new Promise((resolve, reject) => {
try {
const storage = storageType === 'session' ? sessionStorage : localStorage;
storage.setItem(key, value);
resolve('保存成功');
} catch (err) {
reject(err.message);
}
});
},
get: (storageType, key) => {
return new Promise((resolve, reject) => {
try {
const storage = storageType === 'session' ? sessionStorage : localStorage;
const value = storage.getItem(key);
resolve(value);
} catch (err) {
reject(err.message);
}
});
}
};
// 3. 适配器:适配OldStorage的调用格式到NewStorage
const OldStorageAdapter = {
// 适配save方法:旧参数(key, value, success, fail)→ 内部调用新save(默认local)
save: async (key, value, success, fail) => {
try {
const result = await NewStorage.save('local', key, value);
success?.(result); // 回调旧接口的成功函数
} catch (err) {
fail?.(err); // 回调旧接口的失败函数
}
},
// 适配get方法:同理
get: async (key, success, fail) => {
try {
const value = await NewStorage.get('local', key);
success?.(value);
} catch (err) {
fail?.(err);
}
}
};
// 4. 旧业务代码(无需修改,直接调用)
OldStorage.save = OldStorageAdapter.save;
OldStorage.get = OldStorageAdapter.get;
// 测试:旧调用方式仍可用,且实际用新存储工具
OldStorage.save('username', 'zhangsan',
(res) => console.log('旧调用成功:', res), // 输出:旧调用成功:保存成功
(err) => console.log('旧调用失败:', err)
);
OldStorage.get('username',
(val) => console.log('旧调用获取:', val), // 输出:旧调用获取:zhangsan
(err) => console.log('旧调用失败:', err)
);
// 1. 定义核心类型(TS核心:类型约束,减少错误)
type StorageType = 'local' | 'session'; // 限制存储类型,只能是两个值
type SuccessCallback<T = void> = (result: T) => void; // 成功回调类型(支持泛型)
type FailCallback = (error: string) => void; // 失败回调类型
// 2. 旧存储工具(类型化,明确参数/返回值)
const OldStorage = {
save: (
key: string,
value: string,
success?: SuccessCallback<string>,
fail?: FailCallback
) => {
try {
localStorage.setItem(key, value);
success?.('保存成功');
} catch (err) {
fail?.((err as Error).message); // TS类型断言,避免类型错误
}
},
get: (
key: string,
success?: SuccessCallback<string | null>,
fail?: FailCallback
) => {
try {
const value = localStorage.getItem(key);
success?.(value);
} catch (err) {
fail?.((err as Error).message);
}
}
};
// 3. 新存储工具(类型化,参数/返回值清晰)
const NewStorage = {
save: (
storageType: StorageType,
key: string,
value: string
): Promise<string> => { // 明确返回Promise<string>
return new Promise((resolve, reject) => {
try {
const storage = storageType === 'session' ? sessionStorage : localStorage;
storage.setItem(key, value);
resolve('保存成功');
} catch (err) {
reject((err as Error).message);
}
});
},
get: (
storageType: StorageType,
key: string
): Promise<string | null> => { // 明确返回Promise<string | null>
return new Promise((resolve, reject) => {
try {
const storage = storageType === 'session' ? sessionStorage : localStorage;
const value = storage.getItem(key);
resolve(value);
} catch (err) {
reject((err as Error).message);
}
});
}
};
// 4. 适配器(严格遵循旧接口类型,防止参数错误)
const OldStorageAdapter = {
save: async (
key: string,
value: string,
success?: SuccessCallback<string>,
fail?: FailCallback
): Promise<void> => { // 明确返回Promise<void>
try {
const result = await NewStorage.save('local', key, value);
success?.(result);
} catch (err) {
fail?.(err as string); // TS类型约束,确保err是string
}
},
get: async (
key: string,
success?: SuccessCallback<string | null>,
fail?: FailCallback
): Promise<void> => {
try {
const value = await NewStorage.get('local', key);
success?.(value);
} catch (err) {
fail?.(err as string);
}
}
};
// 5. 旧业务代码(类型安全,编辑器有提示)
OldStorage.save = OldStorageAdapter.save;
OldStorage.get = OldStorageAdapter.get;
// 测试:TS会自动提示参数类型,错误传参直接报错
OldStorage.save(
'username', // 必须是string,传number会报错
'zhangsan',
(res) => console.log('旧调用成功:', res), // res自动提示是string
(err) => console.log('旧调用失败:', err) // err自动提示是string
);
OldStorage.get(
'username',
(val) => console.log('旧调用获取:', val), // val自动提示是string | null
(err) => console.log('旧调用失败:', err)
);
TS实现的优越性体现
- 类型约束:通过
StorageType、SuccessCallback等类型,限制参数取值范围(如storageType只能是'local'/'session'),防止传错类型(如传'cookie'直接报错); - 代码提示:编辑器会自动提示函数参数类型、返回值类型(如
success回调的result是string),减少记忆成本; - 错误提前暴露:若旧业务代码误传参数(如
OldStorage.save(123, 'zhangsan', ...),123是number),TS编译阶段直接报错,避免运行时bug; - 可维护性:类型定义即文档,新同事接手时,通过类型就能明确函数的输入输出,无需通读逻辑。
4.代理模式
1. 代理模式核心思想
- 本质:通过「代理器」实现间接访问,避免对象 A 直接操作对象 B
- 核心目标:解耦职责、提升性能、增强权限控制、优化代码结构
- 通用套路:访问链路为「A → 代理器 → B」,代理器承担中间调度、逻辑增强或权限校验工作
2. 四种常见代理类型及实践
1. 事件代理(性能优化型)
- 适用场景:父元素下有多个子元素需绑定相同事件(如列表项点击、导航链接跳转)
- 实现原理:利用 DOM 事件的「冒泡机制」,仅给父元素绑定 1 次事件,通过
e.target识别触发事件的子元素 - 核心优势:减少事件监听数量(避免 N 个子元素绑定 N 个事件),尤其适合子元素动态增减的场景(无需重新绑定事件)
// 父元素(仅绑定1次事件)
const father = document.getElementById('father');
father.addEventListener('click', (e) => {
// 识别目标子元素(仅处理 <a> 标签的点击)
if (e.target.tagName.toLowerCase() === 'a') {
e.preventDefault();
alert(`点击了:${e.target.innerText}`);
}
});
2. 虚拟代理(职责解耦型)
- 适用场景:需分离「核心功能」与「辅助功能」(如图片预加载、异步请求封装)
- 实现原理:用「虚拟对象」(如内存中的 Image 实例、异步请求封装层)承担辅助逻辑,真实对象仅负责核心职责(如 DOM 渲染、数据处理)
- 核心优势:符合「单一职责原则」,避免一个类同时处理多个无关逻辑(如图片类既管 DOM 展示又管加载)
// 真实对象:仅处理 DOM 图片设置(核心职责)
class RealImage {
constructor(imgNode) { this.imgNode = imgNode; }
setSrc(url) { this.imgNode.src = url; }
}
// 虚拟代理:处理图片预加载(辅助逻辑)
class ProxyImage {
static LOADING_URL = 'https://placeholder.com/loading'; // 占位图
constructor(realImage) { this.realImage = realImage; }
setSrc(targetUrl) {
// 步骤1:先显示占位图
this.realImage.setSrc(ProxyImage.LOADING_URL);
// 步骤2:用虚拟 Image 加载真实图片
const virtualImg = new Image();
virtualImg.onload = () => {
// 步骤3:加载完成后更新真实 DOM 图片
this.realImage.setSrc(targetUrl);
};
virtualImg.src = targetUrl; // 触发加载
}
}
3. 缓存代理(效率提升型)
- 适用场景:计算量大、重复计算频繁的场景(如多参数求和、数据格式化、复杂列表筛选)
- 实现原理:用「缓存池」(对象/Map)存储「入参 → 结果」的映射,重复入参直接返回缓存结果,避免二次计算
- 核心优势:用空间换时间,显著减少重复计算的性能开销
// 核心计算函数
const sum = (...args) => {
console.log('执行新计算'); // 仅首次触发
return args.reduce((total, cur) => total + cur, 0);
};
// 缓存代理
const proxySum = (() => {
const cache = {}; // 缓存池:key=入参字符串,value=计算结果
return (...args) => {
// 生成入参唯一标识(避免 [1,2] 和 [2,1] 被误判为不同)
const argsKey = args.sort((a, b) => a - b).join(',');
// 缓存命中则返回,否则计算并存储
return cache[argsKey] ?? (cache[argsKey] = sum(...args));
};
})();
// 测试:重复入参仅计算1次
console.log(proxySum(1, 2, 3)); // 执行新计算 → 6
console.log(proxySum(3, 2, 1)); // 从缓存获取 → 6
4. 保护代理(权限控制型)
- 适用场景:需限制对目标对象的访问权限(如用户信息脱敏、VIP 功能拦截、敏感操作校验)
- 实现原理:在代理器中添加权限校验逻辑,仅当满足条件(如登录、VIP 身份)时,才允许访问目标对象的属性/方法
- 核心优势:保障目标对象安全,避免非法访问或误操作
// 目标对象(敏感用户信息)
const user = { name: '张三', phone: '138xxxx8888', vipLevel: 2 };
// 保护代理(ES6 Proxy 实现)
const userProxy = new Proxy(user, {
get(target, key) {
// 未登录时禁止访问手机号
if (!isLogin() && key === 'phone') {
throw new Error('请登录后查看联系方式');
}
// 非VIP用户隐藏手机号中间4位
if (target.vipLevel < 1 && key === 'phone') {
return target.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
}
return target[key];
}
});
// 模拟登录状态
let isLogin = () => false;
console.log(userProxy.phone); // 报错:请登录后查看联系方式
isLogin = () => true;
console.log(userProxy.phone); // 138****8888(非VIP脱敏)
代理模式核心总结
- 共性:所有代理类型均通过「中间层(代理器)」实现间接访问,核心是「分离访问与实现」
- 差异:代理器的目标不同(性能优化、职责解耦、效率提升、权限控制)
- 适用场景:当需要增强控制、拆分复杂逻辑、优化性能时,优先考虑代理模式
3. 案例练习:商品库存查询代理
场景描述
实现电商商品库存查询功能:
- 从接口查询商品库存(模拟异步请求,每次请求耗时 500ms)
- 相同商品 ID 的查询结果缓存 10 秒(避免重复请求,缓存代理)
- 仅管理员可查看「真实库存」,普通用户仅显示「有货/无货」(权限控制,保护代理)
// 1. 模拟用户状态与权限(实际项目从全局状态/后端获取)
const user = { role: 'user' }; // role: 'user'(普通用户)/ 'admin'(管理员)
// 2. 模拟异步库存查询接口(耗时500ms)
const fetchStock = (goodsId) => {
return new Promise((resolve) => {
setTimeout(() => {
// 模拟不同商品的真实库存
const stockMap = { 101: 23, 102: 0, 103: 56 };
resolve(stockMap[goodsId] ?? 0); // 无对应商品返回0
}, 500);
});
};
// 3. 库存查询代理(整合缓存代理 + 保护代理)
const stockProxy = (() => {
const cache = new Map(); // 缓存池:key=goodsId,value={stock, expireTime}
return async (goodsId) => {
const now = Date.now();
const cacheKey = goodsId.toString();
// 1. 缓存代理:检查缓存是否有效(未过期)
if (cache.has(cacheKey)) {
const { stock, expireTime } = cache.get(cacheKey);
if (now < expireTime) {
console.log(`[缓存命中] 商品${goodsId}的库存`);
return formatStock(stock); // 格式化后返回
}
cache.delete(cacheKey); // 缓存过期,删除
}
// 2. 调用真实接口查询库存
console.log(`[接口请求] 商品${goodsId}的库存`);
const realStock = await fetchStock(goodsId);
// 3. 存储缓存(有效期10秒)
cache.set(cacheKey, {
stock: realStock,
expireTime: now + 10 * 1000 // 10秒后过期
});
// 4. 保护代理:按用户权限格式化库存
return formatStock(realStock);
};
// 辅助函数:按权限格式化库存
function formatStock(realStock) {
if (user.role === 'admin') {
return `真实库存:${realStock}件`;
}
// 普通用户仅显示“有货/无货”
return realStock > 0 ? '库存状态:有货' : '库存状态:无货';
}
})();
// 测试
(async () => {
// 普通用户查询(首次请求)
console.log(await stockProxy(101)); // [接口请求] → 库存状态:有货
// 普通用户重复查询(缓存命中)
console.log(await stockProxy(101)); // [缓存命中] → 库存状态:有货
// 切换为管理员
user.role = 'admin';
// 管理员查询(缓存未过期)
console.log(await stockProxy(101)); // [缓存命中] → 真实库存:23件
// 等待10秒后查询(缓存过期)
setTimeout(async () => {
console.log(await stockProxy(101)); // [接口请求] → 真实库存:23件
}, 10 * 1000);
})();
// 1. 定义接口:明确类型约束,避免参数错误
interface User {
role: 'user' | 'admin'; // 仅允许两种角色,避免非法值
}
interface CacheItem {
stock: number; // 库存数量(数字类型)
expireTime: number; // 过期时间(时间戳)
}
// 2. 模拟用户状态(类型明确,避免role传错值)
const user: User = { role: 'user' };
// 3. 模拟异步库存查询接口(参数和返回值类型明确)
const fetchStock = async (goodsId: number): Promise<number> => {
return new Promise((resolve) => {
setTimeout(() => {
const stockMap: Record<number, number> = { 101: 23, 102: 0, 103: 56 };
resolve(stockMap[goodsId] ?? 0);
}, 500);
});
};
// 4. 库存查询代理(类型约束确保逻辑安全)
const stockProxy = (() => {
const cache = new Map<string, CacheItem>(); // 缓存池类型明确
// 代理函数:入参为number(商品ID),返回值为Promise<string>(格式化结果)
const proxyFn = async (goodsId: number): Promise<string> => {
const now = Date.now();
const cacheKey = goodsId.toString();
// 1. 缓存校验(TS确保cache的get/set方法参数类型正确)
if (cache.has(cacheKey)) {
const { stock, expireTime } = cache.get(cacheKey)!; // 非null断言(has已判断存在)
if (now < expireTime) {
console.log(`[缓存命中] 商品${goodsId}的库存`);
return formatStock(stock);
}
cache.delete(cacheKey);
}
// 2. 接口请求(TS确保await的是Promise类型)
console.log(`[接口请求] 商品${goodsId}的库存`);
const realStock = await fetchStock(goodsId);
// 3. 存储缓存(TS确保CacheItem结构正确)
cache.set(cacheKey, {
stock: realStock,
expireTime: now + 10 * 1000
});
// 4. 权限格式化
return formatStock(realStock);
};
// 辅助函数:按权限格式化(TS确保入参是number,返回值是string)
const formatStock = (realStock: number): string => {
if (user.role === 'admin') {
return `真实库存:${realStock}件`;
}
return realStock > 0 ? '库存状态:有货' : '库存状态:无货';
};
return proxyFn;
})();
// 测试(TS确保调用方式正确,避免传非number类型的goodsId)
(async () => {
console.log(await stockProxy(101)); // 普通用户:有货(接口请求)
console.log(await stockProxy(101)); // 普通用户:有货(缓存命中)
user.role = 'admin';
console.log(await stockProxy(101)); // 管理员:23件(缓存命中)
// 10秒后缓存过期
setTimeout(async () => {
console.log(await stockProxy(101)); // 管理员:23件(接口请求)
}, 10 * 1000);
// 错误示例:TS编译时会报错(goodsId必须是number,不能是string)
// console.log(await stockProxy('101'));
})();
TS 实现的优越性体现
- 类型约束:通过
interface定义User、CacheItem,限制user.role仅能为user或admin,避免传入非法角色(如guest)导致的逻辑错误。 - 参数安全:函数
fetchStock和stockProxy的参数类型明确为number(商品 ID),TS 编译时会拦截非数字类型的调用(如stockProxy('101')直接报错)。 - 开发体验:IDE 可提供自动补全(如输入
user.时提示role属性)和实时错误提示(如少传参数、类型不匹配时立即高亮),降低调试成本。 - 代码可读性:接口定义清晰描述了数据结构(如
CacheItem包含stock和expireTime),其他开发者无需阅读业务逻辑即可理解参数含义,提升协作效率。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)