狀態模式

狀態模式是一種非同尋常的優秀模式,它也許是解決某些需求場景的最好方法。雖然狀態模式並非一種簡單到一目瞭然的模式(它每每還會帶來代碼量的增長),但你一旦明白了狀態模式的精髓,之後必定會感謝它帶給你的無與倫比的好處。javascript

狀態模式的關鍵是區分事物內部的狀態,事物內部的狀態每每會帶來事物的行爲改變。html

1. 初識狀態模式

咱們來想像這樣一個場景:有一個電燈,電燈上面只有一個開關。當電燈開着的時候,此時按下開關,電燈會切換到關閉狀態;再按一次開關,電燈又將被打開。同一個開關按鈕,在不一樣的狀態下,表現出來的行爲是不同的。java

如今用代碼來描述這個場景,首先定義一個 Light 類,能夠預見,電燈對象 light 將從 Light 類建立而出,light 對象將擁有兩個屬性,咱們用 state 來記錄電燈當前的狀態,用 button 表示具體的開關按鈕。下面來編寫這個電燈程序的例子。程序員

1. 1 電燈程序

首先給出不用狀態模式的電燈程序實現:算法

var Light = function () {
    this.state = 'off'; //給電燈設置初始狀態
    this.button = null; //電燈開關按鈕
};

接下來定義 Light.prototype.init 方法,該方法負責在頁面中建立一個真實的 button 節點,假設這個 button 就是電燈的開關按鈕,當 button 的 onclick 事件被觸發時,就是電燈開關被按下的時候,代碼以下:設計模式

Light.prototype.init = function () {
    var button = document.createElement('button');
    self = this;
    button.innerHTML = '開關';
    this.button = document.body.appendChild(button);
    this.button.onclick = function () {
        self.buttonWasPressed();
    }
};

當開關被按下時,程序會調用 self.buttonWasPressed 方法,開關按下以後的全部行爲,都被封裝在這個方法裏,代碼以下:性能優化

Light.prototype.buttonWasPressed = function () {
    if (this.state === 'off'){
        console.log('開燈');
        this.state = 'on';
    } else if (this.state === 'on'){
        console.log('關燈');
        this.state = 'off';
    }
};

var light = new Light();
light.init();

OK ,如今能夠看到,咱們已經編寫了一個強壯的狀態機,這個狀態機的邏輯簡單又縝密,看起來這段代碼設計得無懈可擊,這個程序沒有任何 bug 。實際上這種代碼咱們已經編寫過無數次,好比要交替切換一個 button 的 class ,跟此例同樣,每每先用一個變量 state 來記錄按鈕的當前行爲,在事件發生時,再根據這個狀態來決定下一步的行爲。閉包

使人遺憾的是,這個世界上的電燈並不是只有一種。許多酒店裏有另一種電燈,這種電燈也只有一個開關,但它表現的是:第一次按下打開弱光,第二次按下打開強光,第三次纔是關閉電燈。如今必須改造上面的代碼來完成這種新型電燈的製造:app

Light.prototype.buttonWasPressed = function () {
    if (this.state === 'off'){
        console.log('弱光');
        this.state = 'weakLight';
    } else if (this.state === 'weakLight'){
        console.log('強光');
        this.state = 'strongLight';
    } else if (this.state === 'strongLight') {
        console.log('關燈');
        this.state = 'off';
    }
};

如今這個反例先告一段落,咱們來考慮一下上述程序的缺點。函數

  • 很明顯 buttonWasPressed 方法是違反開放——封閉原則的,每次新增或者修改 light 的狀態,都須要改動 buttonWasPressed 方法中的代碼,這使得 buttonWasPressed 成爲了一個不穩定的方法。
  • 全部跟狀態有關的行爲,都被封裝在 buttonWasPressed 方法裏,若是之後這個電燈又增長了強強光,超強光和終極強光,那咱們將沒法預計這個方法將膨脹到什麼地步。固然爲了簡化示例,此處在狀態發生改變的時候,只是簡單的打印一條 log 和改變 button 的 innerHTML 。在實際開發中,要處理的事情可能比這多得多,也就是說,buttonWasPressed 方法要比如今龐大的多。
  • 狀態的切換很是不明顯,僅僅表現爲對 state 變量賦值,好比 this.state = 'weakLight' 。在實際開發中,這樣的操做很容易被程序員不當心漏掉。咱們也沒有辦法一目瞭然的明白電燈一共有多少狀態,除非耐心的讀完 buttonWasPressed 方法裏的全部代碼。當狀態的種類多起來的時候,某一次切換的過程就好像被埋藏在一個巨大方法裏的某個陰暗角落裏。
  • 狀態之間的切換關係,不過是往 buttonWasPressed 方法裏堆砌 if ,else 語句,增長或者修改一個狀態可能須要改變若干個操做,這使得 buttonWasPressed 更加難以閱讀和維護。

1. 2 狀態模式改進電燈程序

囉嗦了一大堆,如今咱們來學習狀態模式改進電燈的程序。有意思的是,一般咱們談到封裝,通常都會優先封裝對象的行爲,而不是對象的狀態。但在狀態模式中恰好相反,狀態模式的關鍵是把事物的每種狀態都封裝成單獨的類,跟此種狀態有關的行爲都被封裝在這個類的內部,因此 button 被按下的時候,只須要在上下文中,把這個請求委託給當前的狀態便可,該狀態對象會負責渲染它自身的行爲。

同時咱們還能夠把狀態的切換規則實現分佈在狀態類中,這樣就有效的消除了本來存在的大量條件分支語句。

下面進入狀態模式的代碼編寫階段,首先將定義 3 個狀態類,分別是 OffLightState ,WeakLightState ,StrongLightState 。這 3 個類都有一個原型方法 buttonWasPressed ,表明在各自的狀態下,按鈕被按下時將發生的行爲,代碼以下:

var OffLightState = function (light) {
    this.light = light;
};
OffLightState.prototype.buttonWasPressed = function () {
    console.log('弱光');  // offLightState 對應的行爲
    this.light.setState(this.light.weakLightState)  //切換狀態到 weakLightState
};

var WeakLightState = function (light) {
    this.light = light;
};
WeakLightState.prototype.buttonWasPressed = function () {
    console.log('強光');
    this.light.setState(this.light.strongLightState);
};

var StrongLightState = function (light) {
    this.light = light;
};
StrongLightState.prototype.buttonWasPressed = function () {
    console.log('關燈');
    this.light.setState(this.light.offLightState);
};

接下來改寫 Light 類,如今再也不使用一個字符串來記錄當前的狀態,而是使用更加立體化的狀態對象。咱們在 Light 類的構造函數裏爲每一個狀態類都建立一個狀態對象,這樣一來咱們能夠很明顯的看到電燈一共有多少種狀態,代碼以下:

var Light = function () {
    this.offLightState = new OffLightState(this);
    this.weakLightState = new WeakLightState(this);
    this.strongLightState = new StrongLightState(this);
    this.button = null;
};

在 button 按鈕被按下的事件裏,Context 也再也不直接進行任何實質性的操做,而是經過 self.currState.buttonWasPressed() 將請求委託給當前持有的狀態對象去執行,代碼以下:

Light.prototype.init = function () {
    var button = document.createElement('button'),
    self = this;
    button.innerHTML = '開關';
    this.button = document.body.appendChild(button);
    this.currState = this.offLightState;    //設置當前狀態
    this.button.onclick = function () {
        self.currState.buttonWasPressed();
    };
};

最後還要提供一個 Light.prototype.setState 方法,狀態對象能夠經過這個方法來切換 light 對象的狀態。前面已經說過,狀態的切換規律事先被無缺定義在各個狀態類中。在 Context 中再也找不到任何一個跟狀態切換相關的條件分支語句:

Light.prototype.setState = function (newState) {
    this.currState = newState;
};

如今能夠進行一些測試:

var light = new Light();
light.init();

不出意外的話,執行結果跟以前的代碼一致,可是使用狀態模式的好處很明顯,它可使每一種狀態和它對應的行爲之間的關係局部化,這些行爲被分散和封裝在各自對應的狀態類之中,便於閱讀和管理代碼。

另外,狀態之間的切換都被分佈在狀態類內部,這使得咱們無需編寫過多的 if,else 條件分支語句來控制狀態之間的轉換。

當咱們須要爲 light 對象增長一種新的狀態時,只須要增長一個新的狀態類,再稍稍改變一些現有的代碼便可。假設如今 light 對象多了一種超強光的狀態,那就先增長 SuperStrongLightState 類:

var SuperStrongLightState = function (light) {
    this.light = light;
}
SuperStrongLightState.prototype.buttonWasPressed = function () {
    console.log('關燈');
    this.light.setState(this.light.offLightState)
}

而後再 Light 構造函數裏新增一個 superStrongLightState 對象:

var Light = function () {
    this.offLightState = new OffLightState(this);
    this.weakLightState = new WeakLightState(this);
    this.strongLightState = new StrongLightState(this);
    this.superStrongLightState = new SuperStrongLightState(this);
    this.button = null;
};

最後改變狀態類之間的切換規則,從 StrongLightState ☛ OffLightState 變爲 StrongLightState ☛ SupStrongLightState ☛ OffLightState:

var StrongLightState = function (light) {
    this.light = light;
};

StrongLightState.prototype.buttonWasPressed = function () {
    console.log('超強光');
    this.light.setState(this.light.superStrongLightState);
};

2. 狀態模式的定義

經過電燈的例子,相信咱們對於狀態模式已經有了必定程度的瞭解。如今回頭來看 GoF 中對狀態模式的定義:容許一個對象在其內部狀態改變時改變它的行爲,對象看起來彷佛修改了它的類。

咱們以逗號分隔,把這句話分爲兩部分來看。第一部分的意思是將狀態封裝成獨立的類,並將請求委託給當前的狀態對象,當對象的內部狀態改變時,會帶來不一樣的行爲變化。電燈的例子足以說明這一點,在 off 和 on 這兩種不一樣的狀態下,咱們點擊同一個按鈕,獲得的行爲反饋是大相徑庭的。

第二部分是從客戶的角度來看,咱們使用的對象,在不一樣的狀態下具備大相徑庭的行爲,這個對象看起來是從不一樣的類中實例化而來的,實際上這是使用了委託的效果。

3. 缺乏抽象類的變通方法

咱們看到,在狀態類中將定義一些共同行爲方法,Context 最終會將請求委託給狀態對象的這些方法,在這個例子裏,這個方法就是 buttonWasPressed 。不管增長了多少種狀態類,它們都必須實現 buttonWasPressed 方法。

在 Java 中,全部的狀態類必須繼承自一個 State 抽象父類,固然若是沒有共同的功能值得放入抽象父類,也能夠選擇實現 State 接口。這樣作的緣由一方面是咱們曾屢次提過的向上轉型,另外一方面是保證全部的狀態子類都實現了 buttonWasPressed 方法。遺憾的是,JavaScript 既不支持抽象類,也沒有接口的概念。因此在使用狀態模式的時候要格外當心,若是咱們編寫一個狀態子類時,忘記了給這個狀態子類實現 buttonWasPressed 方法,則會在狀態切換的時候拋出異常。由於 Context 老是把請求委託給狀態對象的 buttonWasPressed 方法。

不論怎麼嚴格要求程序員,也許都避免不了犯錯的那一天,畢竟若是沒有編譯器的幫助,只依靠程序員自覺以及一點好運氣,是不靠譜的。這裏建議的解決方案跟模板方法模式中同樣,讓抽象父類的抽象方法直接拋出一個異常,這個異常至少會在程序運行期間就被發現:

var State = function () {};
State.prototype.buttonWasPressed = function () {
    throw new Error('父類的 buttonWasPressed 方法必須被重寫');
};

var OffLightState = function (light) {
    this.light = light;
};
OffLightState.prototype = new State();  //繼承抽象父類
//重寫父類方法
OffLightState.prototype.buttonWasPressed = function () {
    console.log('弱光');
    this.light.setState(this.light.weakLightState);
};

4. 狀態模式的優缺點

優勢以下:

  • 狀態模式定義了狀態與行爲之間的關係,並將它們封裝在一個類裏。經過增長新的狀態類,很容易增長新的狀態和轉換。
  • 避免 Context 無限膨脹,狀態切換的邏輯被分佈在狀態類中,也去掉了 Context 中本來過多的條件分支。
  • 用對象代替字符串來記錄當前狀態,使得狀態的切換更加一目瞭然。
  • Context 中的請求動做和狀態類中封裝的行爲能夠很是容易的獨立變化而互不影響。

狀態模式的缺點是會在系統中定義過多的狀態類,編寫 20 個狀態類是一項枯燥乏味的工做,並且系統中會所以而增長很多對象。另外,因爲邏輯分散在狀態類中,雖然避開了不受歡迎的條件分支語句,但也形成了邏輯分散的問題,咱們沒法在一個地方就看出整個狀態機的邏輯。

5. 狀態模式的性能優化點

有兩種選擇來管理 state 對象的建立和銷燬。第一種是僅當 state 對象被須要時才建立並隨後銷燬,另外一種是一開始就建立好全部的狀態對象,而且始終不銷燬它們。若是 state 對象比較龐大,能夠用第一種方式來節省內存,這樣能夠避免建立一些不會用到的對象並及時的回收它們。但若是狀態的改變很頻繁,最好一開始就把這些 state 對象都建立出來,也沒有必要銷燬它們,由於可能很快將再次用到它們。

6. 狀態模式和策略模式的關係

狀態模式和策略模式像一對雙胞胎,它們都封裝了一系列的算法或者行爲,它們的類圖看起來幾乎如出一轍,但在乎圖上有很大不一樣,所以它們是兩種迥然不一樣的模式。

策略模式和狀態模式的相同點是,它們都有一個上下文,一些策略類或狀態類,上下文把請求委託給這些類來執行。

它們之間的區別是策略模式中的各個策略類之間是平等又平行的,它們之間沒有任何聯繫,因此客戶必須熟知這些策略類的做用,以便客戶能夠隨時主動切換算法;但在狀態模式中,狀態和狀態對應的行爲時早已被封裝好的,狀態之間的切換也早被規定完成,「改變行爲」這件事情發生在狀態模式內部。對客戶來講,並不須要瞭解這些細節。這正是狀態模式的做用所在。

7. JavaScript 版本的狀態機

前面的示例是模擬傳統面嚮對象語言的狀態模式實現,咱們爲每種狀態都定義一個狀態子類,而後再 Context 中持有這些狀態對象的引用,以便把 currState 設置爲當前的狀態對象。

狀態模式是狀態機的實現之一,但在 JavaScript 這種「無類」語言中,沒有規定讓狀態對象必定要從類中建立而來。另一點,JavaScript 能夠很是方便的使用委託技術,並不須要事先讓一個對象持有另外一個對象。下面的狀態機選擇了經過 Function.prototyp.call 方法直接把請求委託給某個字面量對象來執行。

下面改寫電燈的例子,來展現這種更加輕巧的作法:

var Light = function () {
    this.currState = FSM.off;   //設置當前狀態
    this.button = null;
};
Light.prototype.init = function () {
    var button = document.createElement('button'),
    self = this;
    button.innerHTML = "開關";
    this.button = document.body.appendChild(button);
    this.button.onclick = function () {
        self.currState.buttonWasPressed.call(self);
    };
};

var FSM = {
    off: {
        buttonWasPressed: function () {
            console.log('關燈');
            this.currState = FSM.on;
        }
    },
    on: {
        buttonWasPressed: function () {
            console.log('開燈');
            this.currState = FSM.off;
        }
    }
};

var light = new Light();
light.init();

接下來嘗試另一種方法,即利用下面的 delegate 函數來完成這個狀態機編寫。這是面向對象設計和閉包互換的一個例子,前者把變量保存爲對象的屬性,然後者把變量封閉在閉包造成的環境中:

var delegate = function (client, delegation) {
    return {
        buttonWasPressed: function () {
            return delegation.buttonWasPressed.apply(client, arguments);
        }
    }
};

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;
    button.innerHTML = "開關";
    this.button = document.body.appendChild(button);
    this.button.onclick = function () {
        self.currState.buttonWasPressed();
    };
};

var FSM = {
    off: {
        buttonWasPressed: function () {
            console.log('關燈');
            this.currState = this.onState;
        }
    },
    on: {
        buttonWasPressed: function () {
            console.log('開燈');
            this.currState = this.offState;
        }
    }
};

var light = new Light();
light.init();

參考書目:《JavaScript設計模式與開發實踐》

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息