设计模式:适配器模式

设计模式是通用的、可复用的代码设计方案,也可以说是针对某类问题的解决方案,因此,掌握好设计模式,可以帮助我们编写更健壮的代码。

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函数即可。

除了功能适配,数据适配在开发中也很常见,比如设定数据规范,以便于在不同系统和应用程序之间进行数据交互和处理。

总结

通过以上的探讨我们可以发现,适配器模式在实际生活和编程中的应用其实是很普遍和广泛的,为了提高兼容性而增加适配器角色也是一个很常见的行为,适配器的主要功能就是帮助我们抹平差异,也就是在适配器的内部会去根据差异来做一些处理,而这些处理对用户来说是透明的,不需要了解的。

你可能感兴趣的:(设计模式:适配器模式)