前端必懂的设计模式-门面模式

1. 定义
外观模式( )又叫门面模式,指提供一个统一的接口去访问多个子系统的多个不同的接口,为子系统中的一组接口提供统一的高层接口 。使得子系统更容易使用,不仅简化类中的接口 , 而且实现调用者和接口的解耦 。
2. 类图
该设计模式由以下角色组成
门面角色:
外观模式的核心 。它被客户角色调用,它熟悉子系统的功能 。内部根据客户角色的需求预定了几种功能的组合 子系统角色:实现了子系统的功能 。它对客户角色和是未知的 。
子系统角色:
实现了子系统的功能 。它对客户角色和是未知的
客户角色
通过调用来完成要实现的功能
门面模式类图
3. 一个生活中的例子
比如常见的空调、冰箱、洗衣机,内部结构都并不简单,对于我们使用者而言 , 理解他们内部的运行机制的门槛比较高,但是理解遥控器/控制面板上面寥寥几个按钮就相对容易的多 , 这就是外观模式的意义 。
遥控器
在类似场景中,这些例子有以下特点:
一个统一的外观为复杂的子系统提供一个简单的高层功能接口 。原本访问者直接调用子系统内部模块导致的复杂引用关系,现在可以通过只访问这个统一的外观来避免 。
4. 通用实现
在外观模式中,客户端直接对接外观(),通过接口去对接子接口,而子接口里封装的一系列复杂操作 , 则不是我们要的重点 。
结构如下:
外观模式一般是作为子系统的功能出口出现 , 使用的时候可以在其中增加新的功能,但是不推荐这样做,因为外观应该是对已有功能的包装 , 不应在其中掺杂新的功能 。
4.1 计算器
class Sum {sum(a, b) {return a + b;}}class Minus {minus(a, b) {return a - b;}}class Multiply {multiply(a, b) {return a * b;}}class Calculator {sumObjminusObjmultiplyObjconstructor() {this.sumObj = new Sum();this.minusObj = new Minus();this.multiplyObj = new Multiply();}sum(...args) {return this.sumObj.sum(...args);}minus(...args) {return this.minusObj.minus(...args);}multiply(...args) {return this.multiplyObj.multiply(...args);}}let calculator = new Calculator();console.log(calculator.sum(1, 2));console.log(calculator.minus(1, 2));console.log(calculator.multiply(1, 2));复制代码
4.2 计算机
class CPU {startup() { console.log('打开CPU'); }shutdown() { console.log('关闭CPU'); }}class Memory {startup() { console.log('打开内存'); }shutdown() { console.log('关闭内存'); }}class Disk {startup() { console.log('打开硬盘'); }shutdown() { console.log('关闭硬盘'); }}class Computer {cpu;memory;disk;constructor() {this.cpu = new CPU();this.memory = new Memory();this.disk = new Disk();}startup() {this.cpu.startup();this.memory.startup();this.disk.startup();}shutdown() {this.cpu.shutdown();this.memory.shutdown();this.disk.shutdown();}}let computer = new Computer();computer.startup();computer.shutdown();复制代码
4.3 压缩
export { }var zlib = require('zlib');var fs = require('fs');let path = require('path');function open(input) {let ext = path.extname(input);switch (ext) {case '.gz':return unZip(input);case '.rar':return unRar(input);case '.7z':return un7z(input);default:break;}}function unZip(src) {var gunzip = zlib.createGunzip();var inputStream = fs.createReadStream(src);var outputStream = fs.createWriteStream(src.slice(0, -3));console.log('outputStream');inputStream.pipe(gunzip).pipe(outputStream);}function unRar(src) {console.log('Rar解压后的', src);}function un7z(src) {console.log('7z解压后的', src);}open('./source.txt.gz');function zip(src) {var gzip = zlib.createGzip();//创建压缩流var inputStream = fs.createReadStream(src);var outputStream = fs.createWriteStream(src+'.gz');inputStream.pipe(gzip).pipe(outputStream);}zip('source.txt');复制代码
5. 前端应用场景5.1 函数参数重载
有一种情况css如何做一个提交按钮,比如某个函数有多个参数 , 其中一个参数可以传递也可以不传递,你当然可以直接弄两个接口,但是使用函数参数重载的方式,可以让使用者获得更大的自由度 , 让两个使用上基本类似的方法获得统一的外观 。
function bindEvent(elem, type, selector, fn) {if (fn === undefined) {fn = selector;selector = null;}// ... 剩下相关逻辑}bindEvent(elem, 'click', '#div1', fn)bindEvent(elem, 'click', fn)复制代码
上面这个绑定事件的函数中 , 参数就是可选的 。
这种方式在一些工具库或者框架提供的多功能方法上经常得到使用,特别是在通用 API 的某些参数可传可不传的时候 。
参数重载之后的函数在使用上会获得更大的自由度,而不必重新创建一个新的 API,这在 Vue、React、、 等库中使用非常频繁 。
5.2 抹平浏览器兼容性问题
可以让我们处理浏览器兼容和屏蔽了浏览器差异
外观模式经常被用于的库中 , 封装一些接口用于兼容多浏览器,让我们可以间接调用我们封装的外观,从而屏蔽了浏览器差异,便于使用 。
比如经常用的兼容不同浏览器的事件绑定方法:
function addEvent(element, type, fn) {if (element.addEventListener) {// 支持 DOM2 级事件处理方法的浏览器element.addEventListener(type, fn, false);} else if (element.attachEvent) {// 不支持 DOM2 级但支持 attachEventelement.attachEvent('on' + type, fn);} else {element['on' + type] = fn;// 都不支持的浏览器}}var myInput = document.getElementById('myinput');addEvent(myInput, 'click', function() {console.log('绑定 click 事件');})复制代码
除了事件绑定之外,在抹平浏览器兼容性的其他问题上我们也经常使用外观模式:

前端必懂的设计模式-门面模式

文章插图
前端必懂的设计模式-门面模式

文章插图
// 移除 DOM 上的事件function removeEvent(element, type, fn) {if (element.removeEventListener) {element.removeEventListener(type, fn, false);} else if (element.detachEvent) {element.detachEvent('on' + type, fn);} else {element['on' + type] = null;}}// 获取样式function getStyle(obj, styleName) {if (window.getComputedStyle) {var styles = getComputedStyle(obj, null)[styleName];} else {var styles = obj.currentStyle[styleName];}return styles;}// 阻止默认事件var preventDefault = function(event) {if (event.preventDefault) {event.preventDefault();} else {// IE 下event.returnValue = https://www.jianzixun.com/false;}}// 阻止事件冒泡var cancelBubble = function(event) {if (event.stopPropagation) {event.stopPropagation();} else {// IE 下event.cancelBubble = true;}}复制代码
通过将处理不同浏览器兼容性问题的过程封装成一个外观,我们在使用的时候可以直接使用外观方法即可,在遇到兼容性问题的时候,这个外观方法自然帮我们解决,方便又不容易出错 。
5.3 Vue 源码中的函数参数重载
Vue 提供的一个创建元素的方法(opens new )就使用了函数参数重载,使得使用者在使用这个参数的时候很灵活:
export function createElement(context,tag,data,children,normalizationType,alwaysNormalize) {if (Array.isArray(data) || isPrimitive(data)) {// 参数的重载normalizationType = childrenchildren = datadata = https://www.jianzixun.com/undefined}// ...}复制代码
方法里面对第三个参数 data 进行了判断,如果第三个参数的类型是 array、、、 中的一种,那么说明是 (tag [, data], , …) 这样的使用方式,用户传的第二个参数不是 data,而是。
data 这个参数是包含模板相关属性的数据对象 , 如果用户没有什么要设置,那这个参数自然不传,不使用函数参数重载的情况下,需要用户手动传递 null 或者之类,参数重载之后,用户对 data 这个参数可传可不传,使用自由度比较大,也很方便 。
方法的源码参见链接vue/src/core/vdom/-.js
5.4源码中的函数参数重载
的 range 方法的 API 为 _.range([start=0], end, [step=1]),这就很明显使用了参数重载,这个方法调用了一个内部函数 :
function createRange(fromRight) {return (start, end, step) => {// ...if (end === undefined) {end = startstart = 0}// ...}}复制代码
意思就是,如果没有传第二个参数 , 那么就把传入的第一个参数作为 end , 并把 start 置为默认值 。
方法的源码参见链接 /./.js
【前端必懂的设计模式-门面模式】5.5源码中的函数参数重载
函数参数重载在源码中使用比较多,中也有大量使用css如何做一个提交按钮,比如 on、off、bind、one、load、 等方法,这里以 off 方法为例 , 该方法在选择元素上移除一个或多个事件的事件处理函数 。源码如下:
off: function (types, selector, fn) { // ...if (selector === false || typeof selector === 'function') {// ( types [, fn] ) 的使用方式fn = selectorselector = undefined }// ...}复制代码
可以看到如果传入第二个参数为 false 或者是函数的时候,就是 off(types [, fn]) 的使用方式 。
off 方法的源码参见链接 /src/event.js
再比如 load 方法的源码:
jQuery.fn.load = function(url, params, callback) {// ...if (isFunction(params)) {callback = paramsparams = undefined}// ...}复制代码
可以看到对第二个参数进行了判断,如果是函数,就是 load(url [, ]) 的使用方式 。
load 方法的源码参见链接 /src/ajax/load.js(opens new )
5.6源码中的外观模式
当我们使用的 $().ready(…) 来给浏览器加载事件添加回调时 ,  会使用源码中的方法:
bindReady: function() {// ...// Mozilla, Opera and webkit 支持if (document.addEventListener) {document.addEventListener('DOMContentLoaded', DOMContentLoaded, false)// A fallback to window.onload, that will always workwindow.addEventListener('load', jQuery.ready, false)// 如果使用了 IE 的事件绑定形式} else if (document.attachEvent) {document.attachEvent('onreadystatechange', DOMContentLoaded)// A fallback to window.onload, that will always workwindow.attachEvent('onload', jQuery.ready)}// ...}复制代码
通过这个方法,帮我们将不同浏览器下的不同绑定形式隐藏起来,从而简化了使用 。
方法的源码参见链接 /src/core.js
除了屏蔽浏览器兼容性问题之外,还有其他的一些其他外观模式的应用:
比如修改 css 的时候可以 $('p').css('color', 'red') , 也可以 $('p').css('width', 100),对不同样式的操作被封装到同一个外观方法中,极大地方便了使用,对不同样式的特殊处理(比如设置 width 的时候不用加 px)也一同被封装了起来 。
源码参见链接 /src/css.js
再比如的 ajax 的 API “$.ajax(url [, ]),当我们在设置以 JSONP 的形式发送请求的时候,只要传入 : 'jsonp' 设置,会进行一些额外操作帮我们启动 JSONP 流程,并不需要使用者手动添加代码,这些都被封装在 $.ajax() 这个外观方法中了 。
源码参见链接 /src/ajax/jsonp.js
5.7 Axios 源码中的外观模式
Axios 可以使用在不同环境中,那么在不同环境中发送 HTTP 请求的时候会使用不同环境中的特有模块,Axios 这里是使用外观模式来解决这个问题的:
function getDefaultAdapter() {// ...if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {// Nodejs 中使用 HTTP adapteradapter = require('./adapters/http');} else if (typeof XMLHttpRequest !== 'undefined') {// 浏览器使用 XHR adapteradapter = require('./adapters/xhr');}// ...}复制代码
这个方法进行了一个判断,如果在的环境中则使用的 HTTP 模块来发送请求,在浏览器环境中则使用这个浏览器 API 。
方法源码参见链接 axios/lib/.js
5.8 redux
export default function createStore(reducer, preloadedState, enhancer) {if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {enhancer = preloadedStatepreloadedState = undefined}}复制代码
6 设计原则验证
不符合单一职责原则和开放封闭原则 , 因此谨慎使用 , 不可滥用
7 外观模式的优缺点优点:缺点:7. 外观模式的适用场景8. 其他相关模式8.1 外观模式与中介者模式8.2 外观模式与单例模式
有时候一个系统只需要一个外观,比如之前举的 Axios 的 HTTP 模块例子 。这时我们可以将外观模式和单例模式一起使用,把外观实现为单例 。
本文到此结束,希望对大家有所帮助 。