設計模式:介面卡模式

發表於2024-02-11

設計模式是通用的、可複用的程式碼設計方案,也可以說是針對某類問題的解決方案,因此,掌握好設計模式,可以幫助我們編寫更健壯的程式碼。

wiki中將設計模式分為四類,分別是:

  • 建立模式(creational patterns)
  • 結構模式(structural patterns)
  • 行為模式(behavioral patterns)
  • 併發模式(concurrency patterns)

介面卡模式屬於其中的結構型模式,結構型——從名稱上就可以看出——與結構有關,應用這類模式不會影響物件的行為,但是會影響程式碼結構。那麼介面卡模式究竟是怎樣的一種解決方案,適合什麼場景呢,接下來我們就來探究一下。

適配

首先我們來先扣個字眼,適配是什麼意思呢?我直接問ChatGPT,得到了以下的回答:

"適配"在計算機領域通常指的是使軟體、程式或網站能夠在不同的裝置、平臺或環境下正常工作和顯示的過程。適配的主要目的是確保使用者能夠在各種裝置上獲得一致的使用者體驗,而不受裝置型別、螢幕尺寸、作業系統或瀏覽器等因素的影響。

簡單粗暴一點理解就是指的相容性。

所以應用介面卡模式的目的簡單來理解,就是提高相容性。

這類生活場景非常常見,比如公司給員工配了一臺顯示器,但是外接螢幕自帶連線線的接頭與電腦已有的介面不適配,兩者無法連線上,這種情況是很常見的,隨著技術升級、數碼裝置不斷的升級換代,導致市面上存在很多型別的介面,於是轉接頭就應運而生了——轉接頭就是介面卡模式的一種具象應用。

程式設計

適配在程式設計中也是一個常見需求。就比如WIKI中的描述:

It is often used to make existing classes work with others without modifying their source code.

翻譯過來的意思是:它(介面卡模式)通常用於在不修改原始碼的情況下使現有類與其他類協同工作。

很多開發小夥伴在現實工作中對這點應該都有所體會,在程式設計師的工作中很多時候都需要去維護已有專案,迭代新的需求,然後就可能碰到這類場景。

比如老程式碼中有些類或者物件的某個方法,在專案中的其他地方有呼叫,但是某天,突然來了一個需求,增加一個功能模組,然後這個模組需要與這些類或者物件互動,實現與這個方法類似但存在細微不同點的功能。

針對這個需求,如果不應用介面卡模式,我們可以有以下兩種做法:

第一種,如果這個方法呼叫的地方比較少,這裡是說如果,那我們可以簡單粗暴地直接把這個方法改了,測試環節就稍微麻煩點,在測試新功能模組的同時還需要回歸測試原本呼叫這個方法的地方。

第二種,給物件增加一個新的方法提供給新模組呼叫,當然這個新方法中的很多程式碼會與老方法中的程式碼重複。

很顯然,這兩種做法都不夠好,第一種需要回歸測試,而且很容易有遺漏,甚至可能引起原因不明的bug;第二種則可能使專案中存在很多冗餘程式碼,而且還可能影響後期維護,比如修改某段邏輯就要修改兩個方法的程式碼,如果是其他人來接手維護,很可能根本不知道是這樣的情況。

介面卡模式就可以應用於這類場景,它主要幫助我們解決以下問題:

  • 程式碼複用
  • 讓介面不相容的類協同工作

模式描述

介面卡模式描述了它是如何幫助我們解決上述問題的:

  • 定義一個單獨的介面卡類,將一個類(待適配)的(不相容)介面轉換成客戶端需要的另一個介面(target)。
  • 透過介面卡來處理(重用)不具備所需介面的類。

也就是說,應用介面卡模式主要做的事情,就是在程式碼中增加一個介面卡的角色。

前端應用

JavaScript作為一種物件導向程式語言,當然也可以應用介面卡模式。我們來看下面的一個例子:

Web端在以前使用Ajax技術處理非同步的時候,都是透過XHR物件,但是隨著Promise的推出,出現了更簡潔的fetch方法,為了更方便地處理非同步,現在某負責人準備在新專案中應用fetch方法,新專案從老專案中複製了基礎檔案,其中包括了Ajax程式碼,為了使專案成員快速熟悉,Ajax方法最好在用法上保持一致。

假設以下是原本的Ajax程式碼,是基於XMLHttpRequest物件進行封裝的:

function Ajax(method, url, {query, params, headers, successCallback }) {
    // 1. 建立物件
    const xhr = new XMLHttpRequest();
    // 2. 初始化 設定請求型別和url
    method = method.toUpperCase();
    let queryString = '?';
    if (query && query instanceof Object) {
        for (const key in query) {
            queryString += `${key}=${query[key]}&`
        }
        url += queryString.substring(0, queryString.length - 1);
    }
    xhr.open(method, url);
    // 3. 設定請求頭
    for (const key in headers) {
        xhr.setRequestHeader(key, headers[key]);
    }
    // 4. 傳送請求
    let paramString = '';
    if (params && params instanceof Object) {
        for (const key in params) {
            paramString += `${key}=${params[key]}&`
        }
        paramString = paramString.substring(0, paramString.length - 1);
    }
    xhr.send(paramString);
    // 5. 事件繫結 處理服務端返回的結果
    // on 當...的時候
    // readystate 0-初始化建立的時候 1-open的時候 2-send的時候 3-服務端部分返回的時候 4-服務端返回全部的時候
    // change 改變
    xhr.onreadystatechange = function () {
        // 判斷 服務端返回了所有的結果
        if (xhr.readyState === 4) {
            // 判斷響應狀態碼 200 404 403 401 500
            // 2xx 成功
            if (xhr.status >= 200 && xhr.status < 300) {
                let result = {
                    status: xhr.status, // 狀態碼
                    statusText: xhr.statusText, // 狀態字串
                    responseHeaders: xhr.getAllResponseHeaders(), // 所有響應頭
                    response: xhr.response // 響應體
                }
                successCallback(result);
            }
        }
    }
}

為了使專案中熟悉XHR呼叫方式的成員和熟悉fetch的成員能統一呼叫Ajax方法,我們可以使用介面卡模式來對程式碼進行改造。

首先建立一個介面卡物件,在JavaScript中我們知道,函式也是物件,所以我們定義如下函式:

async function AjaxAdapter(method, url, {query, params, headers, successCallback }) {
  // ...
}

這個函式的入參與原本的Ajax函式保持一致。在這裡宣告async非同步函式是因為fetch方法的返回值是Promise型別。

然後我們修改Ajax函式,去呼叫這個介面卡函式:

async function Ajax(method, url, {query, params, headers, successCallback }) {
    return AjaxAdapter(method, url, {query, params, headers, successCallback });
}

我們在介面卡函式中去處理不同的使用方式,比如如果呼叫者傳遞了successCallback這個回撥函式,說明他是舊方式的使用者,那麼就在介面卡函式中將非同步返回的結果透過successCallback進行傳遞,否則就不對非同步結果做處理,由呼叫者自行處理非同步函式的結果。

這樣無論是XHR的使用者還是fetch的使用者,都能同樣使用Ajax函式獲取非同步結果,而不會感知到其中的不同,XHR的使用者就不必一定要去學習fetch的使用,只要像以前一樣使用Ajax函式即可。

除了功能適配,資料適配在開發中也很常見,比如設定資料規範,以便於在不同系統和應用程式之間進行資料互動和處理。

總結

透過以上的探討我們可以發現,介面卡模式在實際生活和程式設計中的應用其實是很普遍和廣泛的,為了提高相容性而增加介面卡角色也是一個很常見的行為,介面卡的主要功能就是幫助我們抹平差異,也就是在介面卡的內部會去根據差異來做一些處理,而這些處理對使用者來說是透明的,不需要了解的。