1. 介紹
1.1 定義?
適配器模式(Adapter Pattern)又稱包裝器模式,將一個類(對象)的接口(方法、屬性)轉化為用戶需要的另一個接口,解決類(對象)之間接口不兼容的問題。

1.2 主要功能?
主要功能是進行轉換匹配,目的是復用已有的功能,而不是來實現新的接口。也就是說,訪問者需要的功能應該是已經實現好了的,不需要適配器模式來實現,適配器模式主要是負責把不兼容的接口轉換成訪問者期望的格式而已。
2. 生活中的例子?
- 電源接口的轉接頭、Type-C 轉 HDMI 等視頻轉接頭。
- 同聲傳譯,充當兩國友人互相交流的中間人

在類似場景中,這些例子有以下特點:
- 舊有接口格式已經不滿足現在的需要。
- 通過增加適配器來更好地使用舊有接口。
3. 通用實現?

3.1 角色?
- Target:目標抽象類
- Adapter:適配器類
- Adaptee:適配者類
- Client:客戶類
3.2 代碼?
class Socket {
output() {
return '輸出220V';
}
}
abstract class Power {
abstract charge(): string;
}
class PowerAdapter extends Power {
constructor(public socket: Socket) {
super();
}
//轉換后的接口和轉換前不一樣
charge() {
return this.socket.output() + ' 經過轉換 輸出24V';
}
}
let powerAdapter = new PowerAdapter(new Socket());
console.log(powerAdapter.charge());
4. 場景?
當你想用已有對象的功能,卻想修改它的接口時,一般可以考慮一下是不是可以應用適配器模式。
- 如果你想要使用一個已經存在的對象,但是它的接口不滿足需求,那么可以使用適配器模式,把已有的實現轉換成你需要的接口。
- 如果你想創建一個可以復用的對象,而且確定需要和一些不兼容的對象一起工作,這種情況可以使用適配器模式,然后需要什么就適配什么。
4.1 axios?
axios源碼中采用了process和XMLHttpRequest。 通過宿主環境的特有對象識別當前環境,適配出不同環境下如:客戶端瀏覽器和nodejs的請求方式。
/adapters 目錄中包含如下這些文件
├─adapters
│ http.js
│ README.md
│ xhr.js
適配器的入參都是config,返回的都是promise
//let axios = require('axios');
let url = require('url');
function axios(config: any): any {
let adaptor = getDefaultAdapter();
return adaptor(config);
}
axios({
method: 'GET',
url: 'http://localhost:8080/api/user?id=1'
}).then(function (response: any) {
console.log(response);
}, function (error: any) {
console.log(error);
})
function xhr(config: any) {
return new Promise(function (resolve, reject) {
var request = new XMLHttpRequest();
request.open(config.method, config.url, true);
request.onreadystatechange = function () {
if (request.readyState == 4) {
if (request.status == 200) {
resolve(request.response);
} else {
reject('請求失敗');
}
}
}
})
}
function http(config: any) {
let http = require('http');
let urlObject = url.parse(config.url);
return new Promise(function (resolve, reject) {
const options = {
hostname: urlObject.hostname,
port: urlObject.port,
path: urlObject.pathname,
method: config.method
};
var req = http.request(options, function (res: any) {
let chunks: any[] = [];
res.on('data', (chunk: any) => {
chunks.push(chunk);
});
res.on('end', () {
resolve(Buffer.concat(chunks).toString());
});
});
req.on('error', (err: any) => {
reject(err);
});
req.end();
})
}
function getDefaultAdapter(): any {
var adapter;
if (typeof XMLHttpRequest !== 'undefined') {
adapter = xhr;
} else if (typeof process !== 'undefined') {
adapter = http;
}
return adapter;
}
server.js
let express = require('express');
let app = express();
app.get('/api/user', (req, res) => {
res.json({ id: req.query.id, name: 'zhufeng' });
});
app.listen(8080);
4.2 jQuery.ajax 適配 Axios?
有的使用 jQuery 的老項目使用 $.ajax 來發送請求,現在的新項目一般使用 Axios,那么現在有個老項目的代碼中全是 $.ajax,如果逐個修改,無疑工作量巨大而且很容易引發各種亂七八糟 bug,這時可以采用適配器模式來將老的使用形式適配到新的技術棧上:
/* 適配器 */
function ajax2AxiosAdapter(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 ajax2AxiosAdapter(options);
}
// 測試:用 jQuery 的方式發送一個 Ajax 請求
$.ajax({
url: '/demo-url',
type: 'POST',
dataType: 'json',
data: {
name: '張三',
id: '13'
},
success: function(data) {
console.log('請求成功!')
},
error: function(err) {
console.error('請求失敗!')
}
})
可以看到老的代碼表現形式依然不變,但是真正發送請求是通過新的發送方式來進行的。當然你也可以把 Axios 的請求適配到 $.ajax 上,就看你如何使用適配器了。
4.3 promisify?
- 作用:將callback形式轉換為Promise對象
- Node中異步回調中有個約定:Error first,回調函數中的第一個參數一定是Error對象,其余參數才是正確的數據。
let fs = require('fs');
var Bluebird = require("bluebird");
let readFile = Bluebird.promisify(fs.readFile);
(async function () {
let content = await readFile('./1.txt', 'utf8');
console.log(content);
})()
function promisify(readFile: any) {
return function (filename: any, encoding: any) {
return new Promise(function (resolve, reject) {
readFile(filename, encoding, function (err: any, data: any) {
if (err)
reject(err);
else
resolve(data);
})
});
}
}
4.4 業務數據適配?
在實際項目中,我們經常會遇到樹形數據結構和表形數據結構的轉換,比如全國省市區結構、公司組織結構、軍隊編制結構等等。以公司組織結構為例,在歷史代碼中,后端給了公司組織結構的樹形數據,在以后的業務迭代中,會增加一些要求非樹形結構的場景。比如增加了將組織維護起來的功能,因此就需要在新增組織的時候選擇上級組織,在某個下拉菜單中選擇這個新增組織的上級菜單。或者增加了將人員歸屬到某一級組織的需求,需要在某個下拉菜單中選擇任一級組織。
在這些業務場景中,都需要將樹形結構平鋪開,但是我們又不能直接將舊有的樹形結構狀態進行修改,因為在項目別的地方已經使用了老的樹形結構狀態,這時我們可以引入適配器來將老的數據結構進行適配:
/* 原來的樹形結構 */
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
}
// 測試:返回平鋪的組織結構
treeDataAdapter(oldTreeData)
增加適配器后,就可以將原先狀態的樹形結構轉化為所需的結構,而并不改動原先的數據,也不對原來使用舊數據結構的代碼有所影響。
4.5 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.6 Sequelize?
基于promise的Node.js ORM工具
Sequelize
Sequelize支持MySQL、MariaDB、SQLite等數據庫方言的適配
方言
//cnpm i sequelize sqlite3 -S
const { Sequelize, Model, DataTypes } = require('sequelize');
const sequelize = new Sequelize('sqlite::memory:');
class User extends Model { }
User.init({
username: DataTypes.STRING
}, { sequelize, modelName: 'user' });
sequelize.sync()
.then(() User.create({
username: 'zhufeng'
}))
.then(result {
console.log(result.toJSON());
});
5. 設計原則驗證?
6. 優缺點?
6.1 優點?
- 已有的功能如果只是接口不兼容,使用適配器適配已有功能,可以使原有邏輯得到更好的復用,有助于避免大規模改寫現有代碼。
- 可擴展性良好,在實現適配器功能的時候,可以調用自己開發的功能,從而方便地擴展系統的功能。
- 靈活性好,因為適配器并沒有對原有對象的功能有所影響,如果不想使用適配器了,那么直接刪掉即可,不會對使用原有對象的代碼有影響。
6.2 缺點?
會讓系統變得零亂,明明調用 A,卻被適配到了 B,如果系統中這樣的情況很多,那么對可閱讀性不太友好。如果沒必要使用適配器模式的話,可以考慮重構,如果使用的話,可以考慮盡量把文檔完善。
7. 其他相關模式?
7.1 適配器模式與代理模式?
- 適配器模式:提供一個不一樣的接口,由于原來的接口格式不能用了,提供新的接口以滿足新場景下的需求。
- 代理模式:提供一模一樣的接口,由于不能直接訪問目標對象,找個代理來幫忙訪問,使用者可以就像訪問目標對象一樣來訪問代理對象。
7.2 適配器模式、裝飾者模式與代理模式?
- 適配器模式:功能不變,只轉換了原有接口訪問格式。
- 裝飾者模式:擴展功能,原有功能不變且可直接使用。
- 代理模式:原有功能不變,但一般是經過限制訪問的。
?文章出自:??前端餐廳ReTech??,如有轉載本文請聯系前端餐廳ReTech今日頭條號。
github:https://github.com/zuopf769
