iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 2
0
Modern Web

JS Design Pattern 系列 第 2

JS Design Pattern Day02-策略模式 Strategy

嗨大家好,這是JS Design Pattern第二天,再次說明這系列主題都是從一本叫做 "JavaScript設計模式與開發實踐" 這本書範例練習來的,然後原本要一起組隊的快樂夥伴第一天就自爆了https://ithelp.ithome.com.tw/articles/10201535
不過別擔心,我將繼續完成他的遺志

今天寫第二種模式:策略模式Strategy!!

策略模式定義:定義一系列演算法,把它們封裝起來,並使它們可以相互替換

當我們要達到某個目的,途中會使用各種方法,這個模式是可以更彈性、更方便的替換這些方法的一種模式,今天會舉兩個例子來說明。
首先第一個例子:現在要寫一個公司依考績來發獎金的程式,考績越好發的獎金(底薪xN個月)越高。

var calcBonus = function(performance, salary) {
	if (performance === 'S') {
		return salary * 4;
	}
	if (performance === 'A') {
		return salary * 3;
	}
	if (performance === 'B') {
		return salary * 2;
	}
};
console.log(calcBonus('A', 100));

好就這樣寫完了,但是有些問題:
1.太多if-else述句
2.違反開放-封閉原則:若要新增一個等級或是修改規則就要深入calcBonus內部實作

所以我們來使用策略模式來重構程式碼,原則就是將不變的部分和變化的部分隔開
在這裡獎金的算法是會變的,而使用方式是不會變的,根據定義"將一系列演算法,把它們封裝起來,並使它們可以相互替換",我們可以將獎金的演算法利用JS的物件特性封裝起來,再放到相同的使用方式中:

//演算法
var strategies = {
	'S': function(salary) {
		return salary * 4;
	},
	'A': function(salary) {
		return salary * 3;
	},
	'B': function(salary) {
		return salary * 2;
	}
};

//使用它
var calcBonus = function(performance, salary) {
	return strategies[performance](salary);
};
console.log(calcBonus('S', 100));

這樣就完成策略模式了。
實際上我們很常會使用到這類模式,再來舉一個前端頁面的例子,很常使用的表單驗證:驗證表單輸入名稱、密碼和手機號碼。

var $form = getForm();
$form.submit(onSubmit);

function onSubmit() {
	var form = this;
	if (form.userName.value === '') {
		alert('userName 不能為空');
		return false;
	}
	if (form.password.value.length < 6) {
		alert('密碼長度少於6位');
		return false;
	}
	if (!/^[09]{2}[0-9]{8}$/.test(form.phoneNumber.value)) {
		alert('手機號碼格式不對');
		return false;
	}
}

//製作表單畫面(可以忽略)
function getForm() {
	var $form = $('<form>').appendTo('body').attr('method', 'post');
	createLine('使用者名稱:', 'userName').appendTo($form);
	createLine('密碼:', 'password').appendTo($form);
	createLine('手機號碼:', 'phoneNumber').appendTo($form);
	createButton().appendTo($form);
	return $form;

	function createButton() {
		return $('<button>').text('submit');
	}

	function createLine(text, name) {
		var $label = $('<span>').text(text);
		var $input = $('<input>').attr('name', name);
		return $('<div>').append($label).append($input);
	}
}

上面這應該是最簡單直接的寫法,一樣會有些缺點:
1.if else結構太多。
2.若檢驗的內容須改變,就必須深入onSubmit中去修改。
3.重用性差,表單常常不只一個,每個都要判斷的話就可能會一直複製貼上。

所以,我們再利用策略模式特性來重構一下:
首先一樣先封裝策略

var strategies = {
	inNotEmpty: function(val, errorMsg) {
		if (val === '') {
			return errorMsg;
		}
	},
	minLength: function(val, length, errorMsg) {
		if (val.length < length) {
			return errorMsg;
		}
	},
	isMobileNumber: function(val, errorMsg) {
		if (!/^[09]{2}[0-9]{8}$/.test(val)) {
			return errorMsg;
		}
	}
};

再來做一個這種表單用的驗證器(myFormValidator),注意myFormValidator裡面會建立一個Validator物件,這個Validator物件會記住你這張表單要檢查的規則要用到哪些檢查邏輯(strategies裡面的那些),以及錯誤的警告要顯示什麼

function myFormValidator(form) {
	var validator = new Validator();
	validator.add(form.userName.value, [{
		strategy: 'inNotEmpty',
		errorMsg: '使用者名稱不為空'
	}, {
		strategy: 'minLength:6',
		errorMsg: '使用者名稱位數不得少於6'
	}]);
	validator.add(form.password.value, [{
		strategy: 'minLength:6',
		errorMsg: '密碼位數不得少於6'
	}]);
	validator.add(form.phoneNumber.value, [{
		strategy: 'isMobileNumber',
		errorMsg: '手機號碼錯誤'
	}]);
	var errorMsg = validator.start();
	return errorMsg;
}

再來我們就要來實作Validator function

var Validator = function() {
	this.cache = [];
};

Validator.prototype.add = function(item, rules) {
	var self = this;
	rules.forEach(function(rule) {
		var strategySet = rule.strategy.split(':');
		self.cache.push(function() {
			var strategyName = strategySet.shift();
			strategySet.unshift(item);
			strategySet.push(rule.errorMsg);
			return strategies[strategyName].apply(self, strategySet);
		});
	});
};

Validator.prototype.start = function() {
	var errorMsg;
	this.cache.some(function(validatorFunc) {
		errorMsg = validatorFunc();
		if (errorMsg) {
			return true;
		}
	});
	return errorMsg;
};

簡單來說就是將物件

{
	strategy: 'minLength:6',
	errorMsg: '密碼位數不得少於6'
}

所指定的策略名稱和錯誤顯示訊息丟給strategies物件來得到所封裝的function,再將此function記起來(丟入cache陣列中),真的檢查的時候直接執行即可。
接下來只要將你做好的myFormValidator綁到前端物件上就完成啦

var $form = getForm();
$form.submit(onSubmit);

function onSubmit() {
	var form = this;
	var errorMsg = myFormValidator(form);
	if (errorMsg) {
		alert(errorMsg);
		return false;
	}
}

最後附上完整CODE:

var $form = getForm();
$form.submit(onSubmit);

function onSubmit() {
	var form = this;
	var errorMsg = myFormValidator(form);
	if (errorMsg) {
		alert(errorMsg);
		return false;
	}
}
var strategies = {
	inNotEmpty: function(val, errorMsg) {
		if (val === '') {
			return errorMsg;
		}
	},
	minLength: function(val, length, errorMsg) {
		if (val.length < length) {
			return errorMsg;
		}
	},
	isMobileNumber: function(val, errorMsg) {
		if (!/^[09]{2}[0-9]{8}$/.test(val)) {
			return errorMsg;
		}
	}
};

function myFormValidator(form) {
	var validator = new Validator();
	validator.add(form.userName.value, [{
		strategy: 'inNotEmpty',
		errorMsg: '使用者名稱不為空'
	}, {
		strategy: 'minLength:6',
		errorMsg: '使用者名稱位數不得少於6'
	}]);
	validator.add(form.password.value, [{
		strategy: 'minLength:6',
		errorMsg: '密碼位數不得少於6'
	}]);
	validator.add(form.phoneNumber.value, [{
		strategy: 'isMobileNumber',
		errorMsg: '手機號碼錯誤'
	}]);
	var errorMsg = validator.start();
	return errorMsg;
}

var Validator = function() {
	this.cache = [];
};

Validator.prototype.add = function(item, rules) {
	var self = this;
	rules.forEach(function(rule) {
		var strategySet = rule.strategy.split(':');
		self.cache.push(function() {
			var strategyName = strategySet.shift();
			strategySet.unshift(item);
			strategySet.push(rule.errorMsg);
			return strategies[strategyName].apply(self, strategySet);
		});
	});
};

Validator.prototype.start = function() {
	var errorMsg;
	this.cache.some(function(validatorFunc) {
		errorMsg = validatorFunc();
		if (errorMsg) {
			return true;
		}
	});
	return errorMsg;
};

這樣寫的好處:
1.避免過多重複代碼,若類似的表單要用到這三個規則的直接套用myFormValidator就好。
2.組合彈性高,若有不同規則組合的表單也可以像製作myFormValidator一樣做一個新的檢查規則。
3.易於擴展,若有新的演算法的話只要新增在strategies裡面即可。

好以上就是策略模式!!


上一篇
JS Design Pattern Day01-單例模式 Singleton
下一篇
JS Design Pattern Day03-代理模式 Proxy
系列文
JS Design Pattern 30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
糜糜卯卯
iT邦新手 5 級 ‧ 2019-08-19 18:56:56

https://www.youtube.com/watch?v=IkG_KuMpQRM
最近看到的「水球潘」大大的教學,教得超棒的
配合書本、大大的文章一起研讀

我要留言

立即登入留言