首页 > 其他分享 >设计模式结构型之适配器模式

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

时间:2024-02-23 18:15:02浏览次数:29  
标签:axios 适配器 模式 播放 MP3 fileName 设计模式 结构型

实验介绍

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

知识点

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

适配器模式介绍

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

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

图片描述

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

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

  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 这一前端中应用极广的网络请求技术。旨在帮助大家了解适配器在前端中的真实应用。最后希望大家能认真完成实验挑战,检验自己的理解程度。

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

标签:axios,适配器,模式,播放,MP3,fileName,设计模式,结构型
From: https://www.cnblogs.com/xzemt/p/18030131

相关文章

  • 设计模式创建型之原型模式
    实验介绍本实验主要为大家介绍了前端中原型模式,为了加深大家对原型的了解,实验中花费大量篇幅讲解了原型及原型的概念,并配上了相关的例子以帮助大家学习。随后我们对class进行了简单的介绍,它可以被简单的认为是语法糖。最后,为了帮助大家理解原型中的克隆,实验也对浅拷贝与深拷贝......
  • 设计模式创建型之工厂模式
    基本概念在给出工厂模式的定义之前,不妨先来了解一下工厂的概念。通过百度百科查到的所谓工厂的定义:是一类用以生产货物的大型工业建筑物,即我们为工厂输送原料,经过工厂对原料进行处理加工之后会输出产物。例如下面这样一个例子:张三是一名大学生,毕业后为了上班方便就考虑买一台......
  • 设计模式创建型之单例模式
    实验介绍本实验主要介绍了设计模式中的单例模式,在前端领域中,有很多地方都运用到了单例模式的思维,例如目前的主流前端框架中所用到的Redux和Vuex。实验首先通过一个小例子为大家展示了单例模式的实现原理,随后通过完成一个自定义的Storage存储器来帮助大家加深对单例模式的理......
  • 3分钟看懂设计模式02:观察者模式
    一、什么是观察者模式观察者模式又叫做发布-订阅模式或者源-监视器模式。结合它的各种别名大概就可以明白这种模式是做什么的。其实就是观察与被观察,一个对象(被观察者)的状态改变会被通知到观察者,并根据通知产生各自的不同的行为。以下为《设计模式的艺术》中给出的定义:观察者......
  • [设计模式]创建型模式-抽象工厂模式
    简介抽象工厂模式是一种创建型设计模式,它提供了一种创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。抽象工厂模式将一组具有共同主题的单个工厂封装起来,它提供接口用于创建相关或依赖对象的家族,而不需要指定具体的类。抽象工厂模式包含以下几个核心角色:抽象工厂(A......
  • 3分钟看懂设计模式01:策略模式
    一、什么是策略模式定义一些列算法类,将每一个算法封装起来,并让它们可以互相替换。策略模式让算法独立于使用它的客户而变化,是一种对象行为型模式。以上是策略模式的一般定义,属于是课本内容。在没有真正理解策略模式之前并不需要对此定义下过多功夫,读一遍直接进入下一章节。二......
  • 设计模式浅析(六) ·命令模式
    设计模式浅析(六)·命令模式日常叨逼叨java设计模式浅析,如果觉得对你有帮助,记得一键三连,谢谢各位观众老爷......
  • 我们在SqlSugar开发框架中,用到的一些设计模式
    我们在《SqlSugar开发框架》中,有时候都会根据一些需要引入一些设计模式,主要的目的是为了解决问题提供便利和代码重用等目的。而不是为用而用,我们的目的是解决问题,并在一定的场景下以水到渠成的方式处理。不过引入任何的设计模式,都会增加一定的学习难度,除非是自己本身领会比较好了,......
  • [设计模式]创建型模式-简单工厂模式
    简介简单工厂模式又称为静态工厂模式,属于创建型模式,但不属于GOF23设计模式。由一个工厂对象决定创建出哪一种产品类的实例。简单工厂模式的实质是由一个工厂类根据传入的参数,动态决定应该创建哪一个产品类。简单工厂适用场景:工厂类负责创建的对象比较少;客户端只需要知道传入工厂......
  • 设计模式构想RPG游戏开发
    对RPG本身特点的分析:想要分析RPG游戏中用了什么设计模式,首先要对RPG本身有一定的了解。传统RPG游戏指的是主人公按照一定的主线剧情去往特定的地点完成特定的任务,游戏的主要部分在于对游戏各个场景的刻画,场景的数量非常之多,怪物虽然也有不同的种类但大多数怪物会被重复遇到以便于......