理解JavaScript設(shè)計(jì)模式(上)

Every developer strives to write maintainable, readable, and reusable code.

很多開(kāi)發(fā)人員在學(xué)習(xí)設(shè)計(jì)模式的時(shí)候都特別的 被動(dòng),為了學(xué)習(xí)設(shè)計(jì)模式而學(xué)習(xí)骄噪,學(xué)完就忘記很少實(shí)踐在工作中岔乔,因此在談JavaScript設(shè)計(jì)模式之前挠日,首先我們要明確以下幾個(gè)問(wèn)題:

  1. Context: 什么情況下使用設(shè)計(jì)模式?
  2. Problem: 我們要解決的問(wèn)題是什么?
  3. Solution: 設(shè)計(jì)模式是怎么解決這個(gè)問(wèn)題的?
  4. Implementation: 要怎么實(shí)現(xiàn)?

帶著這些問(wèn)題,本文介紹了四種JavaScript開(kāi)發(fā)人員必知必會(huì)的設(shè)計(jì)模式治唤。

Module Design Pattern

模塊設(shè)計(jì)模式與 面向?qū)ο?object-oriented) 的語(yǔ)言非常相似贞间,可以將一個(gè)模塊看作是一個(gè)class, 其中優(yōu)點(diǎn)之一就是 封裝的特性粹懒,可以提高我們代碼的可維護(hù)性和易讀性,在模塊的外部無(wú)法操作模塊內(nèi)部的變量顷级,只能通過(guò)模塊提供的接口來(lái)訪(fǎng)問(wèn)凫乖。該設(shè)計(jì)模式是JavaScript最常見(jiàn)的設(shè)計(jì)模式之一,下面我們來(lái)實(shí)現(xiàn)該設(shè)計(jì)模式弓颈。

在JavaScript中實(shí)現(xiàn)“封裝”帽芽,我們可以用立即執(zhí)行函數(shù)(IIFE)創(chuàng)建一個(gè)閉包的結(jié)構(gòu)

(function() {
   // declare private variables and/or functions

    return {
      // declare public variables and/or functions

    }
})();

下面讓我們來(lái)完整這個(gè)設(shè)計(jì)模式。

var Exposer = (function() {
  var privateVariable = 10;

  var privateMethod = function() {
    console.log('Inside a private method!');
    privateVariable++;
  }

  var methodToExpose = function() {
    console.log('This is a method I want to expose!');
  }

  var otherMethodIWantToExpose = function() {
    privateMethod();
  }

  return {
      first: methodToExpose,
      second: otherMethodIWantToExpose
  };
})();

Exposer.first();        // Output: This is a method I want to expose!
Exposer.second();       // Output: Inside a private method!
Exposer.methodToExpose; // undefined 

上面的代碼我們定一個(gè)來(lái)一個(gè)模塊Exposer, 我們可以通過(guò)該模塊提供的接口firstsecond來(lái)操作這個(gè)模塊而無(wú)需關(guān)心其內(nèi)部實(shí)現(xiàn)翔冀,當(dāng)試圖直接訪(fǎng)問(wèn)其內(nèi)部變量的時(shí)候則是無(wú)效的嚣镜,得到的值是undefined。很多JavaScript庫(kù)中都可以看到該設(shè)計(jì)模式的影子橘蜜。

Singleton Design Pattern

單例模式,顧名思義只有一個(gè)實(shí)例付呕。該設(shè)計(jì)模式的用途也非常的廣泛并且實(shí)現(xiàn)起來(lái)非常簡(jiǎn)單计福。比如現(xiàn)在有一個(gè)辦公室,有五名員工公用一臺(tái)打印機(jī)徽职,那么我們就可以使用單例模式來(lái)處理這種情況象颖。

根據(jù)前面討論的模塊設(shè)計(jì)模式,我們先創(chuàng)建一個(gè)打印機(jī)模塊printer姆钉,該模塊提供一個(gè)外部接口讓五名使用者可以訪(fǎng)問(wèn)這臺(tái)打印機(jī)说订。為了確保這五個(gè)人用的是同一臺(tái)打印機(jī),單例模式實(shí)現(xiàn)的關(guān)鍵是: 當(dāng)有人第一次訪(fǎng)問(wèn)實(shí)例時(shí)創(chuàng)建打印機(jī)實(shí)例printerInstance潮瓶,并且保存起來(lái)陶冷。當(dāng)其他人再次訪(fǎng)問(wèn)該實(shí)例時(shí)直接返回這個(gè)保存起來(lái)的實(shí)例。

var printer = (function () {

    var printerInstance;

    function create() {
        return {
            turnOn: function turnOn() {
            console.log('working')
        }
      }
    }

    return {
        getInstance: function () {
            if (!printerInstance) {
                printerInstance = create();
            }
            return printerInstance;
        }
    }
})()

printer.getInstance().turnOn()     // output: working

從上面代碼我們可以看到printer模塊提供了一個(gè)唯一外部可以訪(fǎng)問(wèn)的接口getInstance毯辅,當(dāng)?shù)谝淮卧L(fǎng)問(wèn)該接口時(shí)埂伦,我們先判斷實(shí)例是否被創(chuàng)建,如果沒(méi)有創(chuàng)建則使用create()創(chuàng)建思恐,如果已經(jīng)創(chuàng)建則返回唯一的實(shí)例printerInstance沾谜。大家可以使用單例模式設(shè)計(jì)一個(gè)計(jì)數(shù)器練練手膊毁,定義一個(gè)計(jì)數(shù)器模塊,提供增加基跑、減少婚温、查看三個(gè)接口。

Observer Design Pattern

觀察者模式媳否,當(dāng)應(yīng)用的一個(gè)部分變動(dòng)時(shí)栅螟,其他部分也會(huì)更新。比如Model-View-Controller(MVC)架構(gòu)逆日,當(dāng)model變化的時(shí)候views也會(huì)更新嵌巷。在流行的Web前端Vue、Angular中也是通過(guò)觀察者模式來(lái)通知狀態(tài)變化的室抽。

實(shí)現(xiàn)一個(gè)觀察者模式至少要包含2個(gè)角色如下圖UML圖中所示: SubjectObserver對(duì)象 搪哪。

Observer Design Pattern

下面我們使用JavaScript來(lái)實(shí)現(xiàn)上圖的觀察者模式。
首先我們要定義一個(gè)Subject對(duì)象坪圾,包含以下方法:

  1. 注冊(cè)觀察者對(duì)象
  2. 卸載觀察者對(duì)象
  3. 通知觀察者對(duì)象

然后我們需要定義一個(gè)觀察者對(duì)象晓折,并且實(shí)現(xiàn)通知notify方法。也就是實(shí)現(xiàn)當(dāng)Subject對(duì)象通知觀察者對(duì)象時(shí)兽泄,觀察者對(duì)象要做什么漓概?

var Observer = function() {
  return {
    notify: function(index) {
      console.log("Observer " + index + " is notified!");
    }
  }
}

下面讓我們實(shí)現(xiàn)完整

var Subject = function() {
  var observers = [];

  return {
    subscribeObserver: function(observer) {
      observers.push(observer);
    },
    unsubscribeObserver: function(observer) {
      var index = observers.indexOf(observer);
      if(index > -1) {
        observers.splice(index, 1);
      }
    },
    notifyObserver: function(observer) {
      var index = observers.indexOf(observer);
      if(index > -1) {
        observers[index].notify(index);
      }
    },
    notifyAllObservers: function() {
      for(var i = 0; i < observers.length; i++){
        observers[i].notify(i);
      };
    }
  };
};

var Observer = function() {
  return {
    notify: function(index) {
      console.log("Observer " + index + " is notified!");
    }
  }
}

上面的代碼我們實(shí)現(xiàn)了Subject對(duì)象,在其內(nèi)部聲明了一個(gè)observers
數(shù)組用來(lái)存儲(chǔ)注冊(cè)的observer對(duì)象病梢。下面讓我們來(lái)使用這兩個(gè)對(duì)象

var subject = new Subject();

var observer1 = new Observer();
var observer2 = new Observer();
var observer3 = new Observer();
var observer4 = new Observer();

subject.subscribeObserver(observer1);
subject.subscribeObserver(observer2);
subject.subscribeObserver(observer3);
subject.subscribeObserver(observer4);

subject.notifyObserver(observer2); // Observer 2 is notified!

subject.notifyAllObservers();
// Observer 1 is notified!
// Observer 2 is notified!
// Observer 3 is notified!
// Observer 4 is notified!

以上我們實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的設(shè)計(jì)者模式胃珍,先使用subjectsubscribeObserver(observer)注冊(cè)以后要通知的觀察者對(duì)象,當(dāng)我們想通知注冊(cè)好的觀察者對(duì)象時(shí)蜓陌,只需要使用subject.notifyObserver(observer)即可

Constructor Pattern

在典型的面向?qū)ο笳Z(yǔ)言中觅彰,constructor 是一個(gè)特殊的初始化方法,在JavaScript中幾乎任何東西都是一個(gè)對(duì)象钮热,我們同樣也可以在JavaScript中使用constructor pattern 來(lái)初始化對(duì)象填抬。

不使用constructor pattern時(shí),我們通常是這樣初始化一個(gè)對(duì)象的:

var Pastry = {
  // initialize the pastry
  init: function (type, flavor, levels, price, occasion) {
    this.type = type;
    this.flavor = flavor;
    this.levels = levels;
    this.price = price;
    this.occasion = occasion;
  }
}

var cake = Object.create(Pastry);
cake.init("cake", "vanilla", 3, "$10", "birthday");

我們先在對(duì)象內(nèi)部添加一個(gè)init方法隧期,然后在初始化時(shí)我們使用Object.create(object)創(chuàng)建這個(gè)對(duì)象并調(diào)用其init方法飒责,下面讓我們像面向?qū)ο笳Z(yǔ)言一樣使用new關(guān)鍵字初始化一個(gè)對(duì)象通過(guò) constructor pattern

function Car(model, year, miles){
    this.model = model;
    this.year = year;
    this.miles = miles;
}

Car.prototype.toString = function(){
    return `${this.model} has done ${this.miles} miles`
}

var civic = new Car('Honda Civic', 2009, 20000);
var mondeo = new Car('Ford Mondeo', 2010, 5000);

civic.toString();

上面代碼我們用constructor pattern初始化了2個(gè)對(duì)象:civicmondeo仆潮,我們首先定義了一個(gè)函數(shù)Car并在其內(nèi)部使用關(guān)鍵字this設(shè)置了該對(duì)象所需要的參數(shù)宏蛉,此外我們還通過(guò)在Carprototype上添加toString來(lái)擴(kuò)展該對(duì)象。

Mixin Pattern

Mixin Pattern 實(shí)現(xiàn)了面向?qū)ο笾械?繼承, 繼承的好處可以提高代碼的復(fù)用性鸵闪,比如我們現(xiàn)在有以下一個(gè)對(duì)象

function Person(firstname, lastname){
  this.firstname = firstname;
  this.lastname = lastname;
}

var max = new Person('max', 'lee');

當(dāng)我們想創(chuàng)建一個(gè)新的對(duì)象檐晕,這個(gè)對(duì)象比Person對(duì)象多了一個(gè)power的屬性

function Superman(firstname, lastname, power){
  this.firstname = firstname;
  this.lastname = lastname;
 this.power = power;
}

var max = new Superman('max', 'lee', 'fly');

我們可以看到 PersonSuperman 對(duì)象基本完全一樣,只是Superman多了一個(gè)power屬性, 這樣代碼重復(fù)率很高辟灰,為了解決這個(gè)問(wèn)題个榕,我們可以使用 mixmin pattern 實(shí)現(xiàn)繼承來(lái)避免過(guò)多的重復(fù)代碼。

實(shí)現(xiàn) mixmin pattern有2個(gè)關(guān)鍵點(diǎn)

  1. 調(diào)用superclass的constructor使用.call
  2. 將superclass的prototype賦值給sub-class通過(guò)Object.create(superclass.prototype)

具體實(shí)現(xiàn)如下:

function Person(firstname, lastname){
  this.firstname = firstname;
  this.lastname = lastname;
}

function Superman(firstname, lastname, power){
  Person.call(this, firstname, lastname);
  this.power = power;
}

Superman.prototype = Object.create(Person.prototype);

var max = new Superman('max', 'lee', 'fly');

Facade Pattern

Facade Pattern是為了讓接口更加簡(jiǎn)單易用的一種設(shè)計(jì)模式, 比如jQuery用起來(lái)就是比原生的方法操作DOM更便捷芥喇。該設(shè)計(jì)模式通常和module pattern 結(jié)合使用西采。

讓我們實(shí)現(xiàn)一個(gè)$接口來(lái)更便捷的操作DOM

var $ = (function () {
    return {
        query: function(element){
            return document.querySelector(element);
        }
    }
})();

var titleElement = $.query('title'); // <title> title here </title>

Command Pattern

命令模式是把actions封裝成對(duì)象(command objects),比如我們把計(jì)算器的加減乘除這些action封裝在object里面

    var Calculator = {
        // addition function
        add: function (num1, num2) {
            return num1 + num2;
        },
        // subtraction function
        substract: function (num1, num2) {
            return num1 - num2;
        },
        // multiplication function
        multiply: function (num1, num2) {
            return num1 * num2;
        },
        // division function
        divide: function (num1, num2) {
            return num1 / num2;
        },
    };

我們可以像Calculator.add(1, 1)這樣來(lái)計(jì)算加法械馆,但是我們不想增加對(duì)象之間的依賴(lài)性,以后如果Calculator里面這些方法改變的時(shí)候武通,那么調(diào)用它的地方也都需要改動(dòng)霹崎。那么我們就可以不直接操作Calculator而是通過(guò)“命令”來(lái)操作加減乘除。
先定義一個(gè)計(jì)算接口來(lái)接收命令

Calculator.execute = function(command){
  return Calculator[command.type](command.num1, command.num2)
}

然后我們就可以這樣把命令當(dāng)作一個(gè)參數(shù)伴隨著要計(jì)算的數(shù)字傳給calc方法

console.log(Calculator.execute({type: "divide"    ,num1:1,num2:6}));
console.log(Calculator.execute({type: "multiply"  ,num1:3,num2:5}));

這樣以后就算Calculator的內(nèi)部方法以后改變了冶忱,但是調(diào)用提供的接口不需要改變尾菇。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市囚枪,隨后出現(xiàn)的幾起案子派诬,更是在濱河造成了極大的恐慌,老刑警劉巖链沼,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件默赂,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡括勺,警方通過(guò)查閱死者的電腦和手機(jī)缆八,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)疾捍,“玉大人耀里,你說(shuō)我怎么就攤上這事∈懊ィ” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵底哥,是天一觀的道長(zhǎng)咙鞍。 經(jīng)常有香客問(wèn)我,道長(zhǎng)趾徽,這世上最難降的妖魔是什么续滋? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮孵奶,結(jié)果婚禮上疲酌,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好朗恳,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布湿颅。 她就那樣靜靜地躺著,像睡著了一般粥诫。 火紅的嫁衣襯著肌膚如雪油航。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,631評(píng)論 1 305
  • 那天怀浆,我揣著相機(jī)與錄音谊囚,去河邊找鬼。 笑死执赡,一個(gè)胖子當(dāng)著我的面吹牛镰踏,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播沙合,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼奠伪,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了灌诅?” 一聲冷哼從身側(cè)響起芳来,我...
    開(kāi)封第一講書(shū)人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎猜拾,沒(méi)想到半個(gè)月后即舌,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡挎袜,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年顽聂,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片盯仪。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡紊搪,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出全景,到底是詐尸還是另有隱情耀石,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布爸黄,位于F島的核電站滞伟,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏炕贵。R本人自食惡果不足惜梆奈,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望称开。 院中可真熱鬧亩钟,春花似錦乓梨、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至总处,卻和暖如春狈惫,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背鹦马。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工胧谈, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人荸频。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓菱肖,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親旭从。 傳聞我的和親對(duì)象是個(gè)殘疾皇子稳强,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

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