設計模式大冒險第五關:狀態模式,if/else的「終結者」

這一篇文章是關於設計模式大冒險系列的第五篇文章,這一系列的每一篇文章我都但願可以經過通俗易懂的語言描述或者平常生活中的小例子來幫助你們理解好每一種設計模式。javascript

今天這篇文章來跟你們一塊兒學習一下狀態模式。相信讀完這篇文章以後,你會收穫不少。在之後的開發中,若是遇到了相似的狀況就知道如何更好地處理,可以少用ifelse語句,以及switch語句,寫出更已讀,擴展性更好,更易維護的程序。話很少說,咱們開始今天的文章吧。java

開發過程當中的一些場景

咱們在平時的開發過程當中,常常會遇到這樣一種狀況:就是須要咱們處理一個對象的不一樣狀態下的不一樣行爲。好比最多見的就是訂單,訂單有不少種狀態,每種狀態又對應着不一樣的操做,有些操做是相同的,有些操做是不一樣的。再好比一個音樂播放器程序,在播放器緩衝音樂,播放,暫停,快進,快退,終止等的狀況下又對應着各類操做。有些操做在某些狀況下是容許的,有些操做是不容許的。還有不少不一樣的場景,這裏就不一一列舉了。git

那麼面對上面說的這些狀況咱們應該如何設計咱們的程序,才能讓咱們開發出來的程序更好維護與擴展,也更方便別人閱讀呢?先彆着急,咱們一步一步來。遇到這種狀況咱們應該首先把整個操做的狀態圖畫出來,只有狀態圖畫出來,咱們才能夠清晰的知道這個過程當中會有哪些操做,都發生了哪些狀態的改變。只要咱們作了這一步,而後按照狀態圖的邏輯去實現咱們的程序;先無論代碼的質量如何,至少能夠保證咱們的邏輯功能是知足了需求的。github

生活小例子,個人吹風機

讓咱們從生活中的一個小例子入手吧。最近我家裏新買了一個吹風機,這個吹風機有兩個按鈕。一個按鈕控制吹風機的開關,另外一個按鈕能夠在吹風機打開的狀況下切換吹風的模式。吹風機的模式有三種,分別是熱風,冷熱風交替,和冷風。而且吹風機打開時默認是熱風模式segmentfault

若是讓咱們來編寫一個程序實現上面所說的吹風機的控制功能,咱們應該怎麼實現呢?首先先別急着開始寫代碼,咱們須要把吹風機的狀態圖畫出來。以下圖所示:設計模式

吹風機的狀態圖

上面的狀態圖已經把吹風機的各類狀態都表示出來了,其中圓圈表示了吹風機的狀態,帶箭頭的線表示狀態轉換。從這個狀態圖咱們能夠很直觀的知道:吹風機從關閉狀態到打開狀態默認是熱風模式,而後這三種模式能夠按照順序進行切換,而後在每一種模式下均可以直接關閉吹風機session

通常的實現方式

當咱們知道了整個吹風機的狀態轉換以後,咱們就能夠開始寫代碼了。咱們先按照最直觀的方式去實現咱們的代碼。首先咱們知道吹風機有兩個按鈕,一個控制開關,一個控制吹風機的吹風模式。那麼咱們的程序中須要有兩個變量來分別表示開關狀態吹風機當前所處的模式。這一部分的代碼以下所示:app

function HairDryer() {
   // 定義內部狀態 0:關機狀態 1:開機狀態
   this.isOn = 0;
   // 定義模式 0:熱風 1:冷熱風交替 2:冷風
   this.mode = 0;
}

接下來就要實現吹風機的開關按鈕的功能了,這一部分比較簡單;咱們只須要判斷當前isOn變量,若是是打開狀態就將isOn設置爲關閉狀態,若是是關閉狀態就將isOn設置爲打開狀態。須要注意的一點就是在吹風機關閉的狀況下須要將吹風機的模式重置爲熱風模式wordpress

// 切換吹風機的打開關閉狀態
HairDryer.prototype.turnOnOrOff = function() {
   let { isOn, mode } = this;
   if (isOn === 0) {
      // 打開吹風機
      isOn = 1;
      console.log('吹風機的狀態變爲:[打開狀態],模式是:[熱風模式]');
   } else {
      // 關閉吹風機
      isOn = 0;
      // 重置吹風機的模式
        mode = 0;
      console.log('吹風機的狀態變爲:[關閉狀態]');
   }
   this.isOn = isOn;
   this.mode = mode;
};

在接下來就是實現吹風機的模式切換的功能了,代碼以下所示:函數

// 切換吹風機的模式
HairDryer.prototype.switchMode = function() {
   const { isOn } = this;
   let { mode } = this;
   // 切換模式的前提是:吹風機是開啓狀態
   if (isOn === 1) {
      // 須要知道當前模式
      if (mode === 0) {
         // 若是當前是熱風模式,切換以後就是冷熱風交替模式
         mode = 1;
         console.log('吹風機的模式改變爲:[冷熱風交替模式]');
      } else if (mode === 1) {
         // 若是當前是冷熱風交替模式,切換以後就是冷風模式
         mode = 2;
         console.log('吹風機的模式改變爲:[冷風模式]');
      } else {
         // 若是當前是冷風模式,切換以後就是熱風模式
         mode = 0;
         console.log('吹風機的模式改變爲:[熱風模式]');
      }
   } else {
      console.log('吹風機在關閉的狀態下沒法改變模式');
   }
   this.mode = mode;
};

這一部分的代碼也不算難,可是有一些細節須要注意。首先咱們切換模式須要吹風機是打開的狀態,而後當吹風機是關閉的狀態的時候,咱們不可以切換模式。到這裏爲止,咱們已經把吹風機的控制功能都實現了。接下來就要寫一些代碼來驗證一下咱們上面的程序是否正確,測試的代碼以下所示:

const hairDryer = new HairDryer();
// 打開吹風機,切換吹風機模式
hairDryer.turnOnOrOff();
hairDryer.switchMode();
hairDryer.switchMode();
hairDryer.switchMode();
// 關閉吹風機,嘗試切換模式
hairDryer.turnOnOrOff();
hairDryer.switchMode();
// 打開關閉吹風機
hairDryer.turnOnOrOff();
hairDryer.turnOnOrOff();

輸出的結果以下所示:

吹風機的狀態變爲:[打開狀態],模式是:[熱風模式]
吹風機的模式改變爲:[冷熱風交替模式]
吹風機的模式改變爲:[冷風模式]
吹風機的模式改變爲:[熱風模式]
吹風機的狀態變爲:[關閉狀態]
吹風機在關閉的狀態下沒法改變模式
吹風機的狀態變爲:[打開狀態],模式是:[熱風模式]
吹風機的狀態變爲:[關閉狀態]

從上面測試的結果咱們能夠知道,上面程序編寫的邏輯是沒有問題的,實現了咱們想要的預期的功能。若是想看上面代碼的完整版本能夠點擊這裏瀏覽。

可是你能從上面的代碼中看出什麼問題嗎?做爲一個優秀的工程師,你確定會發現上面的代碼使用了太多的if/else判斷,而後切換吹風機模式的代碼都耦合在一塊兒。這樣會致使一些問題,首先上面代碼的可讀性不是很好,若是沒有註釋的話,想要知道吹風機模式的切換邏輯仍是有點費力的。另外一方面,上面代碼的可擴展性也不是很好,若是咱們想新增長一種模式的話,就須要修改if/else裏面的判斷,很容易出錯。那麼做爲一個優秀的工程師,咱們該如何重構上面的程序呢?

狀態模式的介紹,以及使用狀態模式重構以前的程序

接下來咱們就要進入狀態模式的學習過程了,首先咱們先不用管什麼是狀態模式。咱們先來再次看一下上面關於吹風機的狀態圖,咱們能夠看到吹風機在整個過程當中有四種狀態,分別是:關閉狀態熱風模式狀態冷熱風交替模式狀態冷風模式狀態。而後這四種模式分別都有兩個操做,分別是切換模式切換吹風機的打開和關閉狀態。(注:對於關閉狀態,雖然沒法切換模式,可是在這裏咱們也認爲這種狀態有這個操做,只是操做不會起做用。)

那麼咱們是否是能夠換一種思路去解決這個問題,咱們能夠把具體的操做封裝進每個狀態裏面,而後由對應的狀態去處理對應的操做。咱們只須要控制好狀態之間的切換就能夠了。這樣作可讓咱們把相應的操做委託給相應的狀態去作,不須要再寫那麼多的if/else去判斷狀態,這樣作還可讓咱們把變化封裝進對應的狀態中去。若是須要添加新的狀態,咱們對原來的代碼的改動也會很小

狀態模式的簡單介紹

那麼到這裏咱們來介紹一下狀態模式吧,狀態模式指的是:可以在對象的內部狀態改變的時候改變對象的行爲狀態模式經常用來對一個對象在不一樣狀態下一樣操做時產生的不一樣行爲進行封裝,從而達到可讓對象在運行時改變其行爲的能力。就像咱們上面說的吹風機,在熱風模式下,按下模式切換按鈕能夠切換到冷熱風交替模式;可是若是當前狀態是冷熱風交替模式,那麼按下模式切換按鈕,就切換到了冷風模式了。更詳細的解釋能夠參考State pattern

咱們再來看一下狀態模式的UML圖,以下所示:

狀態模式的UML圖

能夠看到,對於狀態模式來講,有一個Context(上下文),一個抽象的State類,這個抽象類定義好了每個具體的類須要實現的方法。對於每個具體的類來講,它實現了抽象類State定義好的方法,而後Context在須要進行操做的時候,只須要請求對應具體狀態類實例的對應方法就能夠了

使用狀態模式來重構以前的程序

接下來咱們來用狀態模式來重構咱們的程序,首先是Context,對應的代碼以下所示:

// 狀態模式
// 吹風機
class HairDryer {
   // 吹風機的狀態
   state;
   // 關機狀態
   offState;
   // 開機熱風狀態
   hotAirState;
   // 開機冷熱風交替狀態
   alternateHotAndColdAirState;
   // 開機冷風狀態
   coldAirState;

   // 構造函數
   constructor(state) {
      this.offState = new OffState(this);
      this.hotAirState = new HotAirState(this);
      this.alternateHotAndColdAirState = new AlternateHotAndColdAirState(
         this
      );
      this.coldAirState = new ColdAirState(this);
      if (state) {
         this.state = state;
      } else {
         // 默認是關機狀態
         this.state = this.offState;
      }
   }

   // 設置吹風機的狀態
   setState(state) {
      this.state = state;
   }

   // 開關機按鈕
   turnOnOrOff() {
      this.state.turnOnOrOff();
   }
   // 切換模式按鈕
   switchMode() {
      this.state.switchMode();
   }

   // 獲取吹風機的關機狀態
   getOffState() {
      return this.offState;
   }
   // 獲取吹風機的開機熱風狀態
   getHotAirState() {
      return this.hotAirState;
   }
   // 獲取吹風機的開機冷熱風交替狀態
   getAlternateHotAndColdAirState() {
      return this.alternateHotAndColdAirState;
   }
   // 獲取吹風機的開機冷風狀態
   getColdAirState() {
      return this.coldAirState;
   }
}

我來解釋一下上面的代碼,首先咱們使用HairDryer來表示Context,而後HairDryer類的實例屬性有state,這屬性就是表示了吹風機當前所處的狀態。其他的四個屬性分別表示吹風機對應的四個狀態實例。

吹風機有setState能夠設置吹風機的狀態,而後getOffStategetHotAirStategetAlternateHotAndColdAirStategetColdAirState分別用來獲取吹風機的對應狀態實例。你可能會說爲何要在HairDryer類裏面獲取相應的狀態實例呢?彆着急,下面會解釋爲何。

而後turnOnOrOff方法表示打開或者關閉吹風機,switchMode用來表示切換吹風機的模式。還有constructor,咱們默認若是沒有傳遞狀態實例的話,默認是熱風模式狀態。

而後是咱們的抽象類State,由於咱們的實現使用的語言是JavaScriptJavaScript暫時還不支持抽象類,因此用通常的類來代替。這個對咱們實現狀態模式沒有太大的影響。具體的代碼以下:

// 抽象的狀態
class State {
   // 開關機按鈕
   turnOnOrOff() {
      console.log('---按下吹風機 [開關機] 按鈕---');
   }
   // 切換模式按鈕
   switchMode() {
      console.log('---按下吹風機 [模式切換] 按鈕---');
   }
}

State類主要是用來定義好具體的狀態類應該實現的方法,對於咱們這個吹風機的例子來講就是turnOnOrOffswitchMode。它們分別對應,按下吹風機開關機按鈕的處理和按下吹風機的模式切換按鈕的處理。

接下來就是具體的狀態類的實現了,代碼以下所示:

// 吹風機的關機狀態
class OffState extends State {
   // 吹風機對象的引用
   hairDryer;
   constructor(hairDryer) {
      super();
      this.hairDryer = hairDryer;
   }
   // 開關機按鈕
   turnOnOrOff() {
      super.turnOnOrOff();
      this.hairDryer.setState(this.hairDryer.getHotAirState());
      console.log('狀態切換: 關閉狀態 => 開機熱風狀態');
   }
   // 切換模式按鈕
   switchMode() {
      console.log('===吹風機在關閉的狀態下沒法切換模式===');
   }
}

// 吹風機的開機熱風狀態
class HotAirState extends State {
   // 吹風機對象的引用
   hairDryer;
   constructor(hairDryer) {
      super();
      this.hairDryer = hairDryer;
   }
   // 開關機按鈕
   turnOnOrOff() {
      super.turnOnOrOff();
      this.hairDryer.setState(this.hairDryer.getOffState());
      console.log('狀態切換: 開機熱風狀態 => 關閉狀態');
   }
   // 切換模式按鈕
   switchMode() {
      super.switchMode();
      this.hairDryer.setState(
         this.hairDryer.getAlternateHotAndColdAirState()
      );
      console.log('狀態切換: 開機熱風狀態 => 開機冷熱風交替狀態');
   }
}

// 吹風機的開機冷熱風交替狀態
class AlternateHotAndColdAirState extends State {
   // 吹風機對象的引用
   hairDryer;
   constructor(hairDryer) {
      super();
      this.hairDryer = hairDryer;
   }
   // 開關機按鈕
   turnOnOrOff() {
      super.turnOnOrOff();
      this.hairDryer.setState(this.hairDryer.getOffState());
      console.log('狀態切換: 開機冷熱風交替狀態 => 關閉狀態');
   }
   // 切換模式按鈕
   switchMode() {
      super.switchMode();
      this.hairDryer.setState(this.hairDryer.getColdAirState());
      console.log('狀態切換: 開機冷熱風交替狀態 => 開機冷風狀態');
   }
}

// 吹風機的開機冷風狀態
class ColdAirState extends State {
   // 吹風機對象的引用
   hairDryer;
   constructor(hairDryer) {
      super();
      this.hairDryer = hairDryer;
   }
   // 開關機按鈕
   turnOnOrOff() {
      super.turnOnOrOff();
      this.hairDryer.setState(this.hairDryer.getOffState());
      console.log('狀態切換: 開機冷風狀態 => 關閉狀態');
   }
   // 切換模式按鈕
   switchMode() {
      super.switchMode();
      this.hairDryer.setState(this.hairDryer.getHotAirState());
      console.log('狀態切換: 開機冷風狀態 => 開機熱風狀態');
   }
}

由上面的代碼咱們能夠看到,對於每個具體的類來講,都有一個屬性hairDryer,這個屬性用來保存吹風機實例的索引。而後就是對應turnOnOrOffswitchMode方法的實現。咱們能夠看到在具體的類中咱們設置hairDryer的狀態是經過hairDryer實例的setState方法,而後獲取狀態是經過hairDryer對應的獲取狀態的方法。好比:this.hairDryer.getHotAirState()就是獲取吹風機的熱風模式狀態。

在這裏咱們能夠說一下爲何要在HairDryer類裏面獲取相應的狀態實例:由於這樣不一樣的狀態類之間至關於解耦了,它們不須要在各自的類中依賴對應的狀態,直接從hairDryer實例上獲取對應的狀態實例就能夠了。減小了類之間的依賴,使咱們代碼的可維護性變的更好了

接下來就是須要測試一下咱們上面經過狀態模式重構後的代碼有沒有實現咱們想要的功能,測試的代碼以下:

const hairDryer = new HairDryer();
// 打開吹風機
hairDryer.turnOnOrOff();
// 切換模式
hairDryer.switchMode();
// 切換模式
hairDryer.switchMode();
// 切換模式
hairDryer.switchMode();
// 關閉吹風機
hairDryer.turnOnOrOff();
// 吹風機在關閉的狀態下沒法切換模式
hairDryer.switchMode();

輸出的結果以下所示:

---按下吹風機 [開關機] 按鈕---
狀態切換: 關閉狀態 => 開機熱風狀態
---按下吹風機 [模式切換] 按鈕---
狀態切換: 開機熱風狀態 => 開機冷熱風交替狀態
---按下吹風機 [模式切換] 按鈕---
狀態切換: 開機冷熱風交替狀態 => 開機冷風狀態
---按下吹風機 [模式切換] 按鈕---
狀態切換: 開機冷風狀態 => 開機熱風狀態
---按下吹風機 [開關機] 按鈕---
狀態切換: 開機熱風狀態 => 關閉狀態
===吹風機在關閉的狀態下沒法切換模式===

根據上面的測試結果能夠知道,咱們重構以後的代碼也完美地實現了咱們想要的功能。使用狀態模式重構後的完整版本能夠點擊這裏瀏覽。那麼接下來咱們就來分析一下,使用狀態模式與第一種不使用狀態模式相比有哪些優點和劣勢。

使用狀態模式的優點有如下幾個方面:

  • 將應用的代碼解耦,利於閱讀和維護。咱們能夠看到,在第一種方案中,咱們使用了大量的if/else來進行邏輯的判斷,將各類狀態和邏輯放在一塊兒進行處理。在咱們應用相關對象的狀態比較少的狀況下可能不會有太大的問題,可是一旦對象的狀態變得多了起來,這種耦合比較深的代碼維護起來就很困難,很折磨人。
  • 將變化封裝進具體的狀態對象中,至關於將變化局部化,而且進行了封裝。利於之後的維護與拓展。使用狀態模式以後,咱們把相關的操做都封裝進對應的狀態中,若是想修改或者添加新的狀態,也是很方便的。對代碼的修改也比較少,擴展性比較好。
  • 經過組合和委託,讓對象在運行的時候能夠經過改變狀態來改變本身的行爲。咱們只須要將對象的狀態圖畫出來,專一於對象的狀態改變,以及每一個狀態有哪些行爲。這讓咱們的開發變得簡單一些,也不容易出錯,可以保證咱們寫出來的代碼質量是不錯的。

使用狀態模式的劣勢:

  • 固然使用狀態模式也有一點劣勢,那就是增長了代碼中類的數量,也就是增長了代碼量。可是在絕大多數狀況下來講,這個算不上什麼太大的問題。除非你開發的應用對代碼量有着比較嚴格的要求。

狀態模式的總結

經過上面對狀態模式的講解,以及吹風機小例子的實踐,相信你們對狀態模式都有了很深刻的理解。在平時的開發工做中,若是一個對象有不少種狀態,而且這個對象在不一樣狀態下的行爲也不同,那麼咱們就可使用狀態模式來解決這個問題。使用狀態模式可讓咱們的代碼條理清楚,容易閱讀;也方便維護和擴展

爲了驗證你的確已經掌握了狀態模式,這裏給你們出個小題目。仍是以上面的吹風機爲例子,若是如今吹風機新增長了一個按鈕,用來切換風速強度的大小。默認風速的強度是弱風,按下按鈕變爲強風。如今你能修改上面的代碼,而後實現這個功能嗎,趕快動手試試吧~

文章到這裏就結束了,若是你們有什麼問題和疑問歡迎你們在文章下面留言,或者在dreamapplehappy/blog提出來。也歡迎你們關注個人公衆號關山不難越,獲取更多關於設計模式講解的內容。

下面是這一系列的其它的文章,也歡迎你們閱讀,但願你們都可以掌握好這些設計模式的使用場景和解決的方法。若是這篇文章對你有所幫助,那就點個贊,分享一下吧~

參考連接:

相關文章
相關標籤/搜索