程式碼要寫成別人看不懂的樣子(十)

EcbJS發表於2020-11-13

本篇文章參考書籍《JavaScript設計模式》–張容銘

前言

  各位寫程式碼的時候,經常會出現條件判斷吧,那麼條件判斷裡面臉最熟的當屬 if 了吧,這個東西在我們開始編碼的時候,真的是隔一段時間不寫,就渾身難受。

  但是呢,if 這個東西,哪怕是再簡單的判斷,也會有隱含問題,而且在程式碼可讀性上,條件判斷的效果很不友好,當存在多重判斷的時候,簡直就是災難。

  所以為了解決條件判斷的這些弊端,狀態模式應運而生。

狀態模式

  狀態模式:當一個物件的內部狀態發生改變時,會導致其行為的改變,這看起來像是改變了物件。

  我們可以將不同的狀態結果封裝在狀態物件內部,然後該狀態物件返回一個可被呼叫的介面方法,用於呼叫狀態對下內部某種方法。做法如下:

//狀態物件
var ResultState = function() {
	//結果儲存在內部狀態中
	var States = {
		//每種狀態作為一種獨立方法儲存
		state0:funciton() {
			//處理結果0
			console.log('這是第一種情況');
		},
		state1:funciton() {
			//處理結果1
			console.log('這是第二種情況');
		},
		state2:funciton() {
			//處理結果2
			console.log('這是第三種情況');
		},
		state3:funciton() {
			//處理結果3
			console.log('這是第四種情況');
		},
	}
	//獲取某一種狀態,並執行其方法
	function show(result) {
		States['state' + result] && States['state' + result]();
	}
	return {
		//返回撥用狀態方法介面
		show: show
	}
} ();

  當我們想呼叫第三種結果的時候,我們就可以按照下面這種方法實現。

//展示結果3
ResultState.show(3);

  上面方法展示了狀態模式的基本雛形,該模式主要目的就是將條件判斷的不同結果轉化為狀態物件的內部狀態。由於是內部狀態,所以建立時設定成私有變數,只提供一個介面給外部進行增刪改查,方便了我們對狀態物件中,內部物件的管理。

  大家小時候都玩過超級瑪麗吧,我們操作人物的時候可以跳躍,奔跑,蹲下,丟飛鏢等,這些都是一個一個的狀態,如果我們用 if 或者 switch 條件判斷的話,那麼後期會相當難維護,因為增加或者刪除一個狀態需要修改的地方太多了,這時使用狀態模式幫我們管理就再合適不過了。

  對於超級瑪麗,有的時候需要跳起來丟飛鏢,有的時候需要蹲下丟飛鏢,有的時候需要跑起來丟,這些組合狀態如果用 if 或者 switch 判斷的話,無形當中增加的成本是無法想象的。例如:

//單動作條件判斷 每增加一個動作就需要新增一個判斷
var lastAction = '';
function changeMarry(action) {
	if(action = 'jump') {
		//跳躍動作
	} else if(action = 'move') {
		//移動動作
	} else {
		//預設情況
	}
	lastAction = action;
}
//符合動作對條件判斷的開銷是翻倍的
var lastAction1 = '';
var lastAction2 = '';
function changeMarry(action1, action2) {
	if(action1 = 'throw') {
		//丟飛鏢
	} else if(action1 = 'jump') {
		//跳躍
	} else if(action1 = 'jump' && action2 = 'throw') {
		//跳起來扔飛鏢
	} else if(action1 = 'move' && action2 = 'throw') {
		//移動中扔飛鏢
	}
	//保留上一個動作
	lastAction1 = action1 || '';
	lastAction2 = action2 || '';
}

  上面程式碼的可維護性和可讀性都是很差的,日後對於新動作的新增或者修改原有動作的成本都是很大的。那麼為了解決這一問題我們引入狀態模式。解決思路如下:

  1.建立一個狀態物件。

  2.內部儲存狀態變數。

  3.內部封裝好每種動作對應的狀態。

  4.狀態物件返回一個介面物件,可以對內部狀態進行修改或者呼叫。

//建立超級瑪麗狀態類
var MarryState = function() {
	//內部狀態私有變數
	var _currentState = {},
		//動作與狀態方法對映
		states = {
			jump: function() {
				//跳躍
				console.log('jump');
			},
			move: function() {
				//移動
				console.log('move');
			},
			throw: function() {
				//丟飛鏢
				console.log('throw');
			},
			squat: function() {
				//蹲下
				console.log('squat');
			}
		};
	//動作控制類
	var Action = {
		//改變狀態方法
		changeState: function() {
			//組合動作通過傳遞多個引數實現
			var arg = arguments;
			//重置內部狀態
			_currentState = {};
			//如果有動作則新增動作
			if(arg.length) {
				//遍歷動作
				for(var i = 0; len = arg.length; i < len; i++) {
					//向內部狀態中新增動作
					_currentState[arg[i]] = true;
				}
			}
			//返回動作控制類
			return this;
		},
		//執行動作
		goes: function() {
			console.log('觸發一次動作');
			//遍歷內部狀態儲存的動作
			for(var i in currentState) {
				//如果該動作存在則執行
				states[i] && states[i]();
			}
			return this;
		}
	}
	//返回介面方法change ,goes
	return {
		change: Action.changeState,
		goes: Action.goes
	}
}

  超級瑪麗的狀態建立完成了,接下來就是使用了。

//建立一個超級瑪麗
var marry = new MarryState();
marry
	.change('jump', 'throw')  //新增跳躍與丟飛鏢動作
	.goes()          //執行動作
	.goes()          //執行動作
	.change('throw') //新增丟飛鏢動作
	.goes();         //執行動作

  輸出結果如下:

//觸發一次動作
//jump
//throw
//觸發一次動作
//jump
//throw
//觸發一次動作
//throw

  改變狀態類一個狀態,就改變了狀態物件的執行結果,是不是有點類似物件,讓這些狀態管理一下清晰了起來。

策略模式

  各位最近剛過完雙十一吧,參與了將近五千億的專案,感覺怎麼樣?湊滿減的時候是不是感覺自己的數學水平又重新到達巔峰了。雙十一活動有很多哈,比如滿300減40,滿200減25等等,有的商品不參與滿減活動,但是可以先付定金,然後再付尾款,這一點像不像上面的剛介紹的狀態模式,會用到很多條件判斷。

  但是,情況又不太一樣,不管商品採用哪種方案,一般都會只選取其中一種,比如該商品滿300減40了,就不參與滿200減25的活動了,如果使用狀態模式的話,那就會為每一種商品,建立一個狀態物件,這樣做就會產生程式碼冗餘。

  這個時候就需要用策略模式來解決當前問題了。策略模式結構上與狀態模式很像,都是再內部封裝一個物件,然後返回的介面物件實現對內部物件的呼叫。

  不同點是,策略模式不需要管理狀態,狀態間沒有依賴關係,策略之間可以相互替換,在策略物件內部儲存的是相互獨立的一些演算法。

//價格策略物件
var PriceStrategy = function() {
	//內部演算法物件
	var strategy= {
		//滿300減40
		reduce40: function(price) {
			return +price - ~~(price / 300) * 40; //~~表示parseInt
		},
		//滿200減25
		reduce25: function(price) {
			return +price - ~~(price / 200) * 25;
		},
		//定金50付尾款
		deposit: function(price) {
			return +price - 50;
		},
	}
	//策略演算法呼叫介面
	return function(algorithm, price) {
		//如果演算法存在則呼叫演算法,否則返回false
		return strategy[algorithm] && strategy[algorithm](price)
	}
} ();

  策略物件已經寫出來了,我們可以看看怎麼獲取。

var price = PriceStrategy('reduce40', 1000);
console.log(price);    // 880

  策略模式其實應用的場景很多,比如 JQuery 動畫中的緩衝函式,驗證表單時候的正則演算法。

//表單正則策略物件
var InputStrategy = function() {
	var strategy = {
		//是否為空
		notNull: funetion(value) {
			return /\s+/.test(value) ? '請輸入內容' : '';
		},
		//是否是一個數字
		number: funetion(value) {
			return /^[0-9]+(\.[0-9]+)?$/.test(value) ? '' : '請輸入數字';
		},
	}
	return {
		//驗證介面 type 演算法 value 表單值
		check: function(type, value) {
			//去除收尾空白符
			value = value.replace(/^\s+|\s+$/g, '');
			return strategy[type] ? strategy[type](value) : '沒有該型別檢測方法';
		},
		addStrategy: function(type, fn) {
			strategy[type] = fn;
		}
	}
} ();

  上面例子中,我們新增加了一個 addStrategy 用來增加新的策略,這個介面可以幫我們不用修改策略物件內部方法,便可以新增新的策略。用法如下:

//擴充可延續演算法
InputStrategy.addStrategy('nickname', function(value) {
	return /^[a-zA-Z]\w{3,7}$/.test(value) ? '' : '請輸入4-8位暱稱, 如:LHXX';
});

  本片文章的兩個設計模式還是很常見的,也比較簡單,大家用一兩次就能學會,使用後會給以後需求增加帶來很多方便。




相關文章