iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 9
0
Modern Web

JS Design Pattern 系列 第 9

JS Design Pattern Day09-範本方法模式 TemplateMethod

安安大家好,第九天了,今天正能量一下好了:
一個人的成就大小往往取決於他所遇到的困難的程度-跨欄定律。

來吧 今天主題是 範本方法模式

範本方法模式是一種只需要使用繼承就可以實作的模式,基本上由兩部分結構組成:抽象父類別與實作子類別。作法通常是在父類別中封裝了子類別的演算法框架。子類別通過繼承這個抽象類別也繼承了整個演算法結構,並且可以選擇重寫父類別裡的方法。在範本方法模式中,子類別實作相同的部分被上移到父類別中,將不同的部分留在子類別來實作。

舉個據說很經典的例子 Coffe or Tea:

首先我們想要來泡一杯咖啡,通常的步驟大概是以下這些
1.煮水
2.熱水沖泡咖啡
3.咖啡倒進杯子
4.加糖和牛奶
用JS來寫大概會變這樣:

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

再來我們想要煮茶,步驟如下:
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();

然後我們就可以發現這一切行為實在是太像了,所以整理了一下。不管是泡茶還是咖啡都可以分為下面四步驟並且給予他們一個方法名稱:
1.煮水 boilWater()
2.熱水沖泡 brew()
3.茶水倒進杯子 pourInCup()
4.加調味 addCondiments()

茶跟咖啡其實有個統稱,那就是叫做飲料,我們要把共同的部份抽出來,並且用‘飲料’這個統稱來建立抽象父類別。
那我們再修改一下code,現在我們要建立一個父類別(飲料),內容就是我們剛剛定好的步驟:

var Beverage = function() {};

//基本上大家煮水應該都是一樣的,所以在這裡直接實作
Beverage.prototype.boilWater = function() {
	console.log('煮水');
};

//brew,pourInCup,addCondiments這些方法內容都是空的,用來讓子類別來重寫,代表每種飲料內容實作上的差異
Beverage.prototype.brew = function() {};
Beverage.prototype.pourInCup = function() {};
Beverage.prototype.addCondiments = function() {};

//這些順序是一樣的
Beverage.prototype.init = function() {
	this.boilWater();
	this.brew();
	this.pourInCup();
	this.addCondiments();
};

接下來要來建立子類別(coffee),並且讓這個類別繼承父類別。有點像是coffe是飲料的一種的感覺,但因每種飲料做法不同,所以各式飲料(子類別)做法內容會有所差異

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

這樣就完成咖啡了,基本上茶也是一樣的做法只是內容替換成茶實作的內容。coffee最後執行init時,由於Coffee的prototype上都沒有相對應的init物件,所以會順著原型鏈往上找到父類別的init。

其實在所有code之中只有Beverage.prototype.init這段才是是範本方法模式,原因是因為該方法封裝了子類別的演算法框架,做為一個演算法的範本,指導子類別的方法執行順序。

再來,由於JavsScript語法上沒有提供抽象類別的支援,如果我們Coffee或Tea類別忘計實作brew, pourInCup, addCondiments這些方法,我們將會無法知道錯誤狀況,有可能使用之後才發現內容是錯誤的,所以有以下兩種變通方法:
1.用Ducking Type模擬檢查
2.直接在父類別拋出Error,範例如下:

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

另外一個議題是,我們將方法封裝在父類別的時候,可能會遇到有些子類別有不同特性的狀況,例如煮紅茶不需要加糖之類的,那要怎麼辦呢?
我們可以利用hook來解決,舉例來說我們可以設置一個鉤子在父類別裡,我們叫這個鉤子名稱為shouldAddCondiments好了。範例前面一樣建立Beverage類別

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方法');
};

之後要加入shouldAddCondiments方法

Beverage.prototype.shouldAddCondiments = function() {
	return true;
};

之後執行的時候就會先檢查shouldAddCondiments,就可以有不同流程了

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

接下來實作紅茶子類別,當重寫shouldAddCondiments時就可以詢問用戶是否要添加糖

var BlackTea = function() {};
BlackTea.prototype = new Beverage();

BlackTea.prototype.addCondiments = function() {
	console.log('加糖');
};

BlackTea.prototype.shouldAddCondiments = function() {
	return window.confirm('要加糖嗎?');
};
//其他幾個方法省略,內容都一樣
var blackTea = new BlackTea();
blackTea.init();

“JavaScript設計模式與開發實踐”在這邊還講到一個'好萊塢原則',簡單來說就是如果你是演員,你把履歷交給好萊塢導演之後通常就是回家等消息,你如果打電話過去他們就會說:別打電話給我們,有事我會打電話給你。

好萊塢原則蠻常應用於其他模型和場景,例如之前練習寫的發佈/訂閱模式,以及callback的概念。
套了這個觀念之下,我們在實做範本方法模式時似乎也不用一定要用到繼承了,用以下的範例也可以辦到

var Berverage = 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 = Berverage({
	brew: function() {
		console.log('熱水沖泡咖啡');
	},
	pourInCup: function() {
		console.log('咖啡倒進杯子');
	},
	addCondiments: function() {
		console.log('加糖和牛奶');
	}
});

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

ok以上就是範本方法模式


上一篇
JS Design Pattern Day08-組合模式 Composite
下一篇
JS Design Pattern Day10-輕量模式 Flyweight(上)
系列文
JS Design Pattern 30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言