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沒有繼承機制,但是子類如果繼承了父類沒有重寫方法碉克,編輯器不會提醒凌唬,那么執(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é)任鏈模式

定義:使多個對象都有機會處理請求爬立,從而避免請求的發(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('手機庫存不足');
  }
}

編寫責(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)系。

例子:

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

前端的利用:

商品購買:通常商品購買會有選擇框,輸入框们何,還有信息提示框萄焦,我們需要選擇或者輸入時,信息都能有正確的提示冤竹,一個辦法是強耦合拂封,在選擇框變動后,去修改提示框鹦蠕。如果添加新的選擇框冒签,代碼變動會更大。

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

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">請選擇手機顏色和購買數(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 = '請選擇手機顏色';
            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ù)雜一點呢:新添加一個按鈕,如果這個按鈕按下始赎,那么點燈是弱-強-關(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)系作者
  • 序言:七十年代末床牧,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子遭贸,更是在濱河造成了極大的恐慌戈咳,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件壕吹,死亡現(xiàn)場離奇詭異著蛙,居然都是意外死亡,警方通過查閱死者的電腦和手機耳贬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門踏堡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人咒劲,你說我怎么就攤上這事顷蟆〗胗纾” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵帐偎,是天一觀的道長逐纬。 經(jīng)常有香客問我,道長削樊,這世上最難降的妖魔是什么豁生? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮漫贞,結(jié)果婚禮上甸箱,老公的妹妹穿的比我還像新娘。我一直安慰自己绕辖,他們只是感情好摇肌,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著仪际,像睡著了一般围小。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上树碱,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天肯适,我揣著相機與錄音,去河邊找鬼成榜。 笑死框舔,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的赎婚。 我是一名探鬼主播刘绣,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼挣输!你這毒婦竟也來了纬凤?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤撩嚼,失蹤者是張志新(化名)和其女友劉穎停士,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體完丽,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡恋技,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了逻族。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蜻底。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖瓷耙,靈堂內(nèi)的尸體忽然破棺而出朱躺,到底是詐尸還是另有隱情刁赖,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布长搀,位于F島的核電站宇弛,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏源请。R本人自食惡果不足惜枪芒,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望谁尸。 院中可真熱鬧舅踪,春花似錦、人聲如沸良蛮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽决瞳。三九已至货徙,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間皮胡,已是汗流浹背痴颊。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留屡贺,地道東北人蠢棱。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像甩栈,于是被迫代替她去往敵國和親泻仙。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容

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