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

dreamapplehappy發表於2021-01-12

這一篇文章是關於設計模式大冒險系列的第五篇文章,這一系列的每一篇文章我都希望能夠通過通俗易懂的語言描述或者日常生活中的小例子來幫助大家理解好每一種設計模式。

今天這篇文章來跟大家一起學習一下狀態模式。相信讀完這篇文章之後,你會收穫很多。在以後的開發中,如果遇到了類似的情況就知道如何更好地處理,能夠少用ifelse語句,以及switch語句,寫出更已讀,擴充套件性更好,更易維護的程式。話不多說,我們開始今天的文章吧。

開發過程中的一些場景

我們在平時的開發過程中,經常會遇到這樣一種情況:就是需要我們處理一個物件的不同狀態下的不同行為。比如最常見的就是訂單,訂單有很多種狀態,每種狀態又對應著不同的操作,有些操作是相同的,有些操作是不同的。再比如一個音樂播放器程式,在播放器緩衝音樂,播放,暫停,快進,快退,終止等的情況下又對應著各種操作。有些操作在某些情況下是允許的,有些操作是不允許的。還有很多不同的場景,這裡就不一一列舉了。

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

生活小例子,我的吹風機

讓我們從生活中的一個小例子入手吧。最近我家裡新買了一個吹風機,這個吹風機有兩個按鈕。一個按鈕控制吹風機的開關,另一個按鈕可以在吹風機開啟的情況下切換吹風的模式。吹風機的模式有三種,分別是熱風,冷熱風交替,和冷風。並且吹風機開啟時預設是熱風模式

如果讓我們來編寫一個程式實現上面所說的吹風機的控制功能,我們應該怎麼實現呢?首先先別急著開始寫程式碼,我們需要把吹風機的狀態圖畫出來。如下圖所示:

吹風機的狀態圖

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

一般的實現方式

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

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

接下來就要實現吹風機的開關按鈕的功能了,這一部分比較簡單;我們只需要判斷當前isOn變數,如果是開啟狀態就將isOn設定為關閉狀態,如果是關閉狀態就將isOn設定為開啟狀態。需要注意的一點就是在吹風機關閉的情況下需要將吹風機的模式重置為熱風模式

// 切換吹風機的開啟關閉狀態
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提出來。也歡迎大家關注我的公眾號關山不難越,獲取更多關於設計模式講解的內容。

下面是這一系列的其它的文章,也歡迎大家閱讀,希望大家都能夠掌握好這些設計模式的使用場景和解決的方法。如果這篇文章對你有所幫助,那就點個贊,分享一下吧~

參考連結:

相關文章