JavaScript設(shè)計(jì)模式之模板方法模式

在JavaScript開發(fā)中用到繼承的場(chǎng)景其實(shí)并不是很多,很多時(shí)候我們都喜歡用mix-in的方式給對(duì)象擴(kuò)展屬性。但這不代表繼承在JavaScript里沒有用武之地,雖然沒有真正的類和繼承機(jī)制,但我們可以通過原型prototype來變相地實(shí)現(xiàn)繼承。

不過這里并非要討論繼承,而是討論一種基于繼承的設(shè)計(jì)模式——模板方法(Template Method)模式。

模板方法模式的定義和組成

模板方法模式是一種只需使用繼承就可以實(shí)現(xiàn)的非常簡(jiǎn)單的模式。

模板方法模式由兩部分結(jié)構(gòu)組成,第一部分是抽象父類,第二部分是具體的實(shí)現(xiàn)子類。通常在抽象父類中封裝了子類的算法框架,包括實(shí)現(xiàn)一些公共方法以及封裝子類中所有方法的執(zhí)行順序。子類通過繼承這個(gè)抽象類,也繼承了整個(gè)算法結(jié)構(gòu),并且可以選擇重寫父類的方法。

假如我們有一些平行的子類,各個(gè)子類之間有一些相同的行為,也有一些不同的行為。如果相同和不同的行為都混合在各個(gè)子類的實(shí)現(xiàn)中,說明這些相同的行為會(huì)在各個(gè)子類中重復(fù)出現(xiàn)。但實(shí)際上,相同的行為可以被搬移到另外一個(gè)單一的地方,模板方法模式就是為解決這個(gè)問題而生的。在模板方法模式中,子類實(shí)現(xiàn)中的相同部分被上移到父類中,而將不同的部分留待子類來實(shí)現(xiàn)。這也很好地體現(xiàn)了泛化的思想。

第一個(gè)例子——Coffee or Tea

咖啡與茶是一個(gè)經(jīng)典的例子,經(jīng)常用來講解模板方法模式,這個(gè)例子的原型來自《Head First設(shè)計(jì)模式》。這里我們就用JavaScript來實(shí)現(xiàn)這個(gè)例子。

1.先泡一杯咖啡

首先,我們先來泡一杯咖啡,如果沒有什么太個(gè)性化的需求,泡咖啡的步驟通常如下:

(1) 把水煮沸

(2) 用沸水沖泡咖啡

(3) 把咖啡倒進(jìn)杯子

(4) 加糖和牛奶

通過下面這段代碼,我們就能得到一杯香濃的咖啡:

var Coffee = function(){};

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

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

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

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

Coffee.prototype.init = function(){
    this.boilWater();
    this.brewCoffeeGriends();
    this.pourInCup();
    this.addSugarAndMilk();
};

var coffee = new Coffee();
coffee.init();
2.泡一壺茶

接下來,開始準(zhǔn)備我們的茶,泡茶的步驟跟泡咖啡的步驟相差并不大:

(1) 把水煮沸

(2) 用沸水浸泡茶葉

(3) 把茶水倒進(jìn)杯子

(4) 加檸檬

同樣用一段代碼來實(shí)現(xiàn)泡茶的步驟:

var Tea = function(){};

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

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

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

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

Tea.prototype.init = function(){
    this.boilWater();
    this.steepTeaBag();
    this.pourInCup();
    this.addLemon();
};

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

3.分離出共同點(diǎn)

現(xiàn)在我們分別泡好了一杯咖啡和一壺茶,經(jīng)過思考和比較,我們發(fā)現(xiàn)咖啡和茶的沖泡過程是大同小異的。

我們找到泡咖啡和泡茶主要有以下不同點(diǎn)。

  • 原料不同。一個(gè)是咖啡,一個(gè)是茶,但我們可以把它們都抽象為“飲料”。

  • 泡的方式不同。咖啡是沖泡,而茶葉是浸泡,我們可以把它們都抽象為“泡”。

  • 加入的調(diào)料不同。一個(gè)是糖和牛奶,一個(gè)是檸檬,但我們可以把它們都抽象為“調(diào)料”。

經(jīng)過抽象之后,不管是泡咖啡還是泡茶,我們都能整理為下面四步:

(1) 把水煮沸

(2) 用沸水沖泡飲料

(3) 把飲料倒進(jìn)杯子

(4) 加調(diào)料

所以,不管是沖泡還是浸泡,我們都能給它一個(gè)新的方法名稱,比如說brew()。同理,不管是加糖和牛奶,還是加檸檬,我們都可以稱之為addCondiments()。

讓我們忘記最開始創(chuàng)建的Coffee類和Tea類。 現(xiàn)在可以創(chuàng)建一個(gè)抽象父類來表示泡一杯飲料的整個(gè)過程。不論是Coffee,還是Tea,都被我們用Beverage來表示,代碼如下:

var Beverage = function(){};

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

Beverage.prototype.brew = function(){};      // 空方法,應(yīng)該由子類重寫

Beverage.prototype.pourInCup = function(){};    // 空方法,應(yīng)該由子類重寫

Beverage.prototype.addCondiments = function(){};    // 空方法,應(yīng)該由子類重寫

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

4.創(chuàng)建Coffee子類和Tea子類

現(xiàn)在創(chuàng)建一個(gè)Beverage類的對(duì)象對(duì)我們來說沒有意義,因?yàn)槭澜缟夏芎鹊臇|西沒有一種真正叫“飲料”的,飲料在這里還只是一個(gè)抽象的存在。接下來我們要?jiǎng)?chuàng)建咖啡類和茶類,并讓它們繼承飲料類:

var Coffee = function(){};

Coffee.prototype = new Beverage();

接下來要重寫抽象父類中的一些方法,只有“把水煮沸”這個(gè)行為可以直接使用父類Beverage中的boilWater方法,其他方法都需要在Coffee子類中重寫,代碼如下:

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();

至此我們的Coffee類已經(jīng)完成了,當(dāng)調(diào)用coffee對(duì)象的init方法時(shí),由于coffee對(duì)象和Coffee構(gòu)造器的原型prototype上都沒有對(duì)應(yīng)的init方法,所以該請(qǐng)求會(huì)順著原型鏈,被委托給Coffee的“父類”Beverage原型上的init方法。

而Beverage.prototype.init方法中已經(jīng)規(guī)定好了泡飲料的順序,所以我們能成功地泡出一杯咖啡,代碼如下:

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

接下來照葫蘆畫瓢,來創(chuàng)建我們的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();

這里一直討論的是模板方法模式,那么在上面的例子中,到底誰才是所謂的模板方法呢?答案是Beverage.prototype.init。

Beverage.prototype.init被稱為模板方法的原因是,該方法中封裝了子類的算法框架,它作為一個(gè)算法的模板,指導(dǎo)子類以何種順序去執(zhí)行哪些方法。在Beverage.prototype.init方法中,算法內(nèi)的每一個(gè)步驟都清楚地展示在我們眼前。

抽象類

首先要說明的是,模板方法模式是一種嚴(yán)重依賴抽象類的設(shè)計(jì)模式。JavaScript在語言層面并沒有提供對(duì)抽象類的支持,我們也很難模擬抽象類的實(shí)現(xiàn)。這里我們將著重討論Java中抽象類的作用,以及JavaScript沒有抽象類時(shí)所做出的讓步和變通。

1.抽象類的作用

在Java中,類分為兩種,一種為具體類,另一種為抽象類。具體類可以被實(shí)例化,抽象類不能被實(shí)例化。要了解抽象類不能被實(shí)例化的原因,我們可以思考“飲料”這個(gè)抽象類。

想象這樣一個(gè)場(chǎng)景:我們口渴了,去便利店想買一瓶飲料,我們不能直接跟店員說:“來一瓶飲料?!比绻覀冞@樣說了,那么店員接下來肯定會(huì)問:“要什么飲料?”飲料只是一個(gè)抽象名詞,只有當(dāng)我們真正明確了的飲料類型之后,才能得到一杯咖啡、茶、或者可樂。

由于抽象類不能被實(shí)例化,如果有人編寫了一個(gè)抽象類,那么這個(gè)抽象類一定是用來被某些具體類繼承的。

抽象類和接口一樣可以用于向上轉(zhuǎn)型(可參考1.3節(jié)關(guān)于多態(tài)的內(nèi)容),在靜態(tài)類型語言中,編譯器對(duì)類型的檢查總是一個(gè)繞不過的話題與困擾。雖然類型檢查可以提高程序的安全性,但繁瑣而嚴(yán)格的類型檢查也時(shí)常會(huì)讓程序員覺得麻煩。把對(duì)象的真正類型隱藏在抽象類或者接口之后,這些對(duì)象才可以被互相替換使用。這可以讓我們的Java程序盡量遵守依賴倒置原則。

除了用于向上轉(zhuǎn)型,抽象類也可以表示一種契約。繼承了這個(gè)抽象類的所有子類都將擁有跟抽象類一致的接口方法,抽象類的主要作用就是為它的子類定義這些公共接口。如果我們?cè)谧宇愔袆h掉了這些方法中的某一個(gè),那么將不能通過編譯器的檢查,這在某些場(chǎng)景下是非常有用的,比如我們本章討論的模板方法模式,Beverage類的init方法里規(guī)定了沖泡一杯飲料的順序如下:

this.boilWater();    // 把水煮沸
this.brew();     // 用水泡原料
this.pourInCup();    // 把原料倒進(jìn)杯子
this.addCondiments();    // 添加調(diào)料

如果在Coffee子類中沒有實(shí)現(xiàn)對(duì)應(yīng)的brew方法,那么我們百分之百得不到一杯咖啡。既然父類規(guī)定了子類的方法和執(zhí)行這些方法的順序,子類就應(yīng)該擁有這些方法,并且提供正確的實(shí)現(xiàn)。

抽象方法和具體方法

抽象方法被聲明在抽象類中,抽象方法并沒有具體的實(shí)現(xiàn)過程,是一些“啞”方法。比如Beverage類中的brew方法、pourInCup方法和addCondiments方法,都被聲明為抽象方法。當(dāng)子類繼承了這個(gè)抽象類時(shí),必須重寫父類的抽象方法。

除了抽象方法之外,如果每個(gè)子類中都有一些同樣的具體實(shí)現(xiàn)方法,那這些方法也可以選擇放在抽象類中,這可以節(jié)省代碼以達(dá)到復(fù)用的效果,這些方法叫作具體方法。當(dāng)代碼需要改變時(shí),我們只需要改動(dòng)抽象類里的具體方法就可以了。比如飲料中的boilWater方法,假設(shè)沖泡所有的飲料之前,都要先把水煮沸,那我們自然可以把boilWater方法放在抽象類Beverage中。

用Java實(shí)現(xiàn)Coffee or Tea的例子

下面我們嘗試著把Coffee和Tea的例子換成Java代碼,這有助于我們理解抽象類的意義。

// Java代碼

public abstract class Beverage {    // 飲料抽象類
    final void init(){    // 模板方法
        boilWater();
        brew();
        pourInCup();
        addCondiments();
    }

    void boilWater(){        // 具體方法boilWater
        System.out.println( "把水煮沸" );
    }

    abstract void brew();        // 抽象方法brew
    abstract void addCondiments();        // 抽象方法addCondiments
    abstract void pourInCup();        // 抽象方法pourInCup
}


public class Coffee extends Beverage{        // Coffee類
    @Override
    void brew() {    // 子類中重寫brew方法
        System.out.println( "用沸水沖泡咖啡" );
    }

    @Override
    void pourInCup(){    // 子類中重寫pourInCup方法
        System.out.println( "把咖啡倒進(jìn)杯子" );
    }

    @Override
    void addCondiments() {        // 子類中重寫addCondiments方法
        System.out.println( "加糖和牛奶" );
    }
}


public class Tea extends Beverage{        // Tea類
    @Override
    void brew() {        // 子類中重寫brew方法
        System.out.println( "用沸水浸泡茶葉" );
    }

    @Override
    void pourInCup(){        // 子類中重寫pourInCup方法
        System.out.println( "把茶倒進(jìn)杯子" );
    }

    @Override
    void addCondiments() {        // 子類中重寫addCondiments方法
        System.out.println( "加檸檬" );
    }
}

public class Test {

    private static void prepareRecipe( Beverage beverage ){
        beverage.init();
    }

    public static void main( String args[] ){
         Beverage coffee = new Coffee();   // 創(chuàng)建coffee對(duì)象
         prepareRecipe( coffee );   // 開始泡咖啡
        // 把水煮沸
        // 用沸水沖泡咖啡
        // 把咖啡倒進(jìn)杯子
        // 加糖和牛奶

        Beverage tea = new Tea();   // 創(chuàng)建tea對(duì)象
        prepareRecipe( tea );   // 開始泡茶
        // 把水煮沸
        // 用沸水浸泡茶葉
        // 把茶倒進(jìn)杯子
        // 加檸檬
    }
}

JavaScript沒有抽象類的缺點(diǎn)和解決方案

JavaScript并沒有從語法層面提供對(duì)抽象類的支持。抽象類的第一個(gè)作用是隱藏對(duì)象的具體類型,由于JavaScript是一門“類型模糊”的語言,所以隱藏對(duì)象的類型在JavaScript中并不重要。

另一方面, 當(dāng)我們?cè)贘avaScript中使用原型繼承來模擬傳統(tǒng)的類式繼承時(shí),并沒有編譯器幫助我們進(jìn)行任何形式的檢查,我們也沒有辦法保證子類會(huì)重寫父類中的“抽象方法”。

我們知道,Beverage.prototype.init方法作為模板方法,已經(jīng)規(guī)定了子類的算法框架,代碼如下:

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

如果我們的Coffee類或者Tea類忘記實(shí)現(xiàn)這4個(gè)方法中的一個(gè)呢?拿brew方法舉例,如果我們忘記編寫Coffee.prototype.brew方法,那么當(dāng)請(qǐng)求coffee對(duì)象的brew時(shí),請(qǐng)求會(huì)順著原型鏈找到Beverage“父類”對(duì)應(yīng)的Beverage.prototype.brew方法,而Beverage.prototype.brew方法到目前為止是一個(gè)空方法,這顯然是不能符合我們需要的。

在Java中編譯器會(huì)保證子類會(huì)重寫父類中的抽象方法,但在JavaScript中卻沒有進(jìn)行這些檢查工作。我們?cè)诰帉懘a的時(shí)候得不到任何形式的警告,完全寄托于程序員的記憶力和自覺性是很危險(xiǎn)的,特別是當(dāng)我們使用模板方法模式這種完全依賴?yán)^承而實(shí)現(xiàn)的設(shè)計(jì)模式時(shí)。

下面提供兩種變通的解決方案。

  • 第1種方案是用鴨子類型來模擬接口檢查,以便確保子類中確實(shí)重寫了父類的方法。但模擬接口檢查會(huì)帶來不必要的復(fù)雜性,而且要求程序員主動(dòng)進(jìn)行這些接口檢查,這就要求我們?cè)跇I(yè)務(wù)代碼中添加一些跟業(yè)務(wù)邏輯無關(guān)的代碼。

  • 第2種方案是讓Beverage.prototype.brew等方法直接拋出一個(gè)異常,如果因?yàn)榇中耐浘帉慍offee.prototype.brew方法,那么至少我們會(huì)在程序運(yùn)行時(shí)得到一個(gè)錯(cuò)誤:

Beverage.prototype.brew = function(){
    throw new Error( '子類必須重寫brew方法'  );
};
 
 
Beverage.prototype.pourInCup = function(){
    throw new Error( '子類必須重寫pourInCup方法'  );
};
 
 
Beverage.prototype.addCondiments = function(){
    throw new Error( '子類必須重寫addCondiments方法'  );
};

第2種解決方案的優(yōu)點(diǎn)是實(shí)現(xiàn)簡(jiǎn)單,付出的額外代價(jià)很少;缺點(diǎn)是我們得到錯(cuò)誤信息的時(shí)間點(diǎn)太靠后。

我們一共有3次機(jī)會(huì)得到這個(gè)錯(cuò)誤信息,第1次是在編寫代碼的時(shí)候,通過編譯器的檢查來得到錯(cuò)誤信息;第2次是在創(chuàng)建對(duì)象的時(shí)候用鴨子類型來進(jìn)行“接口檢查”;而目前我們不得不利用最后一次機(jī)會(huì),在程序運(yùn)行過程中才知道哪里發(fā)生了錯(cuò)誤。

模板方法模式的使用場(chǎng)景

從大的方面來講,模板方法模式常被架構(gòu)師用于搭建項(xiàng)目的框架,架構(gòu)師定好了框架的骨架,程序員繼承框架的結(jié)構(gòu)之后,負(fù)責(zé)往里面填空,比如Java程序員大多使用過HttpServlet技術(shù)來開發(fā)項(xiàng)目。

一個(gè)基于HttpServlet的程序包含7個(gè)生命周期,這7個(gè)生命周期分別對(duì)應(yīng)一個(gè)do方法。

doGet()
doHead()
doPost()
doPut()
doDelete()
doOption()
doTrace()

HttpServlet類還提供了一個(gè)service方法,它就是這里的模板方法,service規(guī)定了這些do方法的執(zhí)行順序,而這些do方法的具體實(shí)現(xiàn)則需要HttpServlet的子類來提供。

在Web開發(fā)中也能找到很多模板方法模式的適用場(chǎng)景,比如我們?cè)跇?gòu)建一系列的UI組件,這些組件的構(gòu)建過程一般如下所示:

(1) 初始化一個(gè)div容器;

(2) 通過ajax請(qǐng)求拉取相應(yīng)的數(shù)據(jù);

(3) 把數(shù)據(jù)渲染到div容器里面,完成組件的構(gòu)造;

(4) 通知用戶組件渲染完畢。

我們看到,任何組件的構(gòu)建都遵循上面的4步,其中第(1)步和第(4)步是相同的。第(2)步不同的地方只是請(qǐng)求ajax的遠(yuǎn)程地址,第(3)步不同的地方是渲染數(shù)據(jù)的方式。

于是我們可以把這4個(gè)步驟都抽象到父類的模板方法里面,父類中還可以順便提供第(1)步和第(4)步的具體實(shí)現(xiàn)。當(dāng)子類繼承這個(gè)父類之后,會(huì)重寫模板方法里面的第(2)步和第(3)步。

鉤子方法

通過模板方法模式,我們?cè)诟割愔蟹庋b了子類的算法框架。這些算法框架在正常狀態(tài)下是適用于大多數(shù)子類的,但如果有一些特別“個(gè)性”的子類呢?比如我們?cè)陲嬃项怋everage中封裝了飲料的沖泡順序:

(1) 把水煮沸

(2) 用沸水沖泡飲料

(3) 把飲料倒進(jìn)杯子

(4) 加調(diào)料

這4個(gè)沖泡飲料的步驟適用于咖啡和茶,在我們的飲料店里,根據(jù)這4個(gè)步驟制作出來的咖啡和茶,一直順利地提供給絕大部分客人享用。但有一些客人喝咖啡是不加調(diào)料(糖和牛奶)的。既然Beverage作為父類,已經(jīng)規(guī)定好了沖泡飲料的4個(gè)步驟,那么有什么辦法可以讓子類不受這個(gè)約束呢?

鉤子方法(hook)可以用來解決這個(gè)問題,放置鉤子是隔離變化的一種常見手段。我們?cè)诟割愔腥菀鬃兓牡胤椒胖勉^子,鉤子可以有一個(gè)默認(rèn)的實(shí)現(xiàn),究竟要不要“掛鉤”,這由子類自行決定。鉤子方法的返回結(jié)果決定了模板方法后面部分的執(zhí)行步驟,也就是程序接下來的走向,這樣一來,程序就擁有了變化的可能。

在這個(gè)例子里,我們把掛鉤的名字定為customerWantsCondiments,接下來將掛鉤放入Beverage類,看看我們?nèi)绾蔚玫揭槐恍枰呛团D痰目Х?,代碼如下:

var Beverage = function(){};

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

Beverage.prototype.brew = function(){
    throw new Error( '子類必須重寫brew方法' );
};

Beverage.prototype.pourInCup = function(){
    throw new Error( '子類必須重寫pourInCup方法' );
};

Beverage.prototype.addCondiments = function(){
    throw new Error( '子類必須重寫addCondiments方法' );
};

Beverage.prototype.customerWantsCondiments = function(){
    return true;    // 默認(rèn)需要調(diào)料
};

Beverage.prototype.init = function(){
    this.boilWater();
    this.brew();
    this.pourInCup();
    if ( this.customerWantsCondiments() ){    // 如果掛鉤返回true,則需要調(diào)料
        this.addCondiments();
    }
};


var CoffeeWithHook = function(){};

CoffeeWithHook.prototype = new Beverage();

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

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

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

CoffeeWithHook.prototype.customerWantsCondiments = function(){
    return window.confirm( '請(qǐng)問需要調(diào)料嗎?' );
};

var coffeeWithHook = new CoffeeWithHook();
coffeeWithHook.init();

好萊塢原則

學(xué)習(xí)完模板方法模式之后,我們要引入一個(gè)新的設(shè)計(jì)原則——著名的“好萊塢原則”。

好萊塢無疑是演員的天堂,但好萊塢也有很多找不到工作的新人演員,許多新人演員在好萊塢把簡(jiǎn)歷遞給演藝公司之后就只有回家等待電話。有時(shí)候該演員等得不耐煩了,給演藝公司打電話詢問情況,演藝公司往往這樣回答:“不要來找我,我會(huì)給你打電話?!?/p>

在設(shè)計(jì)中,這樣的規(guī)則就稱為好萊塢原則。在這一原則的指導(dǎo)下,我們?cè)试S底層組件將自己掛鉤到高層組件中,而高層組件會(huì)決定什么時(shí)候、以何種方式去使用這些底層組件,高層組件對(duì)待底層組件的方式,跟演藝公司對(duì)待新人演員一樣,都是“別調(diào)用我們,我們會(huì)調(diào)用你”。

模板方法模式是好萊塢原則的一個(gè)典型使用場(chǎng)景,它與好萊塢原則的聯(lián)系非常明顯,當(dāng)我們用模板方法模式編寫一個(gè)程序時(shí),就意味著子類放棄了對(duì)自己的控制權(quán),而是改為父類通知子類,哪些方法應(yīng)該在什么時(shí)候被調(diào)用。作為子類,只負(fù)責(zé)提供一些設(shè)計(jì)上的細(xì)節(jié)。

除此之外,好萊塢原則還常常應(yīng)用于其他模式和場(chǎng)景,例如發(fā)布-訂閱模式和回調(diào)函數(shù)。

  • 發(fā)布—訂閱模式

在發(fā)布—訂閱模式中,發(fā)布者會(huì)把消息推送給訂閱者,這取代了原先不斷去fetch消息的形式。例如假設(shè)我們乘坐出租車去一個(gè)不了解的地方,除了每過5秒鐘就問司機(jī)“是否到達(dá)目的地”之外,還可以在車上美美地睡上一覺,然后跟司機(jī)說好,等目的地到了就叫醒你。這也相當(dāng)于好萊塢原則中提到的“別調(diào)用我們,我們會(huì)調(diào)用你”。

  • 回調(diào)函數(shù)

在ajax異步請(qǐng)求中,由于不知道請(qǐng)求返回的具體時(shí)間,而通過輪詢?nèi)ヅ袛嗍欠穹祷財(cái)?shù)據(jù),這顯然是不理智的行為。所以我們通常會(huì)把接下來的操作放在回調(diào)函數(shù)中,傳入發(fā)起ajax異步請(qǐng)求的函數(shù)。當(dāng)數(shù)據(jù)返回之后,這個(gè)回調(diào)函數(shù)才被執(zhí)行,這也是好萊塢原則的一種體現(xiàn)。把需要執(zhí)行的操作封裝在回調(diào)函數(shù)里,然后把主動(dòng)權(quán)交給另外一個(gè)函數(shù)。至于回調(diào)函數(shù)什么時(shí)候被執(zhí)行,則是另外一個(gè)函數(shù)控制的。

真的需要“繼承”嗎

模板方法模式是基于繼承的一種設(shè)計(jì)模式,父類封裝了子類的算法框架和方法的執(zhí)行順序,子類繼承父類之后,父類通知子類執(zhí)行這些方法,好萊塢原則很好地詮釋了這種設(shè)計(jì)技巧,即高層組件調(diào)用底層組件。

本文我們通過模板方法模式,編寫了一個(gè)Coffee or Tea的例子。模板方法模式是為數(shù)不多的基于繼承的設(shè)計(jì)模式,但JavaScript語言實(shí)際上沒有提供真正的類式繼承,繼承是通過對(duì)象與對(duì)象之間的委托來實(shí)現(xiàn)的。也就是說,雖然我們?cè)谛问缴辖梃b了提供類式繼承的語言,但這里學(xué)習(xí)到的模板方法模式并不十分正宗。而且在JavaScript這般靈活的語言中,實(shí)現(xiàn)這樣一個(gè)例子,是否真的需要繼承這種重武器呢?

在好萊塢原則的指導(dǎo)之下,下面這段代碼可以達(dá)到和繼承一樣的效果。

var Beverage = function( param ){

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

    var brew = param.brew || function(){
        throw new Error( '必須傳遞brew方法' );
    };

    var pourInCup = param.pourInCup || function(){
        throw new Error( '必須傳遞pourInCup方法' );
    };

    var addCondiments = param.addCondiments || function(){
        throw new Error( '必須傳遞addCondiments方法' );
    };

    var F = function(){};

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

    return F;
};

var Coffee = Beverage({
    brew: function(){
        console.log( '用沸水沖泡咖啡' );
    },
    pourInCup: function(){
         console.log( '把咖啡倒進(jìn)杯子' );
     },
     addCondiments: function(){
         console.log( '加糖和牛奶' );
     }
});

var Tea = Beverage({
    brew: function(){
        console.log( '用沸水浸泡茶葉' );
    },
    pourInCup: function(){
         console.log( '把茶倒進(jìn)杯子' );
     },
     addCondiments: function(){
         console.log( '加檸檬' );
     }
});


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

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

在這段代碼中,我們把brew、pourInCup、addCondiments這些方法依次傳入Beverage函數(shù),Beverage 函數(shù)被調(diào)用之后返回構(gòu)造器F。F類中包含了“模板方法”F.prototype.init。跟繼承得到的效果一樣,該“模板方法”里依然封裝了飲料子類的算法框架。

小結(jié)

模板方法模式是一種典型的通過封裝變化提高系統(tǒng)擴(kuò)展性的設(shè)計(jì)模式。在傳統(tǒng)的面向?qū)ο笳Z言中,一個(gè)運(yùn)用了模板方法模式的程序中,子類的方法種類和執(zhí)行順序都是不變的,所以我們把這部分邏輯抽象到父類的模板方法里面。而子類的方法具體怎么實(shí)現(xiàn)則是可變的,于是我們把這部分變化的邏輯封裝到子類中。通過增加新的子類,我們便能給系統(tǒng)增加新的功能,并不需要改動(dòng)抽象父類以及其他子類,這也是符合開放-封閉原則的。

但在JavaScript中,我們很多時(shí)候都不需要依樣畫瓢地去實(shí)現(xiàn)一個(gè)模版方法模式,高階函數(shù)是更好的選擇。

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

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

  • 模板方法模式 模板方法主要由兩部分構(gòu)成, 第一部分是抽象父類,第二部分是具體實(shí)現(xiàn)的子類,通常我們?cè)诔橄蟾割愔蟹庋b子...
    yangfan0095閱讀 315評(píng)論 0 1
  • 摘自《JavaScript設(shè)計(jì)模式與開發(fā)實(shí)踐》 模板方法模式是一種只需使用繼承就可以實(shí)現(xiàn)的非常簡(jiǎn)單的模式。 模板方...
    小小的白菜閱讀 295評(píng)論 0 0
  • javascript設(shè)計(jì)模式與開發(fā)實(shí)踐 設(shè)計(jì)模式 每個(gè)設(shè)計(jì)模式我們需要從三點(diǎn)問題入手: 定義 作用 用法與實(shí)現(xiàn) 單...
    穿牛仔褲的蚊子閱讀 4,441評(píng)論 0 13
  • JavaScript-模板方法模式 模板方法是什么鬼?模板模式又是什么鬼??? 聽說它很復(fù)雜,聽說它很難,我可不可以...
    caraline閱讀 378評(píng)論 1 0
  • 炎熱的夏天,知了不停著唱著枯燥的歌曲。 趙平是某集團(tuán)軍特種部隊(duì)猛虎特別行動(dòng)小組的隊(duì)長(zhǎng),是一個(gè)鐵桿的三國迷,...
    軍王閱讀 358評(píng)論 0 1

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