js之模板方法模式

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

模板方法模式是一種只需使用繼承就可以實現的非常簡單的模式。
模板方法模式由兩部分結構組成,第一部分是抽象父類,第二部分是具體的實現子類。通常在抽象父類中封裝了子類的算法框架,包括實現一些公共方法以及封裝子類中所有方法的執行順
序。子類通過繼承這個抽象類,也繼承了整個算法結構,並且可以選擇重寫父類的方法。

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

先泡一杯咖啡:

首先,我們先來泡一杯咖啡,如果沒有什麼太個性化的需求,泡咖啡的步驟通常如下:
(1) 把水煮沸
(2) 用沸水沖泡咖啡
(3) 把咖啡倒進杯子
(4) 加糖和牛奶
通過下面這段代碼,我們就能得到一杯香濃的咖啡:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <script>
        var Coffee = function(){};
        Coffee.prototype.boilWater = function(){
            console.log( '把水煮沸' );
        };
        Coffee.prototype.brewCoffeeGriends = function(){
            console.log( '用沸水沖泡咖啡' );
        };
        Coffee.prototype.pourInCup = function(){
            console.log( '把咖啡倒進杯子' );
        };
        Coffee.prototype.addSugarAndMilk = function(){
            console.log( '加糖和牛奶' );
        };
        Coffee.prototype.init = function(){
            this.boilWater();
            this.brewCoffeeGriends();
            this.pourInCup();
            this.addSugarAndMilk();
        };
        var coffee = new Coffee();
        coffee.init();
    </script>
</body>
</html>

泡一壺茶:

接下來,開始準備我們的茶,泡茶的步驟跟泡咖啡的步驟相差並不大:
(1) 把水煮沸
(2) 用沸水浸泡茶葉
(3) 把茶水倒進杯子
(4) 加檸檬
同樣用一段代碼來實現泡茶的步驟:

var Tea = function(){};
Tea.prototype.boilWater = function(){
    console.log( '把水煮沸' );
};
Tea.prototype.steepTeaBag = function(){
    console.log( '用沸水浸泡茶葉' );
};
Tea.prototype.pourInCup = function(){
    console.log( '把茶水倒進杯子' );
};
Tea.prototype.addLemon = function(){
    console.log( '加檸檬' );
};
Tea.prototype.init = function(){
    this.boilWater();
    this.steepTeaBag();
    this.pourInCup();
    this.addLemon();
};
var tea = new Tea();
tea.init();

分離出共同點:

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.init = function(){
    this.boilWater();
    this.brew();
    this.pourInCup();
    this.addCondiments();
};

創建 Coffee 子類和 Tea 子類:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <script>
        var Beverage = function(){};
        Beverage.prototype.boilWater = function(){
            console.log( '把水煮沸' );
        };
        Beverage.prototype.brew = function(){}; // 空方法,應該由子類重寫
        Beverage.prototype.pourInCup = function(){}; // 空方法,應該由子類重寫
        Beverage.prototype.addCondiments = function(){}; // 空方法,應該由子類重寫
        Beverage.prototype.init = function(){
            this.boilWater();
            this.brew();
            this.pourInCup();
            this.addCondiments();
        };

        var Coffee = function(){};
        Coffee.prototype = new Beverage();
        Coffee.prototype.brew = function(){
            console.log( '用沸水沖泡咖啡' );
        };
        Coffee.prototype.pourInCup = function(){
            console.log( '把咖啡倒進杯子' );
        };
        Coffee.prototype.addCondiments = function(){
            console.log( '加糖和牛奶' );
        };
        var Coffee = new Coffee();
        Coffee.init();
    </script>
</body>
</html>

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

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

模板方法模式的使用場景:

在 Web開發中也能找到很多模板方法模式的適用場景,比如我們在構建一系列的 UI組件,
這些組件的構建過程一般如下所示:
(1) 初始化一個 div容器;
(2) 通過 ajax請求拉取相應的數據;
(3) 把數據渲染到 div容器裏面,完成組件的構造;
(4) 通知用戶組件渲染完畢。

我們看到,任何組件的構建都遵循上面的 4 步,其中第(1)步和第(4)步是相同的。第(2)步不同的地方只是請求 ajax的遠程地址,第(3)步不同的地方是渲染數據的方式。

於是我們可以把這 4個步驟都抽象到父類的模板方法裏面,父類中還可以順便提供第(1)步和第(4)步的具體實現。當子類繼承這個父類之後,會重寫模板方法裏面的第(2)步和第(3)步。

添加鉤子

通過模板方法模式,我們在父類中封裝了子類的算法框架。這些算法框架在正常狀態下是適用於大多數子類的,但如果有一些特別“個性”的子類呢?比如我們在飲料類 Beverage 中封裝了
飲料的沖泡順序:
(1) 把水煮沸
(2) 用沸水沖泡飲料
(3) 把飲料倒進杯子
(4) 加調料
這 4個沖泡飲料的步驟適用於咖啡和茶,在我們的飲料店裏,根據這 4個步驟製作出來的咖啡和茶,一直順利地提供給絕大部分客人享用。但有一些客人喝咖啡是不加調料(糖和牛奶)的。既然 Beverage 作爲父類,已經規定好了沖泡飲料的 4個步驟,那麼有什麼辦法可以讓子類不受這個約束呢?

鉤子方法( hook )可以用來解決這個問題,放置鉤子是隔離變化的一種常見手段。我們在父類中容易變化的地方放置鉤子,鉤子可以有一個默認的實現,究竟要不要“掛鉤”,這由子類自行決定。鉤子方法的返回結果決定了模板方法後面部分的執行步驟,也就是程序接下來的走向,這樣一來,程序就擁有了變化的可能。在這個例子裏,我們把掛鉤的名字定爲customerWantsCondiments ,接下來將掛鉤放入 Beverage
類,看看我們如何得到一杯不需要糖和牛奶的咖啡,代碼如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <script>
        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; // 默認需要調料
        };
        Beverage.prototype.init = function(){
            this.boilWater();
            this.brew();
            this.pourInCup();
            if ( this.customerWantsCondiments() ){ // 如果掛鉤返回 true,則需要調料
                this.addCondiments();
            }
        };
        var CoffeeWithHook = function(){};
        CoffeeWithHook.prototype = new Beverage();
        CoffeeWithHook.prototype.brew = function(){
            console.log( '用沸水沖泡咖啡' );
        };
        CoffeeWithHook.prototype.pourInCup = function(){
            console.log( '把咖啡倒進杯子' );
        };
        CoffeeWithHook.prototype.addCondiments = function(){
            console.log( '加糖和牛奶' );
        };
        CoffeeWithHook.prototype.customerWantsCondiments = function(){
            return window.confirm( '請問需要調料嗎?' );
        };
        var coffeeWithHook = new CoffeeWithHook();
        coffeeWithHook.init();
    </script>
</body>
</html>

真的需要“繼承”嗎?

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <script>
        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( '把咖啡倒進杯子' );
            },
            addCondiments: function(){
                console.log( '加糖和牛奶' );
            }
        });
        var Tea = Beverage({
            brew: function(){
                console.log( '用沸水浸泡茶葉' );
            },
            pourInCup: function(){
                console.log( '把茶倒進杯子' );
            },
            addCondiments: function(){
                console.log( '加檸檬' );
            }
        });
        var coffee = new Coffee();
        coffee.init();
        var tea = new Tea();
        tea.init();
    </script>
</body>
</html>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章