適配器模式(Adapter Pattern)又稱包裝器模式,將一個類(對象)的接口(方法、屬性)轉化爲用戶需要的另一個接口,解決類(對象)之間接口不兼容的問題。
主要功能是進行轉換匹配,目的是複用已有的功能,而不是來實現新的接口。也就是說,訪問者需要的功能應該是已經實現好了的,不需要適配器模式來實現,適配器模式主要是負責把不兼容的接口轉換成訪問者期望的格式而已。
1. 你曾見過的適配器模式
現實生活中我們會遇到形形色色的適配器,最常見的就是轉接頭了,比如不同規格電源接口的轉接頭、iPhone 手機的 3.5 毫米耳機插口轉接頭、DP/miniDP/HDMI/DVI/VGA 等視頻轉接頭、電腦、手機、ipad 的電源適配器,都是屬於適配器的範疇。
還有一個比較典型的翻譯官場景,比如老闆張三去國外談合作,帶了個翻譯官李四,那麼李四就是作爲講不同語言的人之間交流的適配器 ,老闆張三的話的內容含義沒有變化,翻譯官將老闆的話轉換成國外客戶希望的形式。
在類似場景中,這些例子有以下特點:
1. 舊有接口格式已經不滿足現在的需要;
2. 通過增加適配器來更好地使用舊有接口;
2. 適配器模式的實現
我們可以實現一下電源適配器的例子,一開始我們使用的中國插頭標準:
var chinaPlug = {
type: '中國插頭',
chinaInPlug() {
console.log('開始供電')
}
};
chinaPlug.chinaInPlug();
// 開始供電
但是我們出國旅遊了,到了日本,需要增加一個日本插頭到中國插頭的電源適配器,來將我們原來的電源線用起來:
var chinaPlug = {
type: '中國插頭',
chinaInPlug() {
console.log('開始供電');
}
};
var japanPlug = {
type: '日本插頭',
japanInPlug() {
console.log('開始供電');
}
};
// 日本插頭電源適配器
function japanPlugAdapter(plug) {
return {
chinaInPlug() {
return plug.japanInPlug();
}
}
};
japanPlugAdapter(japanPlug).chinaInPlug();
// 開始供電
由於適配器模式的例子太簡單,如果希望看更多的實戰相關應用,可以看下一個小節。
適配器模式的原理大概如下圖:
訪問者需要目標對象的某個功能,但是這個對象的接口不是自己期望的,那麼通過適配器模式對現有對象的接口進行包裝,來獲得自己需要的接口格式。
3. 適配器模式在實戰中的應用
適配器模式在日常開發中還是比較頻繁的,其實可能你已經使用了,但卻不知道原來這就是適配器模式啊。
我們可以推而廣之,適配器可以將新的軟件實體適配到老的接口,也可以將老的軟件實體適配到新的接口,具體如何來進行適配,可以根據具體使用場景來靈活使用。
3.1. JQuery.ajax 適配 Axios
有的使用 JQuery 的老項目使用 $.ajax 來發送請求,現在的新項目一般使用 Axios,那麼現在有個老項目的代碼中全是 $.ajax,如果你挨個修改,那麼 bug 可能就跟地鼠一樣到處冒出來讓你焦頭爛額,這時可以採用適配器模式來將老的使用形式適配到新的技術棧上:
// 適配器
function ajaxToAxiosAdapter(ajaxOptions) {
return axios({
url: ajaxOptions.url,
method: ajaxOptions.type,
responseType: ajaxOptions.dataType,
data: ajaxOptions.data
})
.then(ajaxOptions.success)
.catch(ajaxOptions.error)
}
// 經過適配器包裝
$.ajax = function(options) {
return ajaxToAxiosAdapter(options)
}
$.ajax({
url: '/demo-url',
type: 'POST',
dataType: 'json',
data: {
name: '張三',
id: '2345'
},
success: function(data) {
console.log('訪問成功')
},
error: function(err) {
console.err('訪問失敗')
}
})
可以看到老的代碼表現形式依然不變,但是真正發送請求是通過新的發送方式來進行的。當然你也可以把 Axios 的請求適配到 $.ajax上,就看你如何使用適配器了
3.2. 業務數據適配
在實際項目中,我們經常會遇到樹形數據結構和表形數據結構的轉換,比如全國省市區結構、公司組織結構、軍隊編制結構等等。以公司組織結構爲例,在歷史代碼中,後端給了公司組織結構的樹形數據,在以後的業務迭代中,會增加一些要求非樹形結構的場景。比如增加了將組織維護起來的功能,因此就需要在新增組織的時候選擇上級組織,在某個下拉菜單中選擇這個新增組織的上級菜單。或者增加了將人員歸屬到某一級組織的需求,需要在某個下拉菜單中選擇任一級組織。
在這些業務場景中,都需要將樹形結構平鋪開,但是我們又不能直接將舊有的樹形結構狀態進行修改,因爲在項目別的地方已經使用了老的樹形結構狀態,這時我們可以引入適配器來將老的數據結構進行適配:
// 原來的樹形結構
const oldTreeData = [
{
name: '總部',
place: '一樓',
children: [
{
name: '財務部',
place: '二樓'
},
{
name: '生產部',
place: '三樓'
},
{
name: '開發部',
place: '三樓',
children: [
{
name: '軟件部',
place: '四樓',
children: [
{ name: '後端部', place: '五樓' },
{ name: '前端部', place: '七樓' },
{ name: '技術部', place: '六樓' }
]
},
{
name: '硬件部',
place: '四樓',
children: [
{ name: 'DSP部', place: '八樓' },
{ name: 'ARM部', place: '二樓' },
{ name: '調試部', place: '三樓' }
]
}
]
}
]
}
];
// 樹形結構平鋪
function treeDataAdapter(treeData, lastArrayData = []) {
treeData.forEach(item => {
if (item.children) {
treeDataAdapter(item.children, lastArrayData)
}
const { name, place } = item
lastArrayData.push({ name, place })
})
return lastArrayData
};
// 返回平鋪的組織結構
var data = treeDataAdapter(oldTreeData);
增加適配器後,就可以將原先狀態的樹形結構轉化爲所需的結構,而並不改動原先的數據,也不對原來使用舊數據結構的代碼有所影響。
3.3. Vue 計算屬性
Vue 中的計算屬性也是一個適配器模式的實例,以官網的例子爲例,我們可以一起來理解一下:
<template>
<div id="example">
<p>Original message: "{{ message }}"</p>
<!-- Hello -->
<p>Computed reversed message: "{{ reversedMessage }}"</p>
<!-- olleH -->
</div>
</template>
<script type='text/javascript'>
export default {
name: 'demo',
data() {
return {
message: 'Hello'
}
},
computed: {
reversedMessage: function() {
return this.message.split('').reverse().join('')
}
}
}
</script>
舊有 data 中的數據不滿足當前的要求,通過計算屬性的規則來適配成我們需要的格式,對原有數據並沒有改變,只改變了原有數據的表現形式。
4. 源碼中的適配器模式
Axios 是比較熱門的網絡請求庫,在瀏覽器中使用的時候,Axios 的用來發送請求的 adapter 本質上是封裝瀏覽器提供的 API
XMLHttpRequest,我們可以看看源碼中是如何封裝這個 API 的,爲了方便觀看,進行了一些省略:
module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
var requestData = config.data;
var requestHeaders = config.headers;
var request = new XMLHttpRequest();
// 初始化一個請求
request.open(
config.method.toUpperCase(),
buildURL(config.url, config.params, config.paramsSerializer), true
);
// 設置最大超時時間
request.timeout = config.timeout;
// readyState 屬性發生變化時的回調
request.onreadystatechange = function handleLoad() { };
// 瀏覽器請求退出時的回調
request.onabort = function handleAbort() { };
// 當請求報錯時的回調
request.onerror = function handleError() { };
// 當請求超時調用的回調
request.ontimeout = function handleTimeout() { };
// 設置HTTP請求頭的值
if ('setRequestHeader' in request) {
request.setRequestHeader(key, val);
};
// 跨域的請求是否應該使用證書
if (config.withCredentials) {
request.withCredentials = true
};
// 響應類型
if (config.responseType) {
request.responseType = config.responseType
};
// 發送請求
request.send(requestData);
})
}
可以看到這個模塊主要是對請求頭、請求配置和一些回調的設置,並沒有對原生的 API 有改動,所以也可以在其他地方正常使用。這個適配器可以看作是對 XMLHttpRequest 的適配,是用戶對 Axios 調用層到原生 XMLHttpRequest 這個 API 之間的適配層。
源碼可以參見 Github 倉庫:axios/lib/adapters/xhr.js
5. 適配器模式的優缺點
適配器模式的優點:
1.已有的功能如果只是接口不兼容,使用適配器適配已有功能,可以使原有邏輯得到更好的複用,有助於避免大規模改寫現有代碼;
2.可擴展性良好,在實現適配器功能的時候,可以調用自己開發的功能,從而方便地擴展系統的功能;
3.靈活性好,因爲適配器並沒有對原有對象的功能有所影響,如果不想使用適配器了,那麼直接刪掉即可,不會對使用原有對象的代碼有影響;
適配器模式的缺點:會讓系統變得零亂,明明調用 A,卻被適配到了 B,如果系統中這樣的情況很多,那麼對可閱讀性不太友好。如果沒必要使用適配器模式的話,可以考慮重構,如果使用的話,可以考慮儘量把文檔完善。
6. 適配器模式的適用場景
當你想用已有對象的功能,卻想修改它的接口時,一般可以考慮一下是不是可以應用適配器模式。
1.如果你想要使用一個已經存在的對象,但是它的接口不滿足需求,那麼可以使用適配器模式,把已有的實現轉換成你需要的接口;
2.如果你想創建一個可以複用的對象,而且確定需要和一些不兼容的對象一起工作,這種情況可以使用適配器模式,然後需要什麼就適配什麼;
7. 其他相關模式
適配器模式和代理模式、裝飾者模式看起來比較類似,都是屬於包裝模式,也就是用一個對象來包裝另一個對象的模式,他們之間的異同在代理模式中已經詳細介紹了,這裏再簡單對比一下。
7.1 適配器模式與代理模式
1.適配器模式: 提供一個不一樣的接口,由於原來的接口格式不能用了,提供新的接口以滿足新場景下的需求;
2.代理模式:提供一模一樣的接口,由於不能直接訪問目標對象,找個代理來幫忙訪問,使用者可以就像訪問目標對象一樣來訪問代理對象;
7.2 適配器模式、裝飾者模式與代理模式
1.適配器模式: 功能不變,只轉換了原有接口訪問格式;
2.裝飾者模式:擴展功能,原有功能不變且可直接使用;
3.代理模式:原有功能不變,但一般是經過限制訪問的;