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

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

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

3.1 角色?
- Target:目標(biāo)抽象類
 - 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();
    }
    //轉(zhuǎn)換后的接口和轉(zhuǎn)換前不一樣
    charge() {
        return this.socket.output() + ' 經(jīng)過轉(zhuǎn)換 輸出24V';
    }
}
let powerAdapter = new PowerAdapter(new Socket());
console.log(powerAdapter.charge());4. 場景?
當(dāng)你想用已有對象的功能,卻想修改它的接口時,一般可以考慮一下是不是可以應(yīng)用適配器模式。
- 如果你想要使用一個已經(jīng)存在的對象,但是它的接口不滿足需求,那么可以使用適配器模式,把已有的實現(xiàn)轉(zhuǎn)換成你需要的接口。
 - 如果你想創(chuàng)建一個可以復(fù)用的對象,而且確定需要和一些不兼容的對象一起工作,這種情況可以使用適配器模式,然后需要什么就適配什么。
 
4.1 axios?
axios源碼中采用了process和XMLHttpRequest。 通過宿主環(huán)境的特有對象識別當(dāng)前環(huán)境,適配出不同環(huán)境下如:客戶端瀏覽器和nodejs的請求方式。
/adapters 目錄中包含如下這些文件
├─adapters
│   http.js
│   README.md
│   xhr.js
適配器的入?yún)⒍际莄onfig,返回的都是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 來發(fā)送請求,現(xiàn)在的新項目一般使用 Axios,那么現(xiàn)在有個老項目的代碼中全是 $.ajax,如果逐個修改,無疑工作量巨大而且很容易引發(fā)各種亂七八糟 bug,這時可以采用適配器模式來將老的使用形式適配到新的技術(shù)棧上:
/* 適配器 */
function ajax2AxiosAdapter(ajaxOptions) {
  return axios({
    url: ajaxOptions.url,
    method: ajaxOptions.type,
    responseType: ajaxOptions.dataType,
    data: ajaxOptions.data
  })
    .then(ajaxOptions.success)
    .catch(ajaxOptions.error)
}
/* 經(jīng)過適配器包裝 */
$.ajax = function(options) {
  return ajax2AxiosAdapter(options);
}
// 測試:用 jQuery 的方式發(fā)送一個 Ajax 請求
$.ajax({
  url: '/demo-url',
  type: 'POST',
  dataType: 'json',
  data: {
    name: '張三',
    id: '13'
  },
  success: function(data) {
    console.log('請求成功!')
  },
  error: function(err) {
    console.error('請求失敗!')
  }
})
可以看到老的代碼表現(xiàn)形式依然不變,但是真正發(fā)送請求是通過新的發(fā)送方式來進行的。當(dāng)然你也可以把 Axios 的請求適配到 $.ajax 上,就看你如何使用適配器了。
4.3 promisify?
- 作用:將callback形式轉(zhuǎn)換為Promise對象
 - Node中異步回調(diào)中有個約定:Error first,回調(diào)函數(shù)中的第一個參數(shù)一定是Error對象,其余參數(shù)才是正確的數(shù)據(jù)。
 
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 業(yè)務(wù)數(shù)據(jù)適配?
在實際項目中,我們經(jīng)常會遇到樹形數(shù)據(jù)結(jié)構(gòu)和表形數(shù)據(jù)結(jié)構(gòu)的轉(zhuǎn)換,比如全國省市區(qū)結(jié)構(gòu)、公司組織結(jié)構(gòu)、軍隊編制結(jié)構(gòu)等等。以公司組織結(jié)構(gòu)為例,在歷史代碼中,后端給了公司組織結(jié)構(gòu)的樹形數(shù)據(jù),在以后的業(yè)務(wù)迭代中,會增加一些要求非樹形結(jié)構(gòu)的場景。比如增加了將組織維護起來的功能,因此就需要在新增組織的時候選擇上級組織,在某個下拉菜單中選擇這個新增組織的上級菜單。或者增加了將人員歸屬到某一級組織的需求,需要在某個下拉菜單中選擇任一級組織。
在這些業(yè)務(wù)場景中,都需要將樹形結(jié)構(gòu)平鋪開,但是我們又不能直接將舊有的樹形結(jié)構(gòu)狀態(tài)進行修改,因為在項目別的地方已經(jīng)使用了老的樹形結(jié)構(gòu)狀態(tài),這時我們可以引入適配器來將老的數(shù)據(jù)結(jié)構(gòu)進行適配:
/* 原來的樹形結(jié)構(gòu) */
const oldTreeData = [
  {
    name: '總部',
    place: '一樓',
    children: [
      { name: '財務(wù)部', place: '二樓' },
      { name: '生產(chǎn)部', place: '三樓' },
      {
        name: '開發(fā)部', place: '三樓', children: [
          {
            name: '軟件部', place: '四樓', children: [
              { name: '后端部', place: '五樓' },
              { name: '前端部', place: '七樓' },
              { name: '技術(shù)支持部', place: '六樓' }]
          }, {
            name: '硬件部', place: '四樓', children: [
              { name: 'DSP部', place: '八樓' },
              { name: 'ARM部', place: '二樓' },
              { name: '調(diào)試部', place: '三樓' }]
          }]
      }
    ]
  }
]
/* 樹形結(jié)構(gòu)平鋪 */
function treeDataAdapter(treeData, lastArrayData = []) {
  treeData.forEach(item {
    if (item.children) {
      treeDataAdapter(item.children, lastArrayData)
    }
    const { name, place } = item
    lastArrayData.push({ name, place })
  })
  return lastArrayData
}
// 測試:返回平鋪的組織結(jié)構(gòu)
treeDataAdapter(oldTreeData)
增加適配器后,就可以將原先狀態(tài)的樹形結(jié)構(gòu)轉(zhuǎn)化為所需的結(jié)構(gòu),而并不改動原先的數(shù)據(jù),也不對原來使用舊數(shù)據(jù)結(jié)構(gòu)的代碼有所影響。
4.5 Vue 計算屬性?
Vue 中的計算屬性也是一個適配器模式的實例,以官網(wǎng)的例子為例,我們可以一起來理解一下:
<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 中的數(shù)據(jù)不滿足當(dāng)前的要求,通過計算屬性的規(guī)則來適配成我們需要的格式,對原有數(shù)據(jù)并沒有改變,只改變了原有數(shù)據(jù)的表現(xiàn)形式。
4.6 Sequelize?
基于promise的Node.js ORM工具
Sequelize
Sequelize支持MySQL、MariaDB、SQLite等數(shù)據(jù)庫方言的適配
方言
//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. 設(shè)計原則驗證?
6. 優(yōu)缺點?
6.1 優(yōu)點?
- 已有的功能如果只是接口不兼容,使用適配器適配已有功能,可以使原有邏輯得到更好的復(fù)用,有助于避免大規(guī)模改寫現(xiàn)有代碼。
 - 可擴展性良好,在實現(xiàn)適配器功能的時候,可以調(diào)用自己開發(fā)的功能,從而方便地擴展系統(tǒng)的功能。
 - 靈活性好,因為適配器并沒有對原有對象的功能有所影響,如果不想使用適配器了,那么直接刪掉即可,不會對使用原有對象的代碼有影響。
 
6.2 缺點?
會讓系統(tǒng)變得零亂,明明調(diào)用 A,卻被適配到了 B,如果系統(tǒng)中這樣的情況很多,那么對可閱讀性不太友好。如果沒必要使用適配器模式的話,可以考慮重構(gòu),如果使用的話,可以考慮盡量把文檔完善。
7. 其他相關(guān)模式?
7.1 適配器模式與代理模式?
- 適配器模式:提供一個不一樣的接口,由于原來的接口格式不能用了,提供新的接口以滿足新場景下的需求。
 - 代理模式:提供一模一樣的接口,由于不能直接訪問目標(biāo)對象,找個代理來幫忙訪問,使用者可以就像訪問目標(biāo)對象一樣來訪問代理對象。
 
7.2 適配器模式、裝飾者模式與代理模式?
- 適配器模式:功能不變,只轉(zhuǎn)換了原有接口訪問格式。
 - 裝飾者模式:擴展功能,原有功能不變且可直接使用。
 - 代理模式:原有功能不變,但一般是經(jīng)過限制訪問的。
 
?文章出自:??前端餐廳ReTech??,如有轉(zhuǎn)載本文請聯(lián)系前端餐廳ReTech今日頭條號。
github:https://github.com/zuopf769
