[TOC]
# 狀態模式
狀態模式的關鍵是區分事物內部的狀態,事物內部狀態的改變往往會帶來事物的行為改變。
## 初識狀態模式
狀態模式的關鍵是把事物的每種狀態都封裝成單獨的類,跟此種狀態有關的行為都被封裝在這個類的內部,只需要在上下文中,把某個請求委托給當前的狀態對象即可,該狀態對象會福州渲染它自身的行為。
```javascript
// offLightState
var OffLightState = function(light){
this.light = light;
};
OffLightState.prototype.buttonWasPressed = function(){
console.log('弱光') // offLightState 對應的行為
this.light.setState(this.light.weekLightState); // 切換狀態到 weekLightState
};
// WeekLightState
var WeekLightState = function(light){
this.light = light;
};
WeekLightState.prototype.buttonWasPressed = function(){
console.log('強光')
this.light.setState(this.light.strongLightState);
};
// StrongLightState
var StrongLightState = function(light){
this.light = light;
};
StrongLightState.prototype.buttonWasPressed = function(){
console.log('關燈')
this.light.setState(this.light.offLightState);
};
// Light 類
var Light = function(){
this.offLightState = new OffLightState(this);
this.weekLightState = new WeekLightState(this);
this.strongLightState = new StrongLightState(this);
this.button = null;
};
Light.prototype.init = function(){
var button = document.createElement('button');
var self = this;
this.button = document.body.appendChild(button);
this.button.innerHTML = '開關';
this.currState = this.offLightState;
this.button.onclick = function(){
self.currState.buttonWasPressed();
};
};
Light.prototype.setState = function(newState){
this.currState = newState;
};
var light = new Light();
light.init();
```
狀態模式可以使每一種狀態和它對應的行為之間的關系局部化,這些行為被分散和封裝在各自對應的狀態類中。
## 狀態模式的定義
GOF中定義:*允許一個對象在其內部狀態改變時改變它的行為,對象開起來似乎改變了它的類*。
- 將狀態封裝成獨立的類,并將請求委托給當前的狀態對象,當對象的內部狀態改變時,會帶來不同的行為變化。
- 使用的對象,在不同的狀態下具有截然不同的行為,這個對象看起來是從不同的類中實例化而來的,實際上是使用了委托的效果。
## 狀態模式的通用結構
首先定義了`Ligth`類,`Light`類在這里也被稱為上下文(`Context`)。隨后在`Light`的構造函數中,我們要創建每一個狀態類的實例對象,`Context`將持有這些狀態對象的引用,以便把請求委托給狀態對象。
```javascript
// Light 類
var Light = function(){
this.offLightState = new OffLightState(this);
this.weekLightState = new WeekLightState(this);
this.strongLightState = new StrongLightState(this);
this.button = null;
};
Light.prototype.init = function(){
var button = document.createElement('button');
var self = this;
this.button = document.body.appendChild(button);
this.button.innerHTML = '開關';
this.currState = this.offLightState;
this.button.onclick = function(){
self.currState.buttonWasPressed();
};
};
```
最后再編寫各種狀態類,`ligth`對象被傳入狀態類的構造函數,狀態對象也需要持有`ligth`對象的引用,以便調用`ligth`中的方法或者直接操作`light`對象
```javascript
// offLightState
var OffLightState = function(light){
this.light = light;
};
OffLightState.prototype.buttonWasPressed = function(){
console.log('弱光') // offLightState 對應的行為
this.light.setState(this.light.weekLightState); // 切換狀態到 weekLightState
};
```
## 缺少抽象類的變通方式
與模板方法模式中相同,讓抽象父類的抽象方法直接拋出一個異常。
```javascript
var State = function(){};
State.prototype.buttonWasPressed = function(){
throw new Error('父類的 buttonWasPressed 方法必須被重寫');
};
var SuperStrongLigthState = function(light){
this.light = light;
};
SuperStrongLigthState.prototype = new State(); // 繼承抽象父類
SuperStrongLigthState.prototype.buttonWasPressed = function(){
....
};
```
## 另一個狀態模式示例 -- 文件上傳
第一步提供`window.external.upload`函數,在頁面中模擬上傳插件
```javascript
window.external.upload = function(state){
console.log(state) // 可能是sign,uploading,done,error
};
var plugin = (function(){
var plugin = document.createElement('embed');
plugin.style.display = 'none';
plugin.type = 'application/txfn-webkit';
plugin.sign = function(){
...
};
plugin.pause = function(){
...
};
plugin.uploading = function(){
...
};
plugin.del = function(){
...
};
plugin.done = function(){
...
};
document.body.appendChild(plugin);
return plugin;
})();
```
第二步編寫`Upload`函數
```javascript
var Upload = function(filename){
this.plugin = plugin;
this.fileName = filename;
this.button1 = null;
this.button2 = null;
this.signState = new SignState(this); // 設置初始狀態為waiting
this.uploadingState = new UploadingState(this);
this.pauseState = new PauseState(this);
this.doneState = new DoneState(this);
this.errorState = new ErrorState(this);
this.currState = this.signState; // 設置當前狀態
}
```
第三步創建`Upload.prototype.init`
```javascript
Upload.prototype.init = function(){
var that = this;
this.dom = document.createElement(div);
this.dom.innerHTML =
'<span>文件名稱' + this.fileName + '</span>\
<button data-action="button1">掃描中</button>\
<button data-action="button2">刪除</button>';
document.body.appendChild(this.dom);
this.button1 = this.dom.querySelector('[data-action="button1"]');
this.button2 = this.dom.querySelector('[data-action="button2"]');
this.bindEvent();
};
```
第四步負責具體的按鈕事件實現,在點擊了按鈕之后,Context 并不做任何具體的操作,而是把請求委托給當前的狀態類來執行
```javascript
Upload.prototype.bindEvent = function(){
var self = this;
this.button1.onclick = function(){
self.currState.clickHandler1();
}
this.button2.onclick = function(){
self.currState.clickHandler2();
}
};
Upload.prototype.sign = function(){
this.plugin.sign();
this.currState = this.signState;
};
Upload.prototype.uploading = function(){
this.plugin.uploading();
this.currState = this.uploadingState;
};
Upload.prototype.pause = function(){
this.plugin.pasue();
this.currState = this.pauseState;
};
Upload.prototype.done = function(){
this.plugin.done();
this.currState = this.doneState;
};
Upload.prototype.error = function(){
this.plugin.error();
this.currState = this.errorState;
};
Upload.prototype.del = function(){
this.plugin.del();
this.currState = this.delState;
};
```
第五步編寫各個狀態類的實現
```javascript
var StateFactory = (function(){
var State = function(){};
State.prototype.clickHanlder1 = function(){
throw new Error('子類必須重寫父類的方法');
}
State.prototype.clickHanlder2 = function(){
throw new Error('子類必須重寫父類的方法');
}
return function(param){
var F = function(uploadObj){
this.uploadObj = uploadObj;
};
F.prototype = new State();
for(var i in param){
F.prototype[i] = param[i];
}
return F;
}
})();
var SignState = StateFactory({
clickHandle1: function(){
console.log(...)
},
clickHandle2: function(){
console.log(...)
}
});
var UploadingState = StateFactory({
clickHandle1: function(){
this.uploadObj.pause();
},
clickHandle2: function(){
console.log(...)
}
});
// 其它的狀態同理
...
```
測試
```javascript
var uploadObj = new Upload('xxx');
uploadObj.init();
window.external.upload = function(state){
uploadObj[state]();
};
window.external.upload('sigin')
```
## 狀態模式的優缺點
優點:
- 狀態模式定義了狀態與行為之間的關系,并將它們封裝在一個類里。通過增加新的狀態類,很容易增加新的狀態和轉換。
- 避免 Context 無限膨脹,狀態切換和邏輯被分布在狀態類中,也去掉了 Context 中原本過多的條件分支。
- 用對象代替字符串來記錄當前狀態,使得狀態的切換更加一目了然。
- Context 中的請求動作和狀態類中封裝的行為可以非常容易地獨立變化而互不影響。
缺點:
會在系統中定義許多的狀態類,而且系統中會因此增加不少對象,由于邏輯分散在狀態類中,也造成了邏輯分散的問題,無法在一個地方就看出整個狀態機的邏輯。
## 狀態模式中的性能優化點
可以在僅當 state 對象被需要時才創建并銷毀,另一種是一開始就創建好所有的狀態對象,并且始終不銷毀它們。
## 狀態模式和策略模式的關系
策略模式和狀態模式的相同點是,它們都有一個上下文、一些策略或者狀態類,上下文把請求委托給這些類來執行。
它們之間的區別是策略模式中的各個策略類之間是平等又平行的,它們之間沒有任何聯系,所以客戶必須熟知這些策略類的作用,以便客戶隨時可以主動切換算法;而在狀態模式中,狀態和狀態對應的行為是早已被封裝好的,狀態之間的切換也早被規定完成,“改變行為”這件事情在狀態內部。
## JavaScript版本的狀態機
```javascript
var delegate = function(client, delegation){
return {
buttonWasPressed: function(){ // 將客戶的操作類委托給 delegation 對象
return delegation.buttonWasPressed.apply(client, argument);
}
}
};
var FSM = {
off: {
buttonWasPressed: function(){
console.log(...);
this.currState = this.onState;
}
},
on: {
buttonWasPressed: function(){
console.log(...);
this.currState = this.offState;
}
}
};
var Light = function(){
this.offState = delegate(this, FSM.off);
this.onState = delegate(this, FSM.on);
this.currState = this.offState; // 設置初始狀態
this.button = null;
};
Light.prototype.init = function(){
var button = document.createElement('button'),
self = this;
this.button = document.body.appendChild(button);
this.button.click = function(){
self.currState.buttonWasPressed();
}
};
var light = new Light();
light.init();
```
## 表驅動的有限狀態機
可以在標準很清楚地看到下一個狀態是由當前狀態和行為共同決定的,這樣一來,我們就可以在表中查找狀態,而不必定義很多條件分支。
[https://github.com/jakesgordon/javascript-state-machine/](https://github.com/jakesgordon/javascript-state-machine/)