Javascript設計模式(四)策略模式

YiHzo發表於2018-11-11

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

在現實中,如果我們想去某個地方旅遊,可以根據實際情況有多種路線

  • 如果沒有時間但是不在乎錢,可以選擇飛機
  • 如果沒有錢,可以選擇大巴活著火車
  • 如果再窮一點,可以選擇騎自行車

使用策略模式計算獎金

現在以年終獎的計算為例

公司年終獎根據員工的工資基數年底績效來發放

  • 績效S,四倍年終獎
  • 績效A,三倍年終獎
  • 績效B,二倍年終獎

最初的實現

var calculateBonus = function(performanceLevel, salary) {
	if (performanceLevel === 'S') {
		return salary*4
	}
	if (performanceLevel === 'A') {
		return salary*3
	}
	if (performanceLevel === 'B') {
		return salary*2
	}
}
calculateBonus('B', 2000) // 4000
calculateBonus('S', 2000) // 8000
複製程式碼

這段程式碼簡單,但是存在顯而易見的缺點

  1. 函式比較龐大,包含很多if-else語句,這些語句需要覆蓋所有的邏輯分支
  2. 缺乏彈性,如果想新增績效C,就得深入函式內部實現,違反開放-封閉原則
  3. 演算法的複用性差

策略模式的實現

var strategies = {
	"S": function(salary) {
		return salary * 4
	},
	"A": function(salary) {
		return salary * 3
	},
	"B": function(salary) {
		return salary * 2
	}				
}
var calculateBonus = function(level, salary) {
	return strategies[level](salary)
}
console.log(calculateBonus('S', 2000)) // 8000
console.log(calculateBonus('B', 2000)) // 4000
複製程式碼

通過使用策略模式重構程式碼,消除來原程式中分支語句。所有計算獎金有關的邏輯分佈在策略物件中,每個策略物件的演算法已被各自封裝在物件內部,當我們對這些策略物件發出“計算獎金”的請求時,它們會返回各自的計算結果,這不僅是多型性的體現,也是“自由交換”的目的。

使用策略模式實現緩動動畫

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1">
		<title></title>
	</head>
	<body>
	<div id="div" style="background: #141527;display: inline-block;position: relative;">我說div</div>
	<script type="text/javascript">
/**
 * 緩動演算法
 * @t 已消耗的時間
 * @b 小球原始位置
 * @c 小球目標位置
 * @d 動畫持續的總時間
 */
var tween = {
        Linear: function(t,b,c,d){ return c*t/d + b; },
	easeIn: function(t,b,c,d){ return c*(t/=d)*t + b;},
	easeOut: function(t,b,c,d){ return -c *(t/=d)*(t-2) + b;},
	easeInOut: function(t,b,c,d){
		if ((t/=d/2) < 1) return c/2*t*t + b;
		return -c/2 * ((--t)*(t-2) - 1) + b;
	}
}

/**
 * 定義Animate類
 */
var Animate = function(dom) {
	this.dom = dom;			// 運動的元素
	this.startTime = 0;		// 動畫開始時間
	this.startPos = 0;		// 元素初始位置
	this.endPos = 0;		// 元素結束位置
	this.propertyName = '';		// 實現動畫的元素屬性
	this.easing = null;		// 緩動演算法
	this.duration = 0;		// 動畫持續的時間
};

/**
 * 啟動動畫
 */
Animate.prototype.start = function(propertyName, endPos, duration, easing) {
	this.startTime = new Date();		// 初始化動畫開始的時間
	this.startPos = this.dom.getBoundingClientRect()[propertyName];
	this.propertyName = propertyName;
	this.endPos = endPos;
	this.duration = duration;
	this.easing = tween[easing];		// 緩動演算法
	var that = this;
	var timed = setInterval(function() {
		// 執行每幀操作
		if(that.step() === false) {		// 動畫已結束
			clearInterval(timed);
		}
	}, 19);
};

// 判斷當前動畫狀態,呼叫update
Animate.prototype.step = function() {
	var t = new Date();		// 執行動畫的當前時間
	if( t.getTime() >= this.startTime.getTime() + this.duration) {		// 動畫已結束
		this.update(this.endPos);
		return false;
	}
	var pos = this.easing(t - this.startTime, this.startPos, this.endPos - this.startPos, this.duration);
	this.update(pos);
};

// 計算位置更新屬性
Animate.prototype.update = function(pos) {
	this.dom.style[this.propertyName] = pos + 'px';
};

var div = document.getElementById('div')
var animate = new Animate(div)
animate.start('top', 500, 1000, 'easeInOut')

	</script>	
	</body>
</html>
複製程式碼

用策略模式實現表單驗證

從定義上看,策略模式就是用來封裝演算法的。但是如果僅僅用來封裝演算法,未免有點大材小用。在實際業務中,策略模式也可以用來封裝一系列的“業務規則”。只要業務規則指向的目標一致,並且可以被替換使用,我們就可以用策略模式來封裝它們。

普通版本的表單驗證

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8" />
	<meta name="viewport" content="width=device-width, initial-scale=1">
	<title></title>
</head>
<body>
<form action="http://xxx.com/register" id="registerForm" method="post">
	請輸入使用者名稱<input type="text" name="username"/><br />
	請輸入密碼<input type="text" name="password"/><br />
	請輸入手機號<input type="text" name="phonenumber"/><br />
	<input type="submit" value="提交" style="padding: 10px 20px;">
</form>
<script type="text/javascript">
        var registerForm = document.getElementById('registerForm')
        
        registerForm.onsubmit = function() {
	if (registerForm.username.value === '') {
		alert('使用者名稱不能為空')
		return false
	}
	if (registerForm.password.value.length < 6) {
		alert('密碼長度不能小於6位')
		return false
	}
	if (!/(^1[3|5|8][0-9]{9}$)/.test(registerForm.phonenumber.value) ){
		alert('手機號碼格式不正確')
		return false
	}						
}
</script>	
</body>
</html>
複製程式碼

這是一種很常見的編碼方式,可以看到缺點和計算獎金一摸一樣

用策略模式重構表單驗證

  1. 很明顯第一步我們需要將驗證邏輯封裝成策略物件
var strategies = {
	isNonEmpty: function(value, errorMsg) {
		if (value === '') {
			return errorMsg
		}
	},
	minLength: function(value, length, errorMsg) {
		if (value.length < length) {
			return errorMsg
		}
	},
	isMobile: function(value, errorMsg) {
		if (!/(^1[3|5|8][0-9]{9}$)/.test(value)) {
			return errorMsg
		}
	}
}
複製程式碼
  1. 接下來實現Validator類,負責接受使用者的請求並委託給strategies
var Validator = function() {
	//儲存校驗規則
	this.cache = [] 
}

// 新增校驗
Validator.prototype.add = function(dom, rules) {
	var self = this
	// 遍歷校驗規則
	for(var i = 0, rule; rule = rules[i++];) { 
		(function(rule){
			//把strategy和引數分開
			var strategyAry = rule.strategy.split(':') 	
			var errorMsg = rule.errorMsg	
			// 把校驗的步驟用空函式包裝起來,並且放入cache
			self.cache.push(function(){	
				// 挑選出校驗規則
				var strategy = strategyAry.shift()
				// 把input的value新增進引數列表
				strategyAry.unshift(dom.value)		
				// 把errorMsg新增進引數列表
				strategyAry.push(errorMsg)			
				return strategies[strategy].apply(dom, strategyAry)
			})
		})(rule)
	}
}

// 啟動校驗
Validator.prototype.start = function() {
	for (var i = 0, validatorFunc; validatorFunc = this.cache[i++];) { 
		// 開始校驗,並取得校驗後的結果
		var errorMsg = validatorFunc() 
		if (errorMsg) {
			return errorMsg
		}
	}
}

複製程式碼
  1. 接下來就是呼叫了
var registerForm = document.getElementById('registerForm')

var validataFunc = function() {
	var validator = new Validator()
	validator.add(registerForm.username, [
			{
				strategy: 'isNonEmpty',
				errorMsg: '使用者名稱不能為空'
			},
			{
				strategy: 'minLength:10',
				errorMsg: '使用者名稱長度不能小於10位'						
			}
		]
	)
	validator.add(registerForm.password, [
			{
				strategy: 'minLength:6',
				errorMsg: '密碼長度不能小於6位'						
			}
		]
	)
	validator.add(registerForm.phonenumber, [
			{
				strategy: 'isMobile',
				errorMsg: '手機號碼格式不正確'						
			}
		]
	)								
	var errorMsg = validator.start()
    return errorMsg							
}

var sub = document.querySelector('input[type="submit"]')
sub.onclick = function() {
	var errorMsg = validataFunc()
	if (errorMsg) {
		console.error(errorMsg)
		return false
	}
}
複製程式碼

使用策略模式重構程式碼之後,我們不僅通過“配置”的方式就可以完成一個表單的校驗,這些規則也可以複用在程式的任何地方,還能以外掛的形式,方便地移植到其他專案中。並且新增或者修改規則也是毫不費力的。

策略模式的優缺點

優點

  1. 策略模式利用組合,委託和多型等技術思想,可以有效避免多重條件選擇語句。
  2. 策略模式提供了對開放-封閉原則的完美支援。將演算法封裝在獨立的strategy中,使得它們易於切換,易於理解,易於擴充套件。
  3. 策略模式中的演算法也可以複用在系統中的其他地方。
  4. 在策略模式中利用組合和委託讓Content擁有執行演算法的能力,這也是繼承的一種更輕便的替代方案。

缺點

  1. 使用策略物件會增加很多策略類或者策略物件,但實際上比把這些邏輯放在Content更好。
  2. 策略模式會向使用者暴露所有實現細節,這其實是違反最少知識原則。

相關文章