设计模式结构型之适配器模式

实验介绍

本节实验为大家带来了适配器模式,适配器模式是作为两个不兼容的接口之间的桥梁,可以将变化都封装于它本身,提供简单统一的接口使用。从一个有趣的例子开始为大家逐步的讲解适配器,帮助大家学习其基本的概念。随后为大家介绍了适配器在前端中的真实应用,加深对适配器的认识。最后为大家简单区分了装饰器模式与适配器模式,可以进行对照学习。

知识点

  • 适配器模式介绍
  • 适配器的应用
  • 装饰器模式与适配器模式的区别
  • 实验挑战

适配器模式介绍

适配器模式是作为两个不兼容的接口之间的桥梁,从名称上应该就能够明白,适配,只有不匹配不兼容的时候才需要适配。而适配器模式也是实现适配器的思想与过程。

举一个真实的例子,在几年前手机中用的内存卡是没有办法直接插入电脑中的,如果想要下载歌曲或者电影,就需要一种叫读卡器的物品,你可以将内存卡插入读卡器,再将读卡器插入笔记本,这样就能实现通过笔记本来读取内存卡,从而进行数据间的交换。

图片描述

在这里,读卡器担任的正式适配器这一角色。

当然不止这一个例子,例如:

  1. 苹果手机从 7 版本开始,它的耳机接口和充电接口共用,并且都是方形孔。如果你想使用以前的圆形插孔耳机,就只能通过一个转换器来实现,这里的转换器就是一个适配器。
  2. 美国电压采用 110V,而中国采用 220 V,此时就需要一个适配器将 110 V 转换成 220 V,否则当你身处国外时是无法对国内电子设备进行充电的。

从上面的这些例子中可以看到,适配器的应用充斥着我们生活的方方面面。

知道了适配器的定义,现在让我们看看如何用代码实现一个适配器吧。

适配器

首先新建 index.html 文件,及 player.js 文件,并引入。

考虑这样一个例子:

在十几年前,技术不如现在发达的时代,一开始社会上流行的最多的 MP3 这种音乐格式,一位罗老板看准了这个机会,于是想通过卖支持播放 MP3 格式的播放器挣钱,因此他找到一个程序员张三,让他实现一个类用于播放 MP3 格式的文件。

张三一听,心想这还不简单,于是很快就实现了 AudioPlayer 类:

// player.js

class AudioPlayer {
  // ...

  play(fileName) {
    console.log("播放 MP3 音乐:", fileName);
  }
}

来看看这个 AudioPlayer 是否能满足罗老板的需求呢:

// player.js

// 构建一个 audioPlayer 实例
const audioPlayer = new AudioPlayer();

// 调用 play,播放 MP3
audioPlayer.play("千里之外"); // 播放 MP3 音乐: 千里之外

功能实现之后,罗老板因为这个技术赚的盆满钵满的,不过好景不长,随着技术的不断革新,市场上又逐渐出现了 VLC 和 MP4 这两种格式的文件。为了加强自己产品的竞争力。他赶紧叫来张三,告诉他一定要赶紧完成这两个功能。

张三一想,这好办啊,于是飞快敲起了键盘,很快就实现了 VideoVclPlayerVideoMp4Player 这两个类:

// player.js

// VLC 播放器
class VideoVclPlayer {
  // ...

  playVLC(fileName) {
    console.log("播放 VLC 视频:", fileName);
  }
}

// MP4 播放器
class VideoMp4Player {
  // ...

  playMp4(fileName) {
    console.log("播放 MP4 视频:", fileName);
  }
}

来看看是否满足需要:

// player.js

// 构建一个 audioPlayer 实例
const vlc = new VideoVclPlayer();

// 调用 playVLC,播放 VLC
vlc.playVLC("哪吒闹海"); // 播放 VLC 视频: 哪吒闹海

// 构建一个 audioPlayer 实例
const mp4 = new VideoMp4Player();

// 调用 playMp4,播放 MP4
mp4.playMp4("大闹天宫"); // 播放 MP4 视频: 大闹天宫

看到这个结果,张三非常满意的点了点头,就在他得意满满的时候,罗老板却一盆冷水泼了下来,他要求张三要让我们原来播放 MP3 的产品要兼容的播放新增的两种格式,还必须保证原来播放 MP3 的功能不受影响。

张三此时慌了,这该如何去实现呢?就在他一筹莫展的时候,无意间看到了插在电脑上的读卡器,顿时脑子里灵光一闪,他完全可以通过适配器模式的思想去实现啊。

于是他决定先实现一个用于播放视频的适配器 MediaAdapter 用于实现适配功能:

// player.js

// 视频的适配器
class MediaAdapter {
  // ...

  // 通过类型字段 type 判断
  play(type, fileName) {
    if (type === "VLC") {
      // 构建 vlc 实例
      const vlc = new VideoVclPlayer();
      vlc.playVLC(fileName);
    } else if (type === "MP4") {
      // 构建 mp4 实例
      const mp4 = new VideoMp4Player();
      mp4.playMp4(fileName);
    }
  }
}

通过这个适配器,就可以实现识别视频格式的一端。

但是除了适配视频的一端,我们还需要对原产品的一端进行适配的修改,就像这样:

// player.js

// 这里需要注释掉原 AudioPlayer 类
class AudioPlayer extends MediaAdapter {
  constructor() {
    super(); // 这是 ES6 中 class 的内容
  }

  play(type, fileName) {
    if (type === "MP3") {
      console.log("播放 MP3 音乐:", fileName);
    } else if (type === "VLC" || type === "MP4") {
      super.play(type, fileName); // 调用继承类的方法
    } else {
      console.log("不支持该格式!");
    }
  }
}

现在让我们尝试使用一下改造后的 AudioPlayer 播放器能否满足老板的需求:

// player.js

// 通过原产品构建 audioPlayer 播放器实例
const audioPlayer = new AudioPlayer();

// 分别调用各种格式的内容
audioPlayer.play("MP3", "千里之外"); // 播放 MP3 音乐: 千里之外
audioPlayer.play("VLC", "哪吒闹海"); // 播放 VLC 视频: 哪吒闹海
audioPlayer.play("MP4", "大闹天宫"); // 播放 MP4 视频: 大闹天宫
audioPlayer.play("JPG", ""); // 不支持该格式!

从输出来看,经过改造后的产品完全能够满足老板的需要,张三也因此受到了老板的表彰。

经过上面的例子,相信大家对适配器能有个更加直观的了解,上面的视频适配器正是连通原产品和新的视频播放功能的“桥梁”。

到这里,有些同学可能会有这样一个疑问:为什么需要单独新增 MediaAdapter 这样一个适配器呢?完全可以直接在 AudioPlayer 中直接添加啊。

这个问题很好回答,首先请看这一段代码:

if (type === "MP3") {
  console.log("播放 MP3 音乐:", fileName);
}

这是我们新的 AudioPlayer 类中的 play 方法中的一段,在这里你可能觉得这样一句输出语句完全没有必要单独包裹在一个 if 判断中,然而这里我们知识为了展示,实际这里的处理逻辑会非常的复杂,涉及到各种声音的处理。

第二点,我们单独隔离出去 MP3 的部分,是因为不能保证以后会有新的播放格式加入进来,通过适配器我们可以很方便的再适配器中添加相关的逻辑,而完全不会影响到原产品播放 MP3 能力。

请再一次明确老板的需求:要求 AudioPlayer 能兼容其他格式,而不是想为它中添加新的功能.

因此使用适配器的方式处理是符合要求的。

适配器的概念其实是比较简单的,通过这个例子相应大家一定能很容易的理解适配器和适配器模式。

以上是为了帮助大家理解适配器而举的例子,作为一名前端 er,我们也需要了解适配器在前端中的应用,因此下一小节会为大家介绍在前端中的适配器应用场景。

适配器的应用

axios 相信很多同学都不陌生,它是我们在前端中用于网络请求的一项技术。通过 axios 可轻松地发起各种姿势的网络请求,而不用去关心底层的实现细节。

这里之所以提到它,也正是因其用到了适配器模式,同时它的兼容方案也值得我们学习和借鉴。

通常在 axios 的使用中,较常用的三种 api 为:

// get 请求
axios
  .get("/xxx?name=zhangsan")
  .then(function (response) {
    // 请求成功时,调用成功的回调函数
    console.log(response);
  })
  .catch(function (error) {
    // 请求异常时,调用异常的回调函数
    console.log(error);
  })
  .then(function () {
    // 继续进行链式处理
  });

// post 请求
axios
  .post("/xxx", {
    name: "zhangsan",
  })
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });

// 通用请求,通过 method 字段判断请求类型
axios({
  method: "post",
  url: "/xxx/xxxx",
  data: {
    name: "zhangsan",
  },
});

axios 厉害的点在于,它不仅可以在浏览器环境下使用,在 node 环境中它仍然有效,用户完全不用担心二者因为环境不同而对 axios 的使用进行区别。

axios 靠着对适配器模式的应用,消除了不同环境下调用它的 api 的差异,用户仅仅需要考虑业务,而不用分心去主动适配不同环境下的网络请求。这一点可以放心的交给 axios 去帮助我们解决。

接下来,就让我们看看在 axios 内部,是如何进行适配的。

dispatchRequest 方法是 axios 中负责派发请求的核心方法:

function dispatchRequest(config) {
  throwIfCancellationRequested(config);

  // ...

  // api 类型
  utils.forEach(
    ["delete", "get", "head", "post", "put", "patch", "common"],
    function cleanHeaderConfig(method) {
      delete config.headers[method];
    }
  );

  // 判断是自定义适配器还是默认适配器
  var adapter = config.adapter || defaults.adapter;

  // 执行适配器
  return adapter(config).then(
    function onAdapterResolution(response) {
      // 请求成功的回调
      throwIfCancellationRequested(config);
      // ...

      return response;
    },
    function onAdapterRejection(reason) {
      // 请求失败的回调
      if (!isCancel(reason)) {
        throwIfCancellationRequested(config);
        // ...
      }

      return Promise.reject(reason);
    }
  );
}

在上面这段代码中,有这么一句,是为了区分自定义配置器和默认配置器的:

var adapter = config.adapter || defaults.adapter;

通常情况下,我们都使用的默认适配器,那么这里的默认适配器又是怎么样的:

function getDefaultAdapter() {
  var adapter;
  if (
    typeof process !== "undefined" &&
    Object.prototype.toString.call(process) === "[object process]"
  ) {
    // node 环境,调用node专属的http适配器
    adapter = require("./adapters/http");
  } else if (typeof XMLHttpRequest !== "undefined") {
    // 非 node 环境,调用基于xhr的适配器
    adapter = require("./adapters/xhr");
  }
  return adapter;
}

可以看到,默认情况下 axios 内部是可以适配不同的环境的。对于用户而言,这些都不需要考虑,交给 axios 就行。

把所有的工作都交给自己,而给用户提供最简单的使用方式,这就是适配器的伟大之处。

这里我们提到 axios 只是为了让大家了解适配器在前端中的真实应用,而 axios 的内容是不仅这一点的,如果想了解更多内容可以参考:

  1. axios 中文文档
  2. axios 源码

小结

下面对适配器做一个简单的小结:

  1. 适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁。
  2. 灵活的修改一个正常运行的系统的接口,该接口已经在项目大量调用,这时应该考虑使用适配器模式。
  3. 这种模式涉及到一个单一的类,该类负责加入独立的或不兼容的接口功能。就像我们上面提到的 MediaAdapter 一样,在 MediaAdapter 中添加了播放视频的能力,把 MediaAdapter 作为兼容的接口。

装饰器模式与适配器模式的区别

经过前面的学习,大家一定对适配器有了清晰的认识,不过刚开始接触设计模式的同学,有时候会把适配器模式和装饰器搞混,在这里将二者做出一个区分:

  1. 装饰器模式,在不修改原方法的基础上,再添加新的功能。关键点在于,原本的方法是不需要改动的。
  2. 适配器模式,其主要目的是兼容,即提供适配器来进行兼容,关键点在于,要保证原有的功能的情况下去兼容执行其他的功能。而并非为了添加新的功能。

这是二者的区别,希望同学们在完全了解两种模式的情况下来揣摩这里的区分描述。

实验挑战

现在市场是又出现了一种 mpeg 视频格式,请同学们在上面张三实现的适配器的基础上增加对 mpeg 格式的兼容。

最后模拟输出效果是这样的:

audioPlayer.play("MPEG", "MPEG 新格式视频"); // 播放 MPEG 视频: MPEG 新格式视频

答案代码放在实验最后课程的源码包里,大家可以自行下载。

实验总结

在实验的一开始,为大家通过读卡器等小例子帮助大家建立直观的认识,随后用罗老板和张三之间生产播放器的例子展开对代码的逐步实现,一层一层的为大家讲解适配器出现的原因和实现的过程,相信在这一过程中,大家能对适配器有一个较深的理解。随后为大家介绍了 axios 这一前端中应用极广的网络请求技术。旨在帮助大家了解适配器在前端中的真实应用。最后希望大家能认真完成实验挑战,检验自己的理解程度。

本节实验源码压缩包下载链接:适配器模式源码

posted @   雨晨*  阅读(23)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
点击右上角即可分享
微信分享提示