JavaScript中的14種設(shè)計模式

本文源于本人關(guān)于《JavaScript設(shè)計模式與開發(fā)實踐》(曾探著)的閱讀總結(jié)。想詳細(xì)了解具體內(nèi)容建議閱讀該書。

1. 策略模式:

定義:定義一系列的算法,把他們一個個封裝起來,并且使他們可以相互替換。

前端中的利用:表單驗證(不同的表單有不同的驗證方式)

一個簡單的例子:公司發(fā)獎金根據(jù)每個人的績效不同來發(fā)不同的獎金,不同的績效,獎金有不同的計算方式。 我們可以用if-else,判斷每個人的績效是什么,從而采用不同的計算方式。但是如果又增加了一個種績效水平,那么我們又得增加if-else分支,這明顯是違反開放-封閉原則的。

核心思想:創(chuàng)建一個策略組,每次有新的績效計算方法則直接加入該組里,不會變動其他代碼。 調(diào)用時,傳入績效字符串,從而采用調(diào)用屬性的方法訪問到正確策略,并調(diào)用該策略。

利用策略模式構(gòu)建獎金發(fā)放:

var strategies = {
  "S": function(salary) {
    return salary * 4;
  },
  "A": function(salary) {
    return salary * 3;
  },
  "B": function(salary) {
    return salary * 2;
  }
}

function calculateBonus(level, salary) {
  return strategies[level](salary);
}

console.log(calculateBonus('A', 13333));

2. 代理模式:

定義:提供一個代用品或占位符,以便控制對它的訪問。

前端中的利用:圖片預(yù)加載(loading圖片)、緩存代理

核心思想:對象A訪問對象B,創(chuàng)建一個對象C,控制對象A對對象B的訪問,從而達(dá)到某種目的。 或者A進(jìn)行某個行為,創(chuàng)建一個對象C控制A進(jìn)行的這個行為。

圖片預(yù)加載:

var myImage = (function () {
      var imgNode = document.createElement('img');
      document.body.appendChild(imgNode);

      return {
        setSrc: function (src) {
          imgNode.src = src;
        }
      }
    })()

它返回了一個對象,擁有普通的圖片加載功能,但是這個功能有一個弊端,網(wǎng)絡(luò)環(huán)境差,圖片遲遲沒有完全加載完成時,會產(chǎn)生一個白框,我們希望這個時候有一個loading的動畫。

var proxyImage = (function () {
      var img = new Image();
      img.onload = function () {
        myImage.setSrc(this.src);
      }
      return {
        setSrc: function (src) {
          myImage.setSrc('./屏幕快照 2017-09-19 上午10.15.58.png');
          img.src = src;
        }
      }
    })()

現(xiàn)在創(chuàng)建了一個代理,我們想要加載圖片時,并不直接調(diào)用圖片加載對象,而是調(diào)用這個代理函數(shù),達(dá)到有l(wèi)oading動畫的目的。

它先把imgNode設(shè)置為loading動畫的gif圖片,然后創(chuàng)建了一個Image對象,等傳入的真實圖片鏈接,圖片加載完成后,再用真實圖片替換掉loading動畫gif。

當(dāng)你已經(jīng)寫完了某個函數(shù),但是某時希望這個函數(shù)的行為有其他效果時,你就可以寫一個代理達(dá)到你的目的。

3. 迭代器模式:

定義:提供一種方法順序訪問一個聚合對象中的各個元素。

前端中的利用:循環(huán)

很多語言都內(nèi)置了迭代器,我們很多時候不認(rèn)為他是一種設(shè)計模式。
這里我們說一下外部迭代器:
  • 必須顯式地請求迭代下一個元素。
  • 增加了一些調(diào)用的復(fù)雜性,但是更為靈活,我們可以手工控制迭代過程和順序。
var Iterator = function(obj) {
  var current = 0;

  var next = function() {
    current += 1;
  };

  var isDone = function() {
    return current >= obj.length;
  };

  var getCurrItem = function() {
    return obj[current];
  };

  return {
    next: next,
    isDone: isDone,
    getCurrItem: getCurrItem,
    length: obj.length
  }
};

var compare = function(iterator1, iterator2) {
  if(iterator1.length!==iterator2.length) {
    console.log('不相等');
  }
  while(!iterator1.isDone() && !iterator2.isDone()){
    if(iterator1.getCurrItem() !== iterator2.getCurrItem()){
      console.log('不相等');
    }
    iterator1.next();
    iterator2.next();
  }
  console.log('相等');
}

compare(Iterator([1, 2, 3]), Iterator([1, 2, 3])); // 相等

4. 命令模式

定義:指的是一個執(zhí)行某些特定事情的指令。
使用場景:有時候需要向某些對象發(fā)送請求,但是并不知道請求的接收者是誰,也不知道被請求的操作是什么。

前端中的利用:菜單程序,按鍵動畫

背景:前端協(xié)作中,有人負(fù)責(zé)寫界面,有人負(fù)責(zé)開發(fā)按鈕之類的具體功能。我們希望寫界面的人直接調(diào)用命令就好,不用關(guān)心,具體實現(xiàn)。

按鍵動畫(每個按鍵代表不同的動畫):

命令創(chuàng)建函數(shù):

var makeCommand = function (receiver, state) {
      return function () {
        receiver[state]();
      }
    }

receiver代表具體動畫的執(zhí)行函數(shù)。

界面同學(xué)只負(fù)責(zé):

document.onkeypress = function (ev) {
      var keyCode = ev.keyCode,
        command = makeCommand(Ryu, commands[keyCode]);

      if (command) {
        command();
      }
    };

而實現(xiàn)操作的同學(xué)寫具體實現(xiàn),和不同按鍵所對應(yīng)的指令名稱:

var Ryu = {
      attack: function () {
        console.log('攻擊');
      },
      defense: function () {
        console.log('防御');
      },
      jump: function () {
        console.log('跳躍');
      },
      crouch: function () {
        console.log('下蹲');
      }
    };
    
var commands = {
      '119': 'jump', // W
      '115': 'crouch', // S
      '97': 'defense', // A 
      '100': 'attack' // D
    }
目前我們的命令模式,只有一個設(shè)置命令,但是這其實完全可以寫成一個對象,包含,記錄命令調(diào)用過程,包含取消命令,等等。

5. 組合模式:

定義:將對象組合成樹形結(jié)果,以表示“部分-整體”的層次結(jié)果。除了用來表示樹形結(jié)構(gòu)之外,組合模式令一個好處是通過對象的多態(tài)性表現(xiàn),使得用戶對單個對象和組合對象的使用具有一致性。

前端中的利用:文件夾掃描

核心思想:樹形結(jié)構(gòu),分為葉子對象和非葉子對象, 葉子對象和非葉子對象擁有一組同樣的方法屬性, 調(diào)用非葉子對象的方法后,該對象和該對象下的所有對象都會執(zhí)行該方法。

文件掃描:當(dāng)我們負(fù)責(zé)粘貼時,我們不會關(guān)心我們選中的是文件還是文件夾,我們都會一并進(jìn)行負(fù)責(zé)粘貼。

文件夾:

var Folder =  function(name) {
  this.name = name;
  this.files = [];
};

Folder.prototype.add = function(file) {
  this.files.push(file);
}

Folder.prototype.scan = function() {
  console.log('開始掃描文件夾:' + this.name);
  for(var i = 0, file; file = this.files[i++];) {
    file.scan();
  }
}

文件:

var File = function(name){
  this.name = name;
}

File.prototype.add = function() {
  throw new Error('文件下面不能再添加文件');
}

File.prototype.scan = function() {
  console.log('開始掃描文件:' + this.name);
}

組成文件結(jié)構(gòu):

var folder = new Folder('學(xué)習(xí)資料');
var folder1 = new Folder('JS');
var folder2 = new Folder('JQ');

var file = new File('學(xué)習(xí)資料');
var file1 = new File('學(xué)習(xí)資料1');
var file2 = new File('學(xué)習(xí)資料2');
var file3 = new File('學(xué)習(xí)資料3');

folder.add(file);
folder.add(file1);

folder1.add(file2);
folder2.add(file3);

var rootFolder = new Folder('root');

rootFolder.add(folder);
rootFolder.add(folder1);
rootFolder.add(folder2);

掃描:

rootFolder.scan();

// 輸出:
// 開始掃描文件夾:root

// 開始掃描文件夾:學(xué)習(xí)資料
// 開始掃描文件:學(xué)習(xí)資料
// 開始掃描文件:學(xué)習(xí)資料1

// 開始掃描文件夾:JS
// 開始掃描文件:學(xué)習(xí)資料2

// 開始掃描文件夾:JQ
// 開始掃描文件:學(xué)習(xí)資料3

6. 模版方法模式

定義:由兩部分結(jié)構(gòu)組成,第一部分就是抽象父類,第二部分就是具體實現(xiàn)的子類。通常父類中封裝了子類的算法框架,包括實現(xiàn)一些公共方法及封裝子類中所有方法的執(zhí)行順序。

使用場景:假如我們有一些平行的子類,各個子類之間有一些相同的行為,也有一些不同的行為。如果相同和不同的行為都混合在各個子類的實現(xiàn)中,說明這些相同的行為會在各個子類中重復(fù)出現(xiàn)。

模版方法模式所做的事情:我們不必重寫一個子類,如果屬于同一類型就可以直接繼承抽象類,然后把變化的邏輯封裝到子類中即可,不需要改動其他子類和父類。

例子:

  • 泡咖啡:
    • 把水煮沸
    • 把沸水沖泡咖啡
    • 把咖啡倒進(jìn)杯子
    • 加糖和牛奶
  • 泡茶:
    • 把水煮沸
    • 用沸水浸泡茶葉
    • 把水倒進(jìn)杯子里
    • 加檸檬

然后進(jìn)行抽象:

  • 把水煮沸
  • 用沸水沖泡飲料
  • 把飲料倒進(jìn)杯子里
  • 加調(diào)料

抽象類代碼:

var Beverage = function() {};

Beverage.prototype.boilWater = function(){
  console.log('把水煮沸');
};

// 空方法,應(yīng)該由子類來重寫
Beverage.prototype.brew = function() {
  throw new Error('子類必須重寫brew方法');
};

// 空方法,應(yīng)該由子類來重寫
Beverage.prototype.pourInCup = function() {
  throw new Error('子類必須重寫pourInCup方法');
};

// 空方法,應(yīng)該由子類來重寫
Beverage.prototype.addCondiments = function() {
  throw new Error('子類必須重寫addCondiments方法');
};

Beverage.prototype.init = function() {
  this.boilWater();
  this.brew();
  this.pourInCup();
  this.addCondiments();
};

因為JS沒有繼承機(jī)制,但是子類如果繼承了父類沒有重寫方法,編輯器不會提醒,那么執(zhí)行的時候會報錯,為了防止程序員漏重寫方法,故在需要重寫的方法中拋出異常。

coffee:

var Coffee = function() {};

Coffee.prototype = new Beverage();

Coffee.prototype.brew = function() {
  console.log('用水沖泡咖啡');
};

Coffee.prototype.pourInCup = function() {
  console.log('把咖啡倒進(jìn)杯子里');
};

Coffee.prototype.addCondiments = function() {
  console.log('加糖和牛奶');
};

var coffee = new Coffee();
coffee.init();

tea:

var Tea = function() {};

Tea.prototype = new Beverage();

Tea.prototype.brew = function() {
  console.log('用水浸泡茶');
};

Tea.prototype.pourInCup = function() {
  console.log('把茶水倒進(jìn)杯子里');
};

Tea.prototype.addCondiments = function() {
  console.log('加檸檬');
};

var tea = new Tea();
tea.init();

# 7. 單例模式

定義:保證一個類僅有一個實例,并提供一個訪問它的全局訪問點。

前端中的利用:登錄框,彈層

核心思想:利用一個變量保存第一次創(chuàng)建的結(jié)果(對象中的某個屬性或者閉包能訪問的變量), 再次創(chuàng)建時,該變量不為空,直接返回改對象。

類:

var Singleton = function(name) {
  this.name = name;
  this.instance = null;
}

Singleton.prototype.getName = function() {
  console.log(this.name);
}

Singleton.getInstance = function(name) {
  if(!this.instance) {
    this.instance = new Singleton(name);
  }
  return this.instance;
}

var a = Singleton.getInstance('123');
var b = Singleton.getInstance('321');
console.log(a === b); // true

Singleton.getInstance是靜態(tài)方法。

通用的惰性單例:

function getSingleton(fn) {
  var instance = null;
  return function() {
    return instance || (instance = fn.apply(this, arguments) );
  }
}

var createObj = function(name) {
  return {name: name};
}

var getSingleObj = getSingleton(createObj);

console.log(getSingleObj('123') === getSingleObj('321'));

fn為實例創(chuàng)建函數(shù),用通用的單例模式包裝之后,他就變成了單例創(chuàng)建函數(shù)。

# 8. 發(fā)布-訂閱模式

定義:也可以叫觀察者模式,它定義對象間的一種一對多的依賴關(guān)系,當(dāng)一個對象的狀態(tài)發(fā)生改變時,所有依賴于它的對象都將得到通知。

前端中的利用:Vue雙向綁定、事件監(jiān)聽函數(shù)。

一個例子-售樓處:

  • 很多人登記了信息,當(dāng)有樓盤的時候,將會通知所有人前來購買。
  • 但是每個人的經(jīng)濟(jì)能力有限,有些人關(guān)注的是別墅樓盤,有些人關(guān)注的是小戶樓盤,所以每個行為訂閱的內(nèi)容也不一樣。
  • 有些人嫌這家售樓處的服務(wù)態(tài)度不好,想取消訂閱。
通用實現(xiàn):創(chuàng)建一個訂閱-發(fā)布對象,該對象擁有一個客戶組對象,擁有訂閱方法,發(fā)布方法,取消方法。
  • 當(dāng)訂閱時:將客戶訂閱的內(nèi)容,和執(zhí)行方法存在客戶組對象中:
listen = function (key, fn) {
    if (!cacheList[key]) {
      cacheList[key] = [];
    }
    cacheList[key].push(fn);
  };
  • 取消訂閱時:
remove = function (key, fn) {
    var fns = cacheList[key];
    if (!fns) return false;
    // 如果只傳了key 代表取消該key下所有客戶
    if (!fn) {
      fns && (fns.length = 0);
    } else {
      for (var i = fns.length - 1; i >= 0; i--) {
        if (fns[i] === fn) {
          fns.splice(i, 1);
        }
      }
    }
  };
  • 發(fā)布:
trigger = function () {
    var key = Array.prototype.shift.call(arguments),
      args = arguments,
      fns = cacheList[key];

    if (!fns || fns.length === 0) return false;

    for (var i = 0, fn; fn = fns[i++];) {
      fn.apply(this, args);
    }
  }
其實僅僅只有一個客戶組時遠(yuǎn)遠(yuǎn)不夠的,更應(yīng)該有創(chuàng)建命名空間的功能,詳見《JavaScript設(shè)計模式與實踐》8.11。

# 9. 享元模式

定義:享元模式是一種用于性能優(yōu)化的模式,核心運用共享技術(shù)來支持大量細(xì)粒度的對象。

例子:我們有50件男士內(nèi)衣,和50件女士內(nèi)衣,我們需要模特穿上拍照。 我們有兩種可能性:

  • 為50件男士內(nèi)衣找50個男模特分別拍照 ,為50件女士內(nèi)衣找50個女模特分別拍照。
  • 找一個男模特,和一個女模特,分別穿50次照相。(享元模式)

這個便是享元模式的模型,目的在于減少共享對象的數(shù)量,我們需要將對象分為內(nèi)部狀態(tài)和外部狀態(tài):

  • 內(nèi)部狀態(tài)存在于對象內(nèi)部
  • 內(nèi)部狀態(tài)可以共享
  • 內(nèi)部狀態(tài)獨立與場景,通常不會改變。
  • 外部狀態(tài)決定于場景,根據(jù)場景的變化而改變。

上面的例子中,性別是內(nèi)部狀態(tài),內(nèi)衣是外部狀態(tài),通過區(qū)分這兩種狀態(tài)來減少系統(tǒng)的對象數(shù)量。

前端中的利用:
文件上傳:用戶選中文件之后,掃碼文件后,為每個文件創(chuàng)建一個upload對象,每個upload對象有一個上傳類型(插件上傳,F(xiàn)lash上傳等,不同文件可能適合不同的上傳方式),但是如果用戶一次性選擇的文件太多,則會出現(xiàn)對象過多,對象爆炸。

我們利用以上的方法,分離出外部狀態(tài)和內(nèi)部狀態(tài)。 每個共享對象不變的應(yīng)該是它的上傳類型(內(nèi)部狀態(tài)),而改變的是每個上傳對象的此時此刻擁有的文件,不同的文件就是外部狀態(tài)。

創(chuàng)建upload對象:

var Upload = function (uploadType) {
      this.uploadType = uploadType;
    };

    Upload.prototype.delFile = function (id) {
      uploadManager.setExternalState(id, this);

      if (this.fileSize < 3000) {
        return this.dom.parentNode.removeChild(this.dom);
      }

      if (window.confirm('確定要刪除該文件嗎?' + this.fileName)) {
        return this.dom.parentNode.removeChild(this.dom);
      }
    }

每次要刪除文件的時候,將這個共享對象指向觸發(fā)點擊函數(shù)的文件,執(zhí)行刪除該文件,對象仍然保留。

創(chuàng)建不同內(nèi)部狀態(tài)的對象(被共享的不同上傳類型的對象):

var UploadFactory = (function () {
      var createdFlyWeightObjs = {};

      return {
        create: function (uploadType) {
          if (createdFlyWeightObjs[uploadType]) {
            return createdFlyWeightObjs[uploadType];
          }

          return createdFlyWeightObjs[uploadType] = new Upload(uploadType);
        }
      }
    })()

定義了一個工廠模式來創(chuàng)建upload對象,如果某種內(nèi)部狀態(tài)對應(yīng)的共享狀態(tài)已經(jīng)被創(chuàng)建過,那么直接返回這個對象,否則創(chuàng)建一個新的對象。

管理器封裝外部狀態(tài):

var uploadManager = (function () {
      var uploadDatabase = {};

      return {
        add: function (id, uploadType, fileName, fileSize) {
          var flyWeightObj = UploadFactory.create(uploadType);

          var dom = document.createElement('div');
          dom.innerHTML = '<span>文件名稱:' + fileName + ',文件大?。? + fileSize + '</span>' +
            '<button class="delFile">刪除</button>';

          dom.querySelector('.delFile').onclick = function () {
            flyWeightObj.delFile(id);
          }

          document.body.appendChild(dom);

          uploadDatabase[id] = {
            fileName, fileSize, dom
          };

          return flyWeightObj;
        },
        setExternalState: function (id, flyWeightObj) {
          var uploadData = uploadDatabase[id];
          for (var i in uploadData) {
            flyWeightObj[i] = uploadData[i];
          }
        }
      }
    })()

uploadManager對象負(fù)責(zé)像UploadFactory提交創(chuàng)建對象的請求,并用一個uploadDatabase對象保存upload對象的所有外部狀態(tài)。

上傳函數(shù):

var id = 0;
    window.startUpload = function (uploadType, files) {
      for (var i = 0, file; file = files[i++];) {
        var uploadObj = uploadManager.add(++id, uploadType, file.fileName, file.fileSize);
      }
    }

    startUpload('plugin', [
      {
        fileName: '1.txt',
        fileSize: 1000,
      },
      {
        fileName: '2.txt',
        fileSize: 2000,
      },
      {
        fileName: '3.txt',
        fileSize: 3000,
      }
    ]);

    startUpload('Flash', [
      {
        fileName: '4.txt',
        fileSize: 4000,
      },
      {
        fileName: '5.txt',
        fileSize: 5000,
      },
      {
        fileName: '6.txt',
        fileSize: 6000,
      }
    ]);

現(xiàn)在不管上傳6個文件,還是2000個文件,都只會創(chuàng)建2個對象。

核心思想:
  • 創(chuàng)建能共享的對象,每個不同的能共享的對象區(qū)別在于內(nèi)部狀態(tài)的不同(uploadType)。
  • 每個共享的對象依然加上自己的操作,但是在執(zhí)行操作之前,需要將共享對象指向當(dāng)前外部狀態(tài)(文件)。
  • 創(chuàng)建一個工廠,能夠創(chuàng)建不同內(nèi)部狀態(tài)都共享對象,如果該種內(nèi)部狀態(tài)的共享對象已經(jīng)存在,則直接返回。
  • 創(chuàng)建一個外部狀態(tài)管理對象,包含一個數(shù)據(jù)庫對象存儲不同外部狀態(tài),包含一個添加函數(shù),和指向函數(shù)(共享對象指向外部狀態(tài))。

# 10. 責(zé)任鏈模式

定義:使多個對象都有機(jī)會處理請求,從而避免請求的發(fā)送者和接收者之間的耦合關(guān)系,將這些對象連成一條鏈,并沿著該鏈傳遞該請求,直到有一個對象處理它為止。

例子:高峰期公交車,我們不能直接把錢遞給售票員,直接給離得比較近的一個人,一直傳遞下去,最終會到售票員手里。

前端中的利用:
電商網(wǎng)站不同用戶種類的下單策略:

  • orderType1用戶:已經(jīng)支付500元,得到100元優(yōu)惠券;未支付500,降級到普通用戶購買界面。
  • orderType2用戶:已經(jīng)支付200元,得到50元優(yōu)惠券;未支付200,降級到普通用戶購買界面。
  • orderType3用戶:普通購買。
  • 庫存限制,針對code3。

新手寫法:根據(jù)orderType,isPay,stock來寫if-else分支來進(jìn)行具體操作。
責(zé)任鏈模式寫法:

分別寫order500、order200、orderNormal的函數(shù),如果滿足條件則執(zhí)行,不滿足條件則返回一個字段表示交給下一個節(jié)點執(zhí)行:

var order500 = function(orderType, pay, stock) {
  if(orderType === 1 && pay === true) {
    console.log('500元訂金預(yù)購,得到100優(yōu)惠券');
  } else {
    return 'nextSuccessor';
  }
};

var order200 = function(orderType, pay, stock) {
  if(orderType === 2 && pay === true) {
    console.log('200元訂金預(yù)購,得到50優(yōu)惠券');
  } else {
    return 'nextSuccessor';
  }
};

var orderNormal = function(orderType, pay, stock) {
  if(stock > 0) {
    console.log('普通購買,無優(yōu)惠券');
  } else {
    console.log('手機(jī)庫存不足');
  }
}

編寫責(zé)任鏈控制函數(shù):

var Chain = function(fn) {
  this.fn = fn;
  this.successor = null;
}

Chain.prototype.setNextSuccessor = function(successor){
  return this.successor = successor;
}

Chain.prototype.passRequest = function(){
// 執(zhí)行該節(jié)點的具體方法
  var ret = this.fn.apply(this, arguments);

// 如果執(zhí)行結(jié)果未不滿足,則調(diào)用下一個節(jié)點的執(zhí)行方法
  if(ret === 'nextSuccessor') {
    return this.successor && this.successor.passRequest.apply(this.successor, arguments);
  }

  return ret;
};

類似于鏈表,每個節(jié)點都保存著下一個節(jié)點,并含有一個該節(jié)點的執(zhí)行函數(shù),和設(shè)置下一個節(jié)點的函數(shù)。

// 將每個具體執(zhí)行函數(shù)封裝為一個責(zé)任鏈節(jié)點
var chainOrder500 = new Chain(order500);
var chainOrder200 = new Chain(order200);
var chainOrderNormal = new Chain(orderNormal);

// 設(shè)置每個節(jié)點的下一個節(jié)點
chainOrder500.setNextSuccessor(chainOrder200);
chainOrder200.setNextSuccessor(chainOrderNormal);

chainOrder500.passRequest(1, true, 500);
chainOrder500.passRequest(2, true, 500);
chainOrder500.passRequest(3, true, 500);
chainOrder500.passRequest(1, false, 0);

這樣只需要第一個節(jié)點執(zhí)行,如果不滿足則請求自動交付給下一個節(jié)點,直到到達(dá)節(jié)點尾部。

如果未來還有更多情況,比如有交了50定金的,可以給10元的優(yōu)惠券,這樣的情況可以直接添加節(jié)點,改變節(jié)點順序,不會對已有的方法做更改。

本例子可以用Promise做該寫,如果成功則Promise.resolve()否則Promise.reject()
還可以使用AOP的方式Function.prototype.after做改寫。

核心思想:將具體執(zhí)行方法包裝為一個個責(zé)任鏈子節(jié)點,執(zhí)行第一個節(jié)點,如果情況滿足則執(zhí)行,不滿足則調(diào)用下一個節(jié)點的執(zhí)行方法。

# 11. 中介者模式

定義:將行為分布到各個對象中,把對象劃分為更小的細(xì)粒度,但是由于細(xì)粒度之間對象的聯(lián)系激增,又有可能反過來降低它們的可復(fù)用性。中介者模式使網(wǎng)狀的多對多關(guān)系變成了相對簡單的一對多關(guān)系。

例子:

  • 機(jī)場指揮中心:每架飛機(jī)不可能和其他所有飛機(jī)逐一聯(lián)系,來確定是否能起飛,是否能滑動,這樣的聯(lián)系都交給了指揮中心來做。每架飛機(jī)只需要聯(lián)系中介者即可。
  • 博彩公司算賠率:和機(jī)場指揮中心是一樣的道理。

前端的利用:

商品購買:通常商品購買會有選擇框,輸入框,還有信息提示框,我們需要選擇或者輸入時,信息都能有正確的提示,一個辦法是強(qiáng)耦合,在選擇框變動后,去修改提示框。如果添加新的選擇框,代碼變動會更大。

引入中介者:具體處理邏輯交給中介者處理,其他選擇框只與中介者交互。

html:

<body>
  選擇顏色:
  <select name="" id="colorSelect">
    <option value="">請選擇</option>
    <option value="red">紅色</option>
    <option value="blue">藍(lán)色</option>
  </select>
  <br> 選擇內(nèi)存:

  <select name="" id="memorySelect">
    <option value="">請選擇</option>
    <option value="32G">32g</option>
    <option value="16G">16g</option>
  </select>
  <br>
  <br> 輸入購買數(shù)量:

  <input type="text" id="numberInput">
  <br>
  <br> 您選擇了顏色:

  <div id="colorInfo"></div>
  <br> 您選擇了內(nèi)存:
  <div id="memoryInfo"></div>
  <br> 您輸入了數(shù)量:
  <div id="numberInfo"></div>
  <br>

  <button id="nextBtn" disabled="true">請選擇手機(jī)顏色和購買數(shù)量</button>
  </body>

獲取各種框dom節(jié)點:

var colorSelect = document.getElementById('colorSelect');
    var memorySelect = document.getElementById('memorySelect');
    var numberInput = document.getElementById('numberInput');

    var colorInfo = document.getElementById('colorInfo');
    var memoryInfo = document.getElementById('memoryInfo');
    var numberInfo = document.getElementById('numberInfo');

    var nextBtn = document.getElementById('nextBtn');

編寫中介者:

var mediator = (function () {
      return {
        changed: function (obj) {
          var color = colorSelect.value,
            memory = memorySelect.value,
            number = numberInput.value,
            stock = goods[color + '|' + memory];

          if (obj === colorSelect) {
            colorInfo.innerHTML = color;
          } else if (obj === memorySelect) {
            memoryInfo.innerHTML = memory;
          } else if (obj === numberInput) {
            numberInfo.innerHTML = number;
          }

          if (!color) {
            nextBtn.disabled = true;
            nextBtn.innerHTML = '請選擇手機(jī)顏色';
            return;
          }

          if (!memory) {
            nextBtn.disabled = true;
            nextBtn.innerHTML = '請選擇內(nèi)存大小';
            return;
          }

          if (!(Number.isInteger(number - 0) && number > 0)) {
            nextBtn.disabled = true;
            nextBtn.innerHTML = '請輸入正確的購買數(shù)量';
            return;
          }

          nextBtn.disabled = false;
          nextBtn.innerHTML = '放入購物車';
        }
      }
    })();

變動只與中介者交互:

colorSelect.onchange = function() {
      mediator.changed(this);
    };

    memorySelect.onchange = function() {
      mediator.changed(this);
    };

    numberInput.oninput = function() {
      mediator.changed(this);
    };

12. 裝飾者模式

定義:在不改變對象自身的基礎(chǔ)上,在程序運行期間給對象動態(tài)添加職責(zé)。(包裝器)

例子:

  • 給自行車擴(kuò)展,給4種自行車擴(kuò)展3個配件,在繼承的基礎(chǔ)上需要建立出12個子類。
  • 但是動態(tài)的把這些動態(tài)添加到自行車上則住需要額外3個類(3個配件)。

裝飾者:

// 保存引用的裝飾者模式

var plane = {
  fire: function() {
    console.log('發(fā)射普通子彈');
  }
}

var missileDecorator = function() {
  console.log('發(fā)射導(dǎo)彈');
}

var atomDecorator = function() {
  console.log('發(fā)射原子彈');
}

var fire1 = plane.fire;

plane.fire = function() {
  fire1();
  missileDecorator(); 
}

var fire2 = plane.fire;

plane.fire = function() {
  fire2();
  atomDecorator();
}

plane.fire();

AtomDecorator 包裝 MissileDecorator 包裝 Plane。 這樣寫完全符合開發(fā)-封閉原則,在添加新功能的時候沒有去改動別人點方法,但是不好的就是,如果包裝點層次太多,中間變量就太多了。還會遇見this劫持的問題:

var _getEleById = document.getElementById;

document.getElementById = function(id) {
    alert(1);
    return _getElementById(id);
}

this被劫持了。

解決以上兩個問題的最好方法就上AOP函數(shù):

Function.prototype.before = function (fn) {
  var _self = this; // 保存原函數(shù)的引用
  return function () { // 返回了包含原函數(shù)和新函數(shù)的代理函數(shù)
    fn.apply(this, arguments);
    return _self.apply(this, arguments); // 執(zhí)行原函數(shù)
  }
}

Function.prototype.after = function (fn) {
  var _self = this; // 保存原函數(shù)的引用
  return function () {
    var ret = _self.apply(this, arguments);
    fn.apply(this, arguments);
    return ret;
  }
}
  • 第一個:返回在函數(shù)之前執(zhí)行
  • 第二個:返回在函數(shù)之后執(zhí)行
前端的利用:數(shù)據(jù)上報這樣和業(yè)務(wù)邏輯無關(guān)的函數(shù)都可以利用包裝者進(jìn)行包裝。

# 13. 狀態(tài)模式:

定義:區(qū)分事物的內(nèi)部狀態(tài),事物的內(nèi)部狀態(tài)的改變往往會帶來事物行為的改變。

例子:

  • 通常的電燈,只有一個按鈕,按下按鈕;
    • 如果電燈是關(guān)的:那么開燈
    • 如果點燈是開著的:那么關(guān)燈

這里換成代碼,就是簡單的if-else,但是如果再復(fù)雜一點呢:新添加一個按鈕,如果這個按鈕按下,那么點燈是弱-強(qiáng)-關(guān)模式;否則是開-關(guān)模式。

這個時候你已經(jīng)開始發(fā)現(xiàn)if-else代碼的缺點了:

  • 每次燈擴(kuò)展,都需要修改內(nèi)部代碼,違反開放-封閉原則
  • 所有與行為有關(guān)的事情都在一個函數(shù)里
  • 狀態(tài)切換不明顯,僅僅只有一個字段的改變
  • if-else太多太繁雜。

狀態(tài)模式下的點燈程序(假設(shè)這里只有一個按鈕,切換開關(guān)):

我們第一步創(chuàng)建點燈(富含狀態(tài)的這個對象):

var Light = function () {
    this.currState = FSM.off;
    this.button = null;
};

this.currState代表的是不同的狀態(tài):這里的狀態(tài)用對象來表示,開關(guān)兩個狀態(tài)就是兩個對象:

var FSM = {
        off: {
        buttonWasPressed: function () {
          console.log('關(guān)燈');
          this.button.innerHTML = '下一次按我是開燈';
          this.currState = FSM.on;
        }
        },
        on: {
        buttonWasPressed: function() {
          console.log('開燈');
          this.button.innerHTML = '下一次按我是關(guān)燈';
          this.currState = FSM.off;
        }
        }
    }

接下來編寫初始化電燈函數(shù):

Light.prototype.init = function () {
      var button = document.createElement('button'),
        self = this;

      button.innerHTML = '已關(guān)燈';
      this.button = document.body.appendChild(button);

      this.button.onclick = function () {
        self.currState.buttonWasPressed.call(self);
      }
    };

給按鈕綁定事件,按鈕觸發(fā)時,觸發(fā)當(dāng)前狀態(tài)對象的更替事件。(執(zhí)行外部行為,切換當(dāng)前的狀態(tài))

總結(jié):

狀態(tài)模式編寫思路:

  • 設(shè)計富含狀態(tài)的對象(主對象):
    • 編寫各種狀態(tài)下的行為
    • 狀態(tài)屬性
    • 初始化:綁定按鈕,在該狀態(tài)下的狀態(tài)切換
  • 設(shè)計各種狀態(tài)對象:
    • 接收主對象的this
    • 按鈕觸發(fā)時,改狀態(tài)利用this,多狀態(tài)則編寫多個不同觸發(fā)函數(shù)
      切換主對象狀態(tài)、調(diào)用主狀態(tài)行為

與策略模式的區(qū)別:

  • 策略模式中每個策略類相互平等沒有關(guān)系
  • 狀態(tài)模式中狀態(tài)類之間的關(guān)系是提前確定好的。

14. 適配器模式

定義:解決兩個軟件實體之間的接口不兼容的問題。

例子:插頭轉(zhuǎn)換器,轉(zhuǎn)換不同地區(qū)的電壓問題。

前端中:

  • 地圖渲染:
    假如地圖渲染的函數(shù)是這樣的:
var renderMap = function( map ) {
  if(map.show instanceof Function) {
    map.show();
  }
}

地圖:

var googleMap = {
  show() {
    console.log('google地圖開始渲染');
  }
}

var baiduMap = {
  display() {
    console.log('baidu地圖開始渲染');
  }
}

我們可以只帶googleMap沒有問題,但是baiduMap提供的接口名明顯不一致,如果去改renderMap函數(shù)違反了開放封閉原則。

那么現(xiàn)在我們只能用適配器包裝一下baiduMap:

var baiduMapAdapter = {
  show() {
    return baiduMap.display();
  }
}

思路:封裝與其他不同的方法或者對象,而不要去改動原有的函數(shù)。

其他例子:xml與json格式適配,json與對象格式的轉(zhuǎn)變等。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,176評論 25 708
  • 工廠模式類似于現(xiàn)實生活中的工廠可以產(chǎn)生大量相似的商品,去做同樣的事情,實現(xiàn)同樣的效果;這時候需要使用工廠模式。簡單...
    舟漁行舟閱讀 8,131評論 2 17
  • 把配置導(dǎo)入一臺硬件配置一模一樣的防火墻,上電,接線。 結(jié)果,同一個出口上的兩個不同的ip地址,一個通,一個不通。 ...
    白堊紀(jì)動物閱讀 238評論 0 0
  • 晨驅(qū)五百里,暮至黃河津。 大哉壯且闊,滾滾浮沉音。 欲發(fā)三秦志,便下齊魯心。 信韁由大宛,揚鞭即東臨!
    鯊魚病毒閱讀 318評論 0 2
  • 我喜歡旅游 喜歡那種在路上的感覺 不管是坐在哐切哐切的綠皮車上 還是坐在疾馳如風(fēng)的高鐵動車上 心里只要想著自己的手...
    不愛看書啊閱讀 383評論 0 0

友情鏈接更多精彩內(nèi)容