C++設計模式——狀態模式
在闡述狀態模式之前,先來看一個例子。一個銀行取款問題: 如果賬戶餘額大於0,則正常取款;如果餘額在-2000和0之間,則透支取款;如果餘額小於-2000,則賬戶處於凍結狀態,無法進行取款操作。
實現程式碼如下:
執行取款這一操作,存在正常狀態、透支狀態、凍結狀態三種狀態。不同狀態下,取款操作對應有不同的行為。如果需要新增一種新的狀態,如:賬戶餘額小於-10000,直接登出此賬戶,並且接受法院的傳票,得修改上述程式碼。執行某一個操作,需要判斷這一操作是在哪一個狀態下執行的,判斷該狀態下是否具有該方法,以及特定狀態下如何實現該方法,將導致大量的if...else條件判斷,新增新的狀態,得修改原始碼,違背"開放封閉原則"。//銀行賬戶 class Account { private: //餘額 int m_nBalance; public: //取款操作 void WithDraw() { if( m_nBalance > 0 ) { cout << "正常取款狀態" << endl; } else if( m_nBalance > -2000 ) { cout << "透支狀態" << endl; } else { cout << "凍結狀態" << endl; } } };
因此有必要對這些狀態進行封裝,將狀態的行為封裝到具體的狀態中。為了解決這些問題,我們可以使用狀態模式,在狀態模式中,我們將物件在每一個狀態下的行為和狀態轉移語句封裝在一個個狀態類中,通過這些狀態類來分散冗長的條件轉移語句,讓系統具有更好的靈活性和可擴充套件性。
1、狀態模式概述
狀態模式用於解決系統中複雜物件的狀態轉換以及不同狀態下行為的封裝問題。當系統中某個物件存在多個狀態,這些狀態之間可以進行轉換,而且物件在不同狀態下行為不相同時可以使用狀態模式。狀態模式將一個物件的狀態從該物件中分離出來,封裝到專門的狀態類中,使得物件狀態可以靈活變化,對於客戶端而言,無須關心物件狀態的轉換以及物件所處的當前狀態,無論對於何種狀態的物件,客戶端都可以一致處理。
狀態模式(State Pattern):允許一個物件在其內部狀態改變時改變它的行為,物件看起來似乎修改了它的類。其別名為狀態物件(Objects for States),狀態模式是一種物件行為型模式。 |
在狀態模式中引入了抽象狀態類和具體狀態類,它們是狀態模式的核心。
狀態模式結構圖
在狀態模式結構圖中包含如下幾個角色:
Context(環境類):環境類又稱為上下文類,它是擁有多種狀態的物件。由於環境類的狀態存在多樣性且在不同狀態下物件的行為有所不同,因此將狀態獨立出去形成單獨的狀態類。在環境類中維護一個抽象狀態類State的例項,這個例項定義當前狀態,在具體實現時,它是一個State子類的物件。
State(抽象狀態類):它用於定義一個介面以封裝與環境類的一個特定狀態相關的行為,在抽象狀態類中聲明瞭各種不同狀態對應的方法,而在其子類中實現類這些方法,由於不同狀態下物件的行為可能不同,因此在不同子類中方法的實現可能存在不同,相同的方法可以寫在抽象狀態類中。
ConcreteState(具體狀態類):它是抽象狀態類的子類,每一個子類實現一個與環境類的一個狀態相關的行為,每一個具體狀態類對應環境的一個具體狀態,不同的具體狀態類其行為有所不同。
在狀態模式中,我們將物件在不同狀態下的行為封裝到不同的狀態類中,為了讓系統具有更好的靈活性和可擴充套件性,同時對各狀態下的共有行為進行封裝,我們需要對狀態進行抽象,引入了抽象狀態類角色,其典型程式碼如下所示:
class State
{
public:
//宣告抽象業務方法,不同的具體狀態類可以不同的實現
void handle();
};
在抽象狀態類的子類即具體狀態類中實現了在抽象狀態類中宣告的業務方法,不同的具體狀態類可以提供完全不同的方法實現,在實際使用時,在一個狀態類中可能包含多個業務方法,如果在具體狀態類中某些業務方法的實現完全相同,可以將這些方法移至抽象狀態類,實現程式碼的複用,典型的具體狀態類程式碼如下所示
class ConcreteState : public State
{
public:
void handle()
{
//方法具體實現程式碼
}
}
環境類維持一個對抽象狀態類的引用,通過setState()方法可以向環境類注入不同的狀態物件,再在環境類的業務方法中呼叫狀態物件的方法,典型程式碼如下所示。
class Context
{
private:
//維持一個對抽象狀態物件的引用
State state;
//其他屬性值,該屬性值的變化可能會導致物件狀態發生變化
int value;
public:
//設定狀態物件
void setState(State state)
{
this.state = state;
}
//呼叫狀態物件的業務方法
void request()
{
state.handle();
//其他程式碼
}
}
環境類實際上是真正擁有狀態的物件,我們只是將環境類中與狀態有關的程式碼提取出來封裝到專門的狀態類中,環境類Context與抽象狀態類State之間是一種關聯關係。和策略模式相似,策略模式封裝的是一個個具體策略,環境類Context引用一個具體的策略;而狀態模式封裝的是一個個具體的狀態,環境類Context也維持了一個對具體狀態的引用。兩者都是通過組合的方式,實現軟體複用的功能。
2、智慧空調的設計與實現
某軟體公司將開發一套智慧空調系統: 系統檢測到溫度處於20---30度之間,則切換到常溫狀態;溫度處於30---45度,則切換到製冷狀態; 溫度小於20度,則切換到制熱狀態。請使用狀態模式對此係統進行設計。 |
從需求中可以看出,空調可以處於三種狀態: 制熱狀態、常溫狀態、製冷狀態。每種狀態下都存在三種行為:保持常溫、製冷、制熱。
空調抽象狀態實現程式碼如下:
//空調抽象狀態類
class AirConditionerState
{
public:
//保持常溫
virtual void KeepNormalTemperature(AirConditioner * pAirConditioner) = 0;
//製冷
virtual void refrigerate(AirConditioner * pAirConditioner) = 0;
//制熱
virtual void Heat(AirConditioner * pAirConditioner) = 0;
};
三種具體狀態類宣告如下:
//常溫狀態
class NormalTemperatureState : public AirConditionerState
{
public:
//保持常溫
void KeepNormalTemperature(AirConditioner * pAirConditioner);
//製冷
void refrigerate(AirConditioner * pAirConditioner);
//制熱
void Heat(AirConditioner * pAirConditioner);
};
//製冷狀態
class RefrigerateState : public AirConditionerState
{
public:
//保持常溫
void KeepNormalTemperature(AirConditioner * pAirConditioner);
//製冷
void refrigerate(AirConditioner * pAirConditioner);
//制熱
void Heat(AirConditioner * pAirConditioner);
};
//制熱狀態
class HeatState : public AirConditionerState
{
public:
//保持常溫
void KeepNormalTemperature(AirConditioner * pAirConditioner);
//製冷
void refrigerate(AirConditioner * pAirConditioner);
//制熱
void Heat(AirConditioner * pAirConditioner);
};
每種狀態下都存在保持常溫、製冷、制熱方法。這些方法帶有一個AirConditioner類引數,方法內部使用這個引數回撥空調的溫度值,根據這個溫度值,用於判斷該方法如何實現,以及如何切換到其他狀態。三種狀態實現程式碼如下:
/******************************正常溫度狀態******************************************/
//保持常溫
void NormalTemperatureState::KeepNormalTemperature(AirConditioner * pAirConditioner)
{
int nTemperature = pAirConditioner->GetTemperature();
if( nTemperature > 20 && nTemperature <= 30 )
{
cout << "已經是常溫狀態,不能調節為常溫" << endl;
}
}
//製冷
void NormalTemperatureState::refrigerate(AirConditioner * pAirConditioner)
{
int nTemperature = pAirConditioner->GetTemperature();
if( nTemperature > 30 && nTemperature <= 45 )
{
pAirConditioner->SetAirConditionerState(pAirConditioner->GetRefrigerateState());
cout << "切換到製冷狀態" << endl;
}
}
//制熱
void NormalTemperatureState::Heat(AirConditioner * pAirConditioner)
{
int nTemperature = pAirConditioner->GetTemperature();
if( nTemperature <= 20 )
{
pAirConditioner->SetAirConditionerState(pAirConditioner->GetHeatState());
cout << "切換到制熱狀態" << endl;
}
}
/******************************製冷狀態******************************************/
//保持常溫
void RefrigerateState::KeepNormalTemperature(AirConditioner * pAirConditioner)
{
int nTemperature = pAirConditioner->GetTemperature();
if( nTemperature > 20 && nTemperature <= 30 )
{
pAirConditioner->SetAirConditionerState(pAirConditioner->GetNormalTemperatureState());
cout << "切換到常溫狀態" << endl;
}
}
//製冷
void RefrigerateState::refrigerate(AirConditioner * pAirConditioner)
{
int nTemperature = pAirConditioner->GetTemperature();
if( nTemperature > 30 && nTemperature <= 45 )
{
cout << "已經是製冷狀態,不能調節為製冷狀態" << endl;
}
}
//制熱
void RefrigerateState::Heat(AirConditioner * pAirConditioner)
{
int nTemperature = pAirConditioner->GetTemperature();
if( nTemperature <= 20 )
{
pAirConditioner->SetAirConditionerState(pAirConditioner->GetHeatState());
cout << "切換到制熱狀態" << endl;
}
}
/******************************制熱狀態******************************************/
//保持常溫
void HeatState::KeepNormalTemperature(AirConditioner * pAirConditioner)
{
int nTemperature = pAirConditioner->GetTemperature();
if( nTemperature > 20 && nTemperature <= 30 )
{
pAirConditioner->SetAirConditionerState(pAirConditioner->GetNormalTemperatureState());
cout << "切換到常溫狀態" << endl;
}
}
//製冷
void HeatState::refrigerate(AirConditioner * pAirConditioner)
{
int nTemperature = pAirConditioner->GetTemperature();
if( nTemperature > 30 && nTemperature <= 45 )
{
pAirConditioner->SetAirConditionerState(pAirConditioner->GetRefrigerateState());
cout << "切換到製冷狀態" << endl;
}
}
//制熱
void HeatState::Heat(AirConditioner * pAirConditioner)
{
int nTemperature = pAirConditioner->GetTemperature();
if( nTemperature <= 20 )
{
cout << "已經是制熱狀態,不能調節為制熱狀態" << endl;
}
}
空調類,也就是環境類Contex,維護了一個狀態的引用,實現的時候將呼叫狀態物件的方法。宣告程式碼如下:
//空調類
class AirConditioner
{
private:
//空調名稱
string m_strAirName;
//空調當前溫度
int m_nTemperature;
//常溫狀態
AirConditionerState * m_pNormalTemperatureState;
//製冷狀態
AirConditionerState * m_pRefrigerateState;
//制熱狀態
AirConditionerState * m_pHeatState;
//當前溫度狀態
AirConditionerState * m_pCurState;
public:
//建構函式
AirConditioner(string strAirName, int nTemperature);
//虛構函式
~AirConditioner();
//調節溫度
void SetTemperature(int nTemperature);
//獲取溫度
int GetTemperature();
//設定空調狀態
void SetAirConditionerState(AirConditionerState * pAirConditionerState);
//獲取常溫狀態
AirConditionerState * GetNormalTemperatureState();
//獲取製冷狀態
AirConditionerState * GetRefrigerateState();
//獲取制熱狀態
AirConditionerState * GetHeatState();
//保持常溫
void KeepNormalTemperature();
//製冷
void refrigerate();
//制熱
void Heat();
};
空調類實現程式碼如下:
//建構函式
AirConditioner::AirConditioner(string strAirName, int nTemperature)
{
m_strAirName = strAirName;
m_nTemperature = nTemperature;
m_pNormalTemperatureState = new NormalTemperatureState();
m_pRefrigerateState = new RefrigerateState();
m_pHeatState = new HeatState();
m_pCurState = m_pNormalTemperatureState;
}
//虛構函式
AirConditioner::~AirConditioner()
{
delete m_pNormalTemperatureState;
m_pNormalTemperatureState = NULL;
delete m_pRefrigerateState;
m_pRefrigerateState = NULL;
delete m_pHeatState;
m_pHeatState = NULL;
}
//調節溫度
void AirConditioner::SetTemperature(int nTemperature)
{
m_nTemperature = nTemperature;
}
//獲取溫度
int AirConditioner::GetTemperature()
{
return m_nTemperature;
}
//設定空調狀態
void AirConditioner::SetAirConditionerState(AirConditionerState * pAirConditionerState)
{
m_pCurState = pAirConditionerState;
}
//獲取常溫狀態
AirConditionerState * AirConditioner::GetNormalTemperatureState()
{
return m_pNormalTemperatureState;
}
//獲取製冷狀態
AirConditionerState * AirConditioner::GetRefrigerateState()
{
return m_pRefrigerateState;
}
//獲取制熱狀態
AirConditionerState * AirConditioner::GetHeatState()
{
return m_pHeatState;
}
//保持常溫
void AirConditioner::KeepNormalTemperature()
{
m_pCurState->KeepNormalTemperature(this);
}
//製冷
void AirConditioner::refrigerate()
{
m_pCurState->refrigerate(this);
}
//制熱
void AirConditioner::Heat()
{
m_pCurState->Heat(this);
}
測試原始碼如下:
#include <iostream>
#include "AirConditioner.h"
using namespace std;
int main()
{
AirConditioner * pAirConditioner = new AirConditioner("海爾空調", 25);
/****************常溫狀態*************************/
pAirConditioner->KeepNormalTemperature();
cout << endl;
/****************製冷狀態*************************/
pAirConditioner->SetTemperature(33);
pAirConditioner->refrigerate();
cout << endl;
/****************制熱狀態*************************/
pAirConditioner->SetTemperature(15);
pAirConditioner->Heat();
/****************銷燬操作*************************/
delete pAirConditioner;
pAirConditioner = NULL;
return 0;
}
編譯並執行,結果如下:
將具體行為封裝在常溫狀態、製冷狀態、制熱狀態中。空調類(也就是環境類)維持一個當前狀態的引用,當客戶端呼叫環境類的方法時,將該呼叫操作委託給具體狀態類。具體狀態類實現該狀態下的行為,以及控制切換到其他狀態。客戶端無需直接操作具體的狀態類,而是由環境類代為處理,降低了客戶端與具體狀態類的耦合性。如果需要新增具體的狀態類也很容易,只需要繼承於抽象狀態類並對環境類稍加修改就可以了。另外,也避免了大量if...else臃腫語句,把這些條件判斷都封裝成一個個狀態類。
3、使用環境類實現狀態的轉換
在狀態模式中實現狀態轉換時,具體狀態類可通過呼叫環境類Context的setState()方法進行狀態的轉換操作,也可以統一由環境類Context來實現狀態的轉換。此時,增加新的具體狀態類可能需要修改其他具體狀態類或者環境類的原始碼,否則系統無法轉換到新增狀態。但是對於客戶端來說,無須關心狀態類,可以為環境類設定預設的狀態類,而將狀態的轉換工作交給具體狀態類或環境類來完成,具體的轉換細節對於客戶端而言是透明的。
在上面的“智慧空調狀態轉換”例項中,我們通過具體狀態類來實現狀態的轉換。除此之外,我們還可以通過環境類來實現狀態轉換,環境類作為一個狀態管理器,統一實現各種狀態之間的轉換操作。
下面通過簡單例項來說明如何使用環境類實現狀態轉換:
某軟體公司某開發人員欲開發一個螢幕放大鏡工具,其具體功能描述如下:使用者單擊“放大鏡”按鈕之後螢幕將放大一倍,再點選一次“放大鏡”按鈕螢幕再放大一倍,第三次點選該按鈕後螢幕將還原到預設大小。 |
State為抽象狀態、NormalState為正常大小狀態、LargerState為放大2倍大小狀態、LargestState為放大4倍大小狀態。Screen為螢幕類,也就是環境類Contex。
螢幕類宣告如下:
//螢幕類
class Screen
{
private:
//當前狀態
State * m_pState;
//正常大小,2倍大小,4倍大小狀態
State * m_pNormalState;
State * m_pLargerState;
State * m_pLargestState;
public:
Screen();
~Screen();
//螢幕點選事件
void OnClick();
//設定狀態
void SetState(State * pState);
};
螢幕類實現程式碼如下://建構函式
Screen::Screen()
{
//建立正常大小、2倍大小、4倍大小物件
m_pNormalState = new NormalState();
m_pLargerState = new LargerState();
m_pLargestState = new LargestState();
//初始狀態為正常大小
m_pState = m_pNormalState;
m_pState->Display();
}
//虛構函式
Screen::~Screen()
{
if( NULL != m_pState )
{
delete m_pState;
m_pState = NULL;
}
}
//設定狀態
void Screen::SetState(State * pState)
{
m_pState = pState;
}
//螢幕點選事件
void Screen::OnClick()
{
if( m_pState == m_pNormalState )
{
SetState(m_pLargerState);
m_pState->Display();
}
else if( m_pState == m_pLargerState )
{
SetState( m_pLargestState );
m_pState->Display();
}
else if( m_pState == m_pLargestState )
{
SetState(m_pNormalState);
m_pState->Display();
}
}
抽象狀態類和具體狀態類實現如下:void NormalState::Display()
{
printf("正常大小\n");
}
void LargerState::Display()
{
printf("2倍大小\n");
}
void LargestState::Display()
{
printf("4倍大小\n");
}
測試程式碼實現如下:
#include <stdio.h>
#include "State.h"
int main()
{
Screen * pScreen = new Screen();
pScreen->OnClick();
pScreen->OnClick();
pScreen->OnClick();
delete pScreen;
pScreen = NULL;
return 0;
}
編譯並執行,結果如下:
在上述程式碼中,所有的狀態轉換操作都由環境類Screen來實現,此時,環境類充當了狀態管理器角色。如果需要增加新的狀態,例如“八倍狀態類”,需要修改環境類,這在一定程度上違背了“開閉原則”,但對其他狀態類沒有任何影響。這也是軟體工程中的一種折中思想。
4、狀態模式總結
狀態模式將一個物件在不同狀態下的不同行為封裝在一個個狀態類中,通過設定不同的狀態物件可以讓環境物件擁有不同的行為。而狀態轉換的細節對於客戶端而言是透明的,客戶端不直接操作狀態類,也就不需要知道狀態轉換細節,降低了客戶端與具體狀態類的耦合性。狀態類和環境類是一種組合的關係,當客戶端呼叫環境類的方法時,環境類將委託呼叫狀態類的方法。使用狀態模式封裝了一個個具體的狀態類,可以避免出現if...else擁擠情況,使得程式碼易於維護,也更具擴充套件性。同時封裝一個個狀態類,也體現了"單一原則"。在實際開發中,狀態模式具有較高的使用頻率。
4.1 主要優點
狀態模式的主要優點如下:
(1) 封裝了狀態的轉換規則,在狀態模式中可以將狀態的轉換程式碼封裝在環境類或者具體狀態類中,可以對狀態轉換程式碼進行集中管理,而不是分散在一個個業務方法中,符合"單一原則"。
(2) 將所有與某個狀態有關的行為放到一個類中,只需要注入一個不同的狀態物件即可使環境物件擁有不同的行為。
(3) 允許狀態轉換邏輯與狀態物件合成一體,而不是提供一個巨大的條件語句塊,狀態模式可以讓我們避免使用龐大的條件語句來將業務方法和狀態轉換程式碼交織在一起。
(4) 可以讓多個環境物件共享一個狀態物件,從而減少系統中物件的個數。
4.2 主要缺點
狀態模式的主要缺點如下:
(1) 狀態模式的使用必然會增加系統中類和物件的個數,導致系統執行開銷增大。
(2) 狀態模式的結構與實現都較為複雜,如果使用不當將導致程式結構和程式碼的混亂,增加系統設計的難度。
(3) 狀態模式對“開閉原則”的支援並不太好,增加新的狀態類需要修改那些負責狀態轉換的原始碼,否則無法轉換到新增狀態;而且修改某個狀態類的行為也需修改對應類的原始碼。
4.3 適用場景
在以下情況下可以考慮使用狀態模式:
(1) 物件的行為依賴於它的狀態(如某些屬性值),狀態的改變將導致行為的變化。
(2) 在程式碼中包含大量與物件狀態有關的條件語句,這些條件語句的出現,會導致程式碼的可維護性和靈活性變差,不能方便地增加和刪除狀態,並且導致客戶類與類庫之間的耦合增強。
4.4 狀態模式具體應用
(1)電梯升降系統的設計: 存在開啟、關閉、執行、停止狀態。各個狀態下將有不同的行為。例如:在執行狀態下可以進行停止操作,但無法進行開啟和關閉操作。
(2)投票系統的設計: 投票1次則為正常投票狀態、投票次數在2---5次之間則為重複投票狀態、投票次數在5---8次之間則為惡意投票狀態、投票次數大於8次則拉入黑名單狀態。
(3)酒店訂房系統的設計:存在訂房狀態、入住狀態、取消訂狀態、退房狀態。各狀態下對應有不同的行為。
(4)超市、酒店、Ktv存在不同等級的使用者,各等級使用者處於不同狀態,對應有不同許可權的行為; 在遊戲中也同樣存在各種不同角色狀態,各狀態對應有不同的行為。
(5)銀行取款系統的設計:存在正常狀態、透支狀態、凍結狀態。不同狀態下將有不同行為。例如:凍結狀態不能進行取款,而正常狀態和透支狀態可以進行取款操作。
(6)作業系統的任務排程狀態圖: 存在等待狀態、就緒狀態、執行狀態、停止狀態。
(7)TCP網路連線過程中,存在三次握手狀態。傳送連線請求、應答請求、建立連線、斷開連線等狀態。
(8)資料庫中的事務處理機制。存在OldClean狀態、OldDirty狀態、OldDelete狀態、Deleted狀態。
(9)在工控領域,通訊領域存在大量的狀態圖,某些晶片也存在時序圖,高低電平的變化,系統時鐘訊號的變化,存在大量狀態的變化。
(10)生活中的狀態模式: 從兒童到中年,再到老年,是人生狀態的變化;從35度驟降到15度是氣候的變化;白手起家到腰纏萬貫是事業的變化。