設計模式讀書筆記之介面卡模式、裝飾者模式

leayun發表於2021-07-24

介面卡模式和裝飾者模式

介面卡模式

介面卡模式是將一個類(物件)的介面(方法或者屬性)轉化成另外一個介面,使得原本由於介面不相容而不能一起工作的那些類(物件)可以一起工作

舉個例子:

飛機類和火車類,他們都是交通運輸工具,都適用於中長途,但就行駛方式來說,火車是在地上跑的,飛機是在天上飛的。如果要讓火車在天上飛(flying),則可以複用飛機的飛行功能,但其具體的行駛動作還是應該在地上跑(running),此時,我們就可以建立一個火車的介面卡,能夠讓火車也支援 flying 方法,但其內部還是呼叫的 running

  • 首先定義飛機和火車的抽象類函式
// 抽象工廠方法(實現子類繼承抽象類的工廠)
var VehicleFactory = function (subType, superType) {
  // 判斷抽象工廠中是否有該抽象類
  if (typeof VehicleFactory[superType] === "function") {
    // 快取類(寄生式繼承)
    function F() {}
    // 繼承父類屬性和方法
    F.prototype = VehicleFactory[superType].prototype;
    // 子類原型繼承父類
    var p = new F();
    // 將子類constructor指向子類
    p.constructor = subType;
    // 設定子類的原型
    subType.prototype = p;
  } else {
    // 不存在改抽象類則丟擲錯誤
    throw new Error("未建立該抽象類");
  }
};

// 飛機抽象類(抽象類是一種宣告但不能使用的類)
VehicleFactory.Airplane = function () {};
VehicleFactory.Airplane.prototype = {
  flying: function () {
    throw new Error("該方法未定義!");
  },
  transportation: function () {
    throw new Error("該方法未定義!");
  },
};

// 火車抽象類
VehicleFactory.Train = function () {};
VehicleFactory.Train.prototype = {
  running: function () {
    throw new Error("該方法未定義!");
  },
  transportation: function () {
    throw new Error("該方法未定義!");
  },
};
  • 定義具體的火車和飛機的建構函式
// 飛機
var CivilAircraft = function () {
  VehicleFactory.Airplane.call(this);
};
VehicleFactory(CivilAircraft, "Airplane"); // 原型是Airplane
CivilAircraft.prototype.transportation = function () {
  console.log("速度很快的交通工具!");
};
CivilAircraft.prototype.flying = function () {
  console.log("能夠飛起來!");
};

// 火車
var CivilTrain = function () {
  VehicleFactory.Train.call(this);
};
VehicleFactory(CivilTrain, "Train"); // 原型是Train
CivilTrain.prototype.transportation = function () {
  console.log("比飛機慢的交通工具!");
};
CivilTrain.prototype.running = function () {
  console.log("在地上馳騁!");
};
  • 為了讓火車能夠飛起來,讓火車也支援 flying 方法,就需要建立一個新的火車介面卡 TrainAdapter
// 火車介面卡
var TrainAdapter = function (oTrain) {
  VehicleFactory.Airplane.call(this);
  this.oTrain = oTrain;
};
TrainAdapter.prototype = new VehicleFactory.Airplane();
TrainAdapter.prototype.flying = function () {
  this.oTrain.running();
};
TrainAdapter.prototype.transportation = function () {
  this.oTrain.transportation();
};

該建構函式接受一個火車的例項物件,然後使用 VehicleFactory.Airplane 進行 apply,其介面卡原型是 VehicleFactory.Airplane,然後要重新修改其原型的 flying 方法,以便內部呼叫 oTrain.running()方法

  • 再測試一下飛機、火車及介面卡的行為
var oCivilAircraft = new CivilAircraft();
var oCivilTrain = new CivilTrain();
var oTrainAdapter = new TrainAdapter(oCivilTrain);

//原有的飛機行為
oCivilAircraft.flying(); // 能夠飛起來!
oCivilAircraft.transportation(); // 速度很快的交通工具!

//原有的火車行為
oCivilTrain.running(); // 在地上馳騁!
oCivilTrain.transportation(); // 比飛機慢的交通工具!

//介面卡火車的行為(火車呼叫飛機的方法名稱)
oTrainAdapter.transportation(); // 比飛機慢的交通工具!
oTrainAdapter.flying(); // 在地上馳騁!

驗證成功,但是此處也暴露出這種介面卡有一個問題,就是火車類原本的 transportation 方法在介面卡中也需要重寫一遍。如果需要介面卡在繼承火車類的基礎上擴充套件所有飛機類的行為時,可以考慮使用多繼承

介面卡還有一些其他應用,在《JavaScript設計模式》這本書上是以 A 框架適配 JQuery 框架做介紹的。如果兩個框架的api非常的相似,那麼用 window.A = A = jQuery即可實現兩個框架的適配,這樣可以在不改變原來程式碼的情況下正確的執行新框架的介面。除此之外,介面卡模式還可用於:

  • 使用一個已經存在的物件,但其方法或屬性介面不符合你的要求;

  • 前後端資料傳遞,把後端資料適配成我們可用的資料格式再使用;

  • 你想建立一個可複用的物件,該物件可以與其它不相關的物件或不可見物件(即介面方法或屬性不相容的物件)協同工作;

  • 想使用已經存在的物件,但是不能對每一個都進行原型繼承以匹配它的介面。物件介面卡可以適配它的父物件介面方法或屬性。

裝飾者模式

裝飾者模式就是在不改變原物件的基礎上,通過對其進行包裝擴充套件(新增屬性或者方法)使原有物件可以滿足更復雜的需求

舉個例子:

現在有一個前人完成的專案,產品經理說要加新需求,當使用者點選輸入框時,如果輸入框輸入的內容有限制,那麼在輸入框下提示相應文案,且不同的輸入框提示文案不相同。如名字輸入框提示輸入數字字母,電話輸入框提示輸入純數字等等。如果輸入框很多,那麼一條一條查詢程式碼並進行修改將非常麻煩

這時候我們可以使用裝飾者模式,在原有的功能的基礎上新增一些新功能來滿足需求,這時候我們就不需要重寫或者修改原來定義的方法

// 裝飾者
var decorator = function (input, fn) {
  // 獲取事件源
  var input = document.getElementById(input);
  // 如果事件源已經繫結事件
  if (typeof input.onclick === "function") {
    // 快取事件源原有回撥函式
    var oldClickFn = input.onClick;
    // 為事件源定義新的事件
    input.onClick = function () {
      // 事件源原有回撥函式
      oldClickFn();
      // 執行事件源新增回撥函式
      fn();
    };
  } else {
    // 事件源未繫結事件,直接為事件源新增新增回撥函式
    input.onClick = fn;
  }
};

此時,我們在原有基礎上為專案新增新功能時,可以不用深入瞭解原來的程式碼,只要呼叫這個裝飾者並傳入你新增的方法就可以了

// 姓名框新增功能
decorator('name_input', function() {
  console.log('姓名輸入框只能輸入漢字和英文!')
})
// 電話框新增功能
decorator('tel_input', function() {
  console.log('電話輸入框只能輸入純數字!)
})

裝飾者模式很簡單,就是對原有物件的屬性和方法的新增。但是裝飾者模式很強大,因為它可以對原有功能進行擴充套件,比如在一些框架中對瀏覽器原有方法的擴充套件再封裝就是用到了裝飾者這個模式。

介面卡模式和裝飾者模式的差別

  • 介面卡模式呼叫新增的方法時,雖然方法名字不同,但是呼叫的還是原來的方法,需要了解原有方法的具體細節
  • 裝飾者模式中,新增一個方法可以不用考慮原有方法的實現細節,在原封不動的保持原有方法的前提下,新增我們自己的方法

學習中發現的問題:在介面卡模式中,書中寫到

window.A = A = jQuery;

這個連等式是怎麼執行的呢?下面這個程式執行結果是什麼呢?

var a = { n: 1 };
a.x = a = { n: 2 };
console.log(a.x); // 輸出?

上面這個問題也很有趣,它輸出的答案是undefined,為什麼呢?因為在開始執行的時候,它初始化了一個a.x的變數,並且它的值為undefined,且這個a的值指向的是原來的{ n: 1},而賦值號(=)是從右到左執行,因此此時右邊的a = {n: 2},a指向了一個新的地址,該地址裡面儲存的值為{ n: 2},然後a.x又被賦了一個值為{n: 2},注意此a非彼a,我們可以用一箇中間變數看一下:
image

參考:
《Javascript 設計模式》 - 張榮銘
JS 介面卡模式

相關文章