1. 定義
外觀模式(Facade Pattern)又叫門面模式,指提供一個統(tǒng)一的接口去訪問多個子系統(tǒng)的多個不同的接口,為子系統(tǒng)中的一組接口提供統(tǒng)一的高層接口。使得子系統(tǒng)更容易使用,不僅簡化類中的接口,而且實現(xiàn)調(diào)用者和接口的解耦。
2. 類圖
該設(shè)計模式由以下角色組成
門面角色:
外觀模式的核心。它被客戶角色調(diào)用,它熟悉子系統(tǒng)的功能。內(nèi)部根據(jù)客戶角色的需求預(yù)定了幾種功能的組合 子系統(tǒng)角色:實現(xiàn)了子系統(tǒng)的功能。它對客戶角色和Facade是未知的。
子系統(tǒng)角色:
實現(xiàn)了子系統(tǒng)的功能。它對客戶角色和Facade是未知的
客戶角色
通過調(diào)用Facede來完成要實現(xiàn)的功能

門面模式類圖
3. 一個生活中的例子
比如常見的空調(diào)、冰箱、洗衣機(jī),內(nèi)部結(jié)構(gòu)都并不簡單,對于我們使用者而言,理解他們內(nèi)部的運行機(jī)制的門檻比較高,但是理解遙控器/控制面板上面寥寥幾個按鈕就相對容易的多,這就是外觀模式的意義。

遙控器
在類似場景中,這些例子有以下特點:
一個統(tǒng)一的外觀為復(fù)雜的子系統(tǒng)提供一個簡單的高層功能接口。 原本訪問者直接調(diào)用子系統(tǒng)內(nèi)部模塊導(dǎo)致的復(fù)雜引用關(guān)系,現(xiàn)在可以通過只訪問這個統(tǒng)一的外觀來避免。
4. 通用實現(xiàn)
在外觀模式中,客戶端直接對接外觀(Facade),通過接口去對接子接口,而子接口里封裝的一系列復(fù)雜操作,則不是我們要關(guān)注的重點。
結(jié)構(gòu)如下:

外觀模式一般是作為子系統(tǒng)的功能出口出現(xiàn),使用的時候可以在其中增加新的功能,但是不推薦這樣做,因為外觀應(yīng)該是對已有功能的包裝,不應(yīng)在其中摻雜新的功能。
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 {
sumObj
minusObj
multiplyObj
constructor() {
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));
復(fù)制代碼
4.2 計算機(jī)

class CPU {
startup() { console.log('打開CPU'); }
shutdown() { console.log('關(guān)閉CPU'); }
}
class Memory {
startup() { console.log('打開內(nèi)存'); }
shutdown() { console.log('關(guān)閉內(nèi)存'); }
}
class Disk {
startup() { console.log('打開硬盤'); }
shutdown() { console.log('關(guān)閉硬盤'); }
}
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();
復(fù)制代碼
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();//創(chuàng)建壓縮流
var inputStream = fs.createReadStream(src);
var outputStream = fs.createWriteStream(src+'.gz');
inputStream.pipe(gzip).pipe(outputStream);
}
zip('source.txt');
復(fù)制代碼
5. 前端應(yīng)用場景
5.1 函數(shù)參數(shù)重載
有一種情況,比如某個函數(shù)有多個參數(shù),其中一個參數(shù)可以傳遞也可以不傳遞,你當(dāng)然可以直接弄兩個接口,但是使用函數(shù)參數(shù)重載的方式,可以讓使用者獲得更大的自由度,讓兩個使用上基本類似的方法獲得統(tǒng)一的外觀。
function bindEvent(elem, type, selector, fn) {
if (fn === undefined) {
fn = selector;
selector = null;
}
// ... 剩下相關(guān)邏輯
}
bindEvent(elem, 'click', '#div1', fn)
bindEvent(elem, 'click', fn)
復(fù)制代碼
上面這個綁定事件的函數(shù)中,參數(shù) selector 就是可選的。
這種方式在一些工具庫或者框架提供的多功能方法上經(jīng)常得到使用,特別是在通用 API 的某些參數(shù)可傳可不傳的時候。
參數(shù)重載之后的函數(shù)在使用上會獲得更大的自由度,而不必重新創(chuàng)建一個新的 API,這在 Vue、React、jQuery、Lodash 等庫中使用非常頻繁。
5.2 polyfill抹平瀏覽器兼容性問題
polyfill可以讓我們處理瀏覽器兼容和屏蔽了瀏覽器差異。
外觀模式經(jīng)常被用于 JavaScript 的庫中,封裝一些接口用于兼容多瀏覽器,讓我們可以間接調(diào)用我們封裝的外觀,從而屏蔽了瀏覽器差異,便于使用。
比如經(jīng)常用的兼容不同瀏覽器的事件綁定方法:
function addEvent(element, type, fn) {
if (element.addEventListener) { // 支持 DOM2 級事件處理方法的瀏覽器
element.addEventListener(type, fn, false);
} else if (element.attachEvent) { // 不支持 DOM2 級但支持 attachEvent
element.attachEvent('on' + type, fn);
} else {
element['on' + type] = fn; // 都不支持的瀏覽器
}
}
var myInput = document.getElementById('myinput');
addEvent(myInput, 'click', function() {
console.log('綁定 click 事件');
})
復(fù)制代碼
除了事件綁定之外,在抹平瀏覽器兼容性的其他問題上我們也經(jīng)常使用外觀模式:
// 移除 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;
}
// 阻止默認(rèn)事件
var preventDefault = function(event) {
if (event.preventDefault) {
event.preventDefault();
} else { // IE 下
event.returnValue = false;
}
}
// 阻止事件冒泡
var cancelBubble = function(event) {
if (event.stopPropagation) {
event.stopPropagation();
} else { // IE 下
event.cancelBubble = true;
}
}
復(fù)制代碼
通過將處理不同瀏覽器兼容性問題的過程封裝成一個外觀,我們在使用的時候可以直接使用外觀方法即可,在遇到兼容性問題的時候,這個外觀方法自然幫我們解決,方便又不容易出錯。
5.3 Vue 源碼中的函數(shù)參數(shù)重載
Vue 提供的一個創(chuàng)建元素的方法 createElement (opens new window)就使用了函數(shù)參數(shù)重載,使得使用者在使用這個參數(shù)的時候很靈活:
export function createElement(
context,
tag,
data,
children,
normalizationType,
alwaysNormalize
) {
if (Array.isArray(data) || isPrimitive(data)) { // 參數(shù)的重載
normalizationType = children
children = data
data = undefined
}
// ...
}
復(fù)制代碼
createElement 方法里面對第三個參數(shù) data 進(jìn)行了判斷,如果第三個參數(shù)的類型是 array、string、number、boolean 中的一種,那么說明是 createElement(tag [, data], children, ...) 這樣的使用方式,用戶傳的第二個參數(shù)不是 data,而是 children。
data 這個參數(shù)是包含模板相關(guān)屬性的數(shù)據(jù)對象,如果用戶沒有什么要設(shè)置,那這個參數(shù)自然不傳,不使用函數(shù)參數(shù)重載的情況下,需要用戶手動傳遞 null 或者 undefined 之類,參數(shù)重載之后,用戶對 data 這個參數(shù)可傳可不傳,使用自由度比較大,也很方便。
createElement方法的源碼參見 Github 鏈接vue/src/core/vdom/create-element.js
5.4 Lodash 源碼中的函數(shù)參數(shù)重載
Lodash 的 range 方法的 API 為 _.range([start=0], end, [step=1]),這就很明顯使用了參數(shù)重載,這個方法調(diào)用了一個內(nèi)部函數(shù) createRange:
function createRange(fromRight) {
return (start, end, step) => {
// ...
if (end === undefined) {
end = start
start = 0
}
// ...
}
}
復(fù)制代碼
意思就是,如果沒有傳第二個參數(shù),那么就把傳入的第一個參數(shù)作為 end,并把 start 置為默認(rèn)值。
createRange 方法的源碼參見 Github 鏈接
lodash/.internal/createRange.js
5.5 jQuery 源碼中的函數(shù)參數(shù)重載
函數(shù)參數(shù)重載在源碼中使用比較多,jQuery 中也有大量使用,比如 on、off、bind、one、load、ajaxPrefilter 等方法,這里以 off 方法為例,該方法在選擇元素上移除一個或多個事件的事件處理函數(shù)。源碼如下:
off: function (types, selector, fn) {
// ...
if (selector === false || typeof selector === 'function') {
// ( types [, fn] ) 的使用方式
fn = selector
selector = undefined
}
// ...
}
復(fù)制代碼
可以看到如果傳入第二個參數(shù)為 false 或者是函數(shù)的時候,就是 off(types [, fn]) 的使用方式。
off 方法的源碼參見 Github 鏈接 jquery/src/event.js
再比如 load 方法的源碼:
jQuery.fn.load = function(url, params, callback) {
// ...
if (isFunction(params)) {
callback = params
params = undefined
}
// ...
}
復(fù)制代碼
可以看到 jQuery 對第二個參數(shù)進(jìn)行了判斷,如果是函數(shù),就是 load(url [, callback]) 的使用方式。
load 方法的源碼參見 Github 鏈接 jquery/src/ajax/load.js(opens new window)
5.6 jQuery 源碼中的外觀模式
當(dāng)我們使用 jQuery 的 $(document).ready(...) 來給瀏覽器加載事件添加回調(diào)時,jQuery 會使用源碼中的 bindReady 方法:
bindReady: function() {
// ...
// Mozilla, Opera and webkit 支持
if (document.addEventListener) {
document.addEventListener('DOMContentLoaded', DOMContentLoaded, false)
// A fallback to window.onload, that will always work
window.addEventListener('load', jQuery.ready, false)
// 如果使用了 IE 的事件綁定形式
} else if (document.attachEvent) {
document.attachEvent('onreadystatechange', DOMContentLoaded)
// A fallback to window.onload, that will always work
window.attachEvent('onload', jQuery.ready)
}
// ...
}
復(fù)制代碼
通過這個方法,jQuery 幫我們將不同瀏覽器下的不同綁定形式隱藏起來,從而簡化了使用。
bindReady 方法的源碼參見 Github 鏈接 jquery/src/core.js
除了屏蔽瀏覽器兼容性問題之外,jQuery 還有其他的一些其他外觀模式的應(yīng)用:
比如修改 css 的時候可以 $('p').css('color', 'red'),也可以 $('p').css('width', 100),對不同樣式的操作被封裝到同一個外觀方法中,極大地方便了使用,對不同樣式的特殊處理(比如設(shè)置 width 的時候不用加 px)也一同被封裝了起來。
源碼參見 Github 鏈接 jquery/src/css.js
再比如 jQuery 的 ajax 的 API ``$.ajax(url [, settings]),當(dāng)我們在設(shè)置以 JSONP 的形式發(fā)送請求的時候,只要傳入 dataType: 'jsonp' 設(shè)置,jQuery 會進(jìn)行一些額外操作幫我們啟動 JSONP 流程,并不需要使用者手動添加代碼,這些都被封裝在 $.ajax() 這個外觀方法中了。
源碼參見 Github 鏈接 jquery/src/ajax/jsonp.js
5.7 Axios 源碼中的外觀模式
Axios 可以使用在不同環(huán)境中,那么在不同環(huán)境中發(fā)送 HTTP 請求的時候會使用不同環(huán)境中的特有模塊,Axios 這里是使用外觀模式來解決這個問題的:
function getDefaultAdapter() {
// ...
if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
// Nodejs 中使用 HTTP adapter
adapter = require('./adapters/http');
} else if (typeof XMLHttpRequest !== 'undefined') {
// 瀏覽器使用 XHR adapter
adapter = require('./adapters/xhr');
}
// ...
}
復(fù)制代碼
這個方法進(jìn)行了一個判斷,如果在 Nodejs 的環(huán)境中則使用 Nodejs 的 HTTP 模塊來發(fā)送請求,在瀏覽器環(huán)境中則使用 XMLHTTPRequest 這個瀏覽器 API。
getDefaultAdapter 方法源碼參見 Github 鏈接 axios/lib/defaults.js
5.8 redux createStore
createStore
export default function createStore(reducer, preloadedState, enhancer) {
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}
}
復(fù)制代碼
6 設(shè)計原則驗證
不符合單一職責(zé)原則和開放封閉原則,因此謹(jǐn)慎使用,不可濫用
7 外觀模式的優(yōu)缺點
優(yōu)點:
- 訪問者不需要再了解子系統(tǒng)內(nèi)部模塊的功能,而只需和外觀交互即可,使得訪問者對子系統(tǒng)的使用變得簡單,符合最少知識原則,增強(qiáng)了可移植性和可讀性。
- 減少了與子系統(tǒng)模塊的直接引用,實現(xiàn)了訪問者與子系統(tǒng)中模塊之間的松耦合,增加了可維護(hù)性和可擴(kuò)展性。
- 通過合理使用外觀模式,可以幫助我們更好地劃分系統(tǒng)訪問層次,比如把需要暴露給外部的功能集中到外觀中,這樣既方便訪問者使用,也很好地隱藏了內(nèi)部的細(xì)節(jié),提升了安全性。
缺點:
- 不符合開閉原則,對修改關(guān)閉,對擴(kuò)展開放,如果外觀模塊出錯,那么只能通過修改的方式來解決問題,因為外觀模塊是子系統(tǒng)的唯一出口。
- 不需要或不合理的使用外觀會讓人迷惑,過猶不及。
7. 外觀模式的適用場景
- 維護(hù)設(shè)計粗糙和難以理解的遺留系統(tǒng),或者系統(tǒng)非常復(fù)雜的時候,可以為這些系統(tǒng)設(shè)置外觀模塊,給外界提供清晰的接口,以后新系統(tǒng)只需與外觀交互即可。
- 你寫了若干小模塊,可以完成某個大功能,但日后常用的是大功能,可以使用外觀來提供大功能,因為外界也不需要了解小模塊的功能。
- 團(tuán)隊協(xié)作時,可以給各自負(fù)責(zé)的模塊建立合適的外觀,以簡化使用,節(jié)約溝通時間。 如果構(gòu)建多層系統(tǒng),可以使用外觀模式來將系統(tǒng)分層,讓外觀模塊成為每層的入口,簡化層間調(diào)用,松散層間耦合。
8. 其他相關(guān)模式
8.1 外觀模式與中介者模式
- 外觀模式:封裝子使用者對子系統(tǒng)內(nèi)模塊的直接交互,方便使用者對子系統(tǒng)的調(diào)用。
- 中介者模式:封裝子系統(tǒng)間各模塊之間的直接交互,松散模塊間的耦合。
8.2 外觀模式與單例模式
有時候一個系統(tǒng)只需要一個外觀,比如之前舉的 Axios 的 HTTP 模塊例子。這時我們可以將外觀模式和單例模式一起使用,把外觀實現(xiàn)為單例。
文章出自:??peffy??,如有轉(zhuǎn)載本文請聯(lián)系前端餐廳ReTech今日頭條號。
github:https://github.com/zuopf769
