设计模式是通用的、可复用的代码设计方案,也可以说是针对某类问题的解决方案,因此,掌握好设计模式,可以帮助我们编写更健壮的代码。
wiki中将设计模式分为四类,分别是:
- 创建模式(creational patterns)
- 结构模式(structural patterns)
- 行为模式(behavioral patterns)
- 并发模式(concurrency patterns)
适配器模式属于其中的结构型模式,结构型——从名称上就可以看出——与结构有关,应用这类模式不会影响对象的行为,但是会影响代码结构。那么适配器模式究竟是怎样的一种解决方案,适合什么场景呢,接下来我们就来探究一下。
适配
首先我们来先扣个字眼,适配是什么意思呢?我直接问ChatGPT,得到了以下的回答:
"适配"在计算机领域通常指的是使软件、程序或网站能够在不同的设备、平台或环境下正常工作和显示的过程。适配的主要目的是确保用户能够在各种设备上获得一致的用户体验,而不受设备类型、屏幕尺寸、操作系统或浏览器等因素的影响。
简单粗暴一点理解就是指的兼容性。
所以应用适配器模式的目的简单来理解,就是提高兼容性。
这类生活场景非常常见,比如公司给员工配了一台显示器,但是外接屏幕自带连接线的接头与电脑已有的接口不适配,两者无法连接上,这种情况是很常见的,随着技术升级、数码设备不断的升级换代,导致市面上存在很多类型的接口,于是转接头就应运而生了——转接头就是适配器模式的一种具象应用。
编程
适配在编程中也是一个常见需求。就比如WIKI中的描述:
It is often used to make existing classes work with others without modifying their source code.
翻译过来的意思是:它(适配器模式)通常用于在不修改源代码的情况下使现有类与其他类协同工作。
很多开发小伙伴在现实工作中对这点应该都有所体会,在程序员的工作中很多时候都需要去维护已有项目,迭代新的需求,然后就可能碰到这类场景。
比如老代码中有些类或者对象的某个方法,在项目中的其他地方有调用,但是某天,突然来了一个需求,增加一个功能模块,然后这个模块需要与这些类或者对象交互,实现与这个方法类似但存在细微不同点的功能。
针对这个需求,如果不应用适配器模式,我们可以有以下两种做法:
第一种,如果这个方法调用的地方比较少,这里是说如果,那我们可以简单粗暴地直接把这个方法改了,测试环节就稍微麻烦点,在测试新功能模块的同时还需要回归测试原本调用这个方法的地方。
第二种,给对象增加一个新的方法提供给新模块调用,当然这个新方法中的很多代码会与老方法中的代码重复。
很显然,这两种做法都不够好,第一种需要回归测试,而且很容易有遗漏,甚至可能引起原因不明的bug;第二种则可能使项目中存在很多冗余代码,而且还可能影响后期维护,比如修改某段逻辑就要修改两个方法的代码,如果是其他人来接手维护,很可能根本不知道是这样的情况。
适配器模式就可以应用于这类场景,它主要帮助我们解决以下问题:
- 代码复用
- 让接口不兼容的类协同工作
模式描述
适配器模式描述了它是如何帮助我们解决上述问题的:
- 定义一个单独的适配器类,将一个类(待适配)的(不兼容)接口转换成客户端需要的另一个接口(target)。
- 通过适配器来处理(重用)不具备所需接口的类。
也就是说,应用适配器模式主要做的事情,就是在代码中增加一个适配器的角色。
前端应用
JavaScript作为一种面向对象编程语言,当然也可以应用适配器模式。我们来看下面的一个例子:
Web端在以前使用Ajax技术处理异步的时候,都是通过XHR对象,但是随着Promise的推出,出现了更简洁的fetch方法,为了更方便地处理异步,现在某负责人准备在新项目中应用fetch方法,新项目从老项目中拷贝了基础文件,其中包括了Ajax代码,为了使项目成员快速熟悉,Ajax方法最好在用法上保持一致。
假设以下是原本的Ajax代码,是基于XMLHttpRequest对象进行封装的:
function Ajax(method, url, {query, params, headers, successCallback }) {
// 1. 创建对象
const xhr = new XMLHttpRequest();
// 2. 初始化 设置请求类型和url
method = method.toUpperCase();
let queryString = '?';
if (query && query instanceof Object) {
for (const key in query) {
queryString += `${key}=${query[key]}&`
}
url += queryString.substring(0, queryString.length - 1);
}
xhr.open(method, url);
// 3. 设置请求头
for (const key in headers) {
xhr.setRequestHeader(key, headers[key]);
}
// 4. 发送请求
let paramString = '';
if (params && params instanceof Object) {
for (const key in params) {
paramString += `${key}=${params[key]}&`
}
paramString = paramString.substring(0, paramString.length - 1);
}
xhr.send(paramString);
// 5. 事件绑定 处理服务端返回的结果
// on 当...的时候
// readystate 0-初始化创建的时候 1-open的时候 2-send的时候 3-服务端部分返回的时候 4-服务端返回全部的时候
// change 改变
xhr.onreadystatechange = function () {
// 判断 服务端返回了所有的结果
if (xhr.readyState === 4) {
// 判断响应状态码 200 404 403 401 500
// 2xx 成功
if (xhr.status >= 200 && xhr.status < 300) {
let result = {
status: xhr.status, // 状态码
statusText: xhr.statusText, // 状态字符串
responseHeaders: xhr.getAllResponseHeaders(), // 所有响应头
response: xhr.response // 响应体
}
successCallback(result);
}
}
}
}
为了使项目中熟悉XHR调用方式的成员和熟悉fetch的成员能统一调用Ajax方法,我们可以使用适配器模式来对代码进行改造。
首先创建一个适配器对象,在JavaScript中我们知道,函数也是对象,所以我们定义如下函数:
async function AjaxAdapter(method, url, {query, params, headers, successCallback }) {
// ...
}
这个函数的入参与原本的Ajax函数保持一致。在这里声明async
异步函数是因为fetch方法的返回值是Promise类型。
然后我们修改Ajax
函数,去调用这个适配器函数:
async function Ajax(method, url, {query, params, headers, successCallback }) {
return AjaxAdapter(method, url, {query, params, headers, successCallback });
}
我们在适配器函数中去处理不同的使用方式,比如如果调用者传递了successCallback
这个回调函数,说明他是旧方式的使用者,那么就在适配器函数中将异步返回的结果通过successCallback
进行传递,否则就不对异步结果做处理,由调用者自行处理异步函数的结果。
这样无论是XHR的使用者还是fetch的使用者,都能同样使用Ajax
函数获取异步结果,而不会感知到其中的不同,XHR的使用者就不必一定要去学习fetch的使用,只要像以前一样使用Ajax
函数即可。
除了功能适配,数据适配在开发中也很常见,比如设定数据规范,以便于在不同系统和应用程序之间进行数据交互和处理。
总结
通过以上的探讨我们可以发现,适配器模式在实际生活和编程中的应用其实是很普遍和广泛的,为了提高兼容性而增加适配器角色也是一个很常见的行为,适配器的主要功能就是帮助我们抹平差异,也就是在适配器的内部会去根据差异来做一些处理,而这些处理对用户来说是透明的,不需要了解的。