1. 狀態模式(State Pattern)的定義
(1)定義:允許一個對象在其內部狀態改變時改變它的行爲。對象看起來似乎修改了它的類。
①狀態和行爲,它們的關係可以描述爲“狀態決定行爲”
②因狀態是在運行期被改變,行爲也會在運行期根據狀態的改變而改變。看起來,同一個對象,在不同的運行時刻,行爲是不一樣的,就像是類被修改了一樣。
(2)狀態模式的結構和說明
①Context:環境,也稱上下文,通常用來定義客戶感興趣的接口,同時維護一個來具體處理當前狀態的實例對象。
②State:狀態接口,用來封裝與上下文的一個特定狀態所對應的行爲。
③ConcreteState:具體實現狀態處理的類,每個類實現一個跟上下文相關的狀態的具體處理。
【編程實驗】動態改變交通燈的狀態
//行爲型模式——狀態模式 //場景:動態改變交通燈的狀態 /* 使用State模式,一言以蔽之,技術上就是使用委託來動態改變類的 行爲。而語義上就是在運行時改變一個類的狀態。面我們使用State模式來 模擬交通燈的狀態變化。交通燈就是有三種的狀態變化:紅黃綠,而變化 的循環是:紅->黃->綠->黃->紅 */ #include <iostream> #include <string> using namespace std; class Context; //前向聲明 //********************(State接口)******************************** //交通燈 class LightState { public: virtual void run() = 0; }; //紅燈 class RedState : public LightState { public: void run() { cout << "紅" << endl; } }; //黃燈 class YellowState : public LightState { public: void run() { cout << "黃" << endl; } }; class GreenState : public LightState { public: void run() { cout << "綠" << endl; } }; //******************************Context************************** //相當於Context角色 class TrafficLight { LightState* state; // //當前燈運行次數,任何設備都有消耗,即壽命。 int nCnt; int nTotalCnt; //總壽命 public: TrafficLight() { nCnt = 0; //剛出廠的燈,運行次數爲0 //只能運行100次,質量真差 nTotalCnt = 100; } //設置狀態 void setState(LightState* s) { state = s; } void run() { state->run(); ++nCnt; } //燈是否壞了 bool isBad() { bool bRet = (nCnt > nTotalCnt); if(bRet) { cout << "燈壞了" << endl; } return bRet; } }; int main() { //交通燈 TrafficLight tl; //燈的三種狀態 RedState rs; GreenState gs; YellowState ys; while(! tl.isBad()) { //控制狀態的變化 tl.setState(&rs); tl.run(); tl.setState(&ys); tl.run(); tl.setState(&gs); tl.run(); } return 0; }
2. 思考狀態模式
(1)狀態模式的本質:根據狀態來分離和選擇行爲
(2)狀態和行爲
①狀態模式的功能就是把狀態和狀態對應的行爲分離出來,每個狀態所對應的功能處理封裝在一個獨立的類裏。通過維護狀態的變化,來調用不同狀態對應的不同功能。
②爲了操作不同的狀態類,定義一個狀態接口來約束它們,這樣外部就可以面向這個統一的狀態接口編程,而無須關心具體的狀態類實現了。
③狀態和行爲是相關聯的,它們的關係可以描述爲狀態決定行爲。因狀態是在運行期被改變,行爲也會在運行期根據狀態的改變而改變,看起來,同一個對象,在不同的運行時刻,行爲是不一樣的,就像是類被修改了一樣。
(3)行爲的平行性
①注意是平行性,而不是平等性。所謂的平行性指的是各個狀態的行爲所處的層次是一樣的,相互獨立的、沒有關聯的,是根據不同的狀態來決定到底走平行線的哪一條。因爲行爲是不同的,當然對應的實現也是不同的,相互之間是不可替換的。
②平等性:強調用的可替換性,大家是同一行爲的不同描述或實現,因此在同一個行爲發生的時候,可以根據條件挑選任意一個實現來進行相應的處理。
③狀態模式的結構和策略模式的結構完全一樣。但是它們的目的、實現、本質卻完全不同。還有行爲之間的特性也是狀態模式和策略模式的一個很重要的區別,狀態模式的行爲是平行性,不可相互替換的;而策略模式的行爲是平等的,是可以相互替換的。
(4)Context和state
①在狀態模式中,上下文持有state對象,但上下文本身並不處理跟狀態相關的行爲,而是把處理狀態的功能委託給了狀態對應的狀態處理類來處理。
②在具體的狀態處理類中經常需要獲取上下文自身的數據,甚至在必要的時候回調上下文中的方法。因此,通常將上下文自身當作一個參數傳遞給具體的狀態處理類。
③客戶端一般只和上下文交互。客戶端通常不負責運行期間狀態的維護,也不負責決定後續到底使用哪一個具體的狀態處理對象,這點與策略模式是不同的。
(5)不完美的OCP體驗(開閉原則)
①修改功能:由於每個狀態對應的處理己經封裝到對應的狀態類中了,要修改己有的某個狀態的功能,直接進行修改那個類就可以了,對其他程序沒有影響。
②添加新的實現類:得修改Context中request方法。這不完全遵循OCP原則。這要說明一下,設計原則是大家在設計和開發中儘量去遵守的,但不是一定要遵守,尤其是完全遵守。因爲實際開發中,完全遵守那些原則幾乎是不可能完成的任務。
(6)創建和銷燬狀態對象
①如果要進入的狀態在運行時是不可知的,而且上下文是比較穩定的,不會經常改變狀態,而且使用也不頻繁的,可以在需要狀態對象的時候創建,使用完銷燬它們。
②如果狀態改變很頻繁,也就是需要頻繁的創建狀態對象,而且狀態對象還存儲着大量的數量信息,這種情況可以提前創建它們並且始終不銷燬。
③如果無法確定狀態改變是否頻繁,而且有些狀態對象的數據量大,有些比較小,一切都是未知的,可以採用延遲加載和緩存結合的方式,就是當第一次需要使用狀態對象時創建,使用完後並不銷燬對象,而是把這個對象緩存起來,等待一下次使用,而且在合適的時候,由緩存框架銷燬狀態對象。在實際工程開發過程中,這個方案是首選,因爲它兼顧了前兩種方案的優點,又避免了它們的缺點。
【編程實驗】電梯狀態的切換
(1)電梯狀態的切換示意圖
(2)UML類圖
//行爲型模式——狀態模式 //場景:電梯狀態切換 #include <iostream> #include <string> using namespace std; class Context; //前向聲明 //********************(State接口)******************************** //抽象電梯狀態 class LiftState { protected: Context* context; public: void setContext(Context* context) { this->context = context; } //電梯開啓動作 virtual void open() = 0; //電梯關門動作 virtual void close() = 0; //電梯運行動作 virtual void run() = 0; //電梯停止動作 virtual void stop() = 0; }; //***********************Context環境類***************************** class Context { private: LiftState* liftState; public: static LiftState* openningState; static LiftState* closingState; static LiftState* runningState; static LiftState* stoppingState; LiftState& getLiftState() { return *liftState; } void setLiftState(LiftState* liftState) { this->liftState = liftState; //把當前的環境通知到各個實現類中 this->liftState->setContext(this); } void open() { liftState->open(); } void close() { liftState->close(); } void run() { liftState->run(); } void stop() { liftState->stop(); } }; //***************************ConcreteState類******************************** //開門狀態 class OpenningState : public LiftState { public: //打開電梯門 void open() { cout << "電梯門開啓..." << endl; } //開門狀態下,可以按下“關門”按鈕 void close() { //將當前狀態修改爲關門狀態 context->setLiftState(Context::closingState); //關門 context->getLiftState().close(); } //開門狀態下不能運行! void run() { //do nothing; } //開門狀態下,按停止沒效果 void stop() { //do nothing } }; //關門狀態 class ClosingState : public LiftState { public: //打開電梯門 void open() { //將當前狀態修改爲開門狀態 context->setLiftState(Context::openningState); context->getLiftState().open(); } //關門狀態 void close() { cout << "電梯門關閉..." << endl; } //關門狀態,可以按“運行”按鈕 void run() { //將當前狀態修改爲運行狀態 context->setLiftState(Context::runningState); context->getLiftState().run(); } //關門狀態下,可以切換到停止狀態 void stop() { //將當前狀態修改爲停止狀態 context->setLiftState(Context::stoppingState); context->getLiftState().stop(); } }; //運行狀態 class RunningState : public LiftState { public: //運行狀態,按“開門”按鈕沒效課 void open() { //do nothing } //運行狀態按“關門”按鈕沒效果 void close() { //do nothing; } //運行狀態 void run() { cout << "電梯上下運行..." << endl; } //運行狀態下,可按“停止” void stop() { //將當前狀態修改爲停止狀態 context->setLiftState(Context::stoppingState); context->getLiftState().stop(); } }; //停止狀態 class StoppingState : public LiftState { public: //停止狀態,可以打開電梯門 void open() { //將當前狀態修改爲開門狀態 context->setLiftState(Context::openningState); context->getLiftState().open(); } //停止狀態下,可以按下“關門”按鈕沒效果 void close() { //do nothing } //關門狀態,可以按“運行”按鈕! void run() { //將當前狀態修改爲運行狀態 context->setLiftState(Context::runningState); context->getLiftState().run(); } //開門狀態下,按停止沒效果 void stop() { cout << "電梯停止了..." << endl; } }; LiftState* Context::openningState = new OpenningState(); LiftState* Context::closingState = new ClosingState(); LiftState* Context::runningState = new RunningState(); LiftState* Context::stoppingState = new StoppingState(); int main() { Context context; context.setLiftState(Context::closingState); context.open(); context.close(); context.run(); context.stop(); return 0; } /*輸出結果: 電梯門開啓... 電梯門關閉... 電梯上下運行... 電梯停止了... */
【編程實驗】在線投票系統
//行爲型模式——狀態模式 //場景:在線投票系統 /* 四種狀態: 1.正常投票狀態:同一用戶只能投一票 2.重複投票用戶:正常投票以後,有意或無意重複投票 2.惡意投票用戶:一個用戶反覆投票超過5次,判爲惡意刷票,取消該用戶所有的投票及資格 4.黑名單用戶:超過8次,進黑名單,禁止再登錄系統。 */ #include <iostream> #include <string> #include <map> using namespace std; //投票管理類 class VoteManager; //前向聲明 //*******************************投票狀態******************** //封裝一個投票狀態相關的行爲(相同於State模式的State接口角色) class VoteState { protected: VoteManager* voteManager; public: //設置投票上下文,用來在實現狀態對應的功能處理的時候可以 //回調上下文的數據。 void setVoteManager(VoteManager* value) { voteManager = value; } //處理狀態對象的行爲。user爲投票人,voteItem爲投票項 virtual void vote(string user, string voteItem) = 0; }; //投票管理類 class VoteManager { private: VoteState* state;//持有VoteState的指針 //記錄用戶投票的結果key-value爲用戶名稱-投票選項 map<string,string> mapVote; //記錄用戶投票的次數:鍵值對爲用戶名稱-投票次數 map<string,int> mapVoteCount; static VoteState* normalVoteState; static VoteState* repeatVoteState; static VoteState* spiteVoteState; static VoteState* blackVoteState; public: map<string,string>& getMapVote() { return mapVote; } //投票 void vote(string user, string voteItem) { //1.先爲該用戶增加設票次數 //從記錄中取出己有的投票次數 int oldVoteCnt = mapVoteCount[user]; if(oldVoteCnt <=0) oldVoteCnt = 0; mapVoteCount[user] = ++oldVoteCnt; //2.判斷該用戶投票次數,就相當於判斷對應的狀態 if(oldVoteCnt == 1) state = normalVoteState; else if ((1 < oldVoteCnt ) && (oldVoteCnt <5)) state = repeatVoteState; else if ((5 <= oldVoteCnt) && (oldVoteCnt < 8)) state = spiteVoteState; else if (oldVoteCnt >= 8) state = blackVoteState; //3.然後轉調狀態對象來進行相應的操作 state->setVoteManager(this); state->vote(user, voteItem); } }; //*******************************具體的狀態類********************** //正常投票狀態對應的處理 class NormalVoteState : public VoteState { public: void vote(string user, string voteItem) { if(voteManager != NULL) { //正常投票,記錄到投票記錄中 map<string,string> & mapVote = voteManager->getMapVote(); mapVote[user] = voteItem; cout << "恭喜你投票成功" << endl; } } }; //重複投票狀態對應的處理 class RepeatVoteState : public VoteState { public: void vote(string user, string voteItem) { //重複投票 //暫時不做處理 cout <<"請不要重複投票" << endl; } }; //惡意投票狀態對應的處理 class SpiteVoteState : public VoteState { public: void vote(string user, string voteItem) { if(voteManager != NULL) { //惡意投票,取消用戶的投票資格並取消投票記錄 map<string,string> & mapVote = voteManager->getMapVote(); map<string,string>::iterator iter = mapVote.find(user); if(iter != mapVote.end()) mapVote.erase(iter); cout << "你有惡意刷票行爲,取消投票資格" << endl; } } }; //黑名單狀態對應的處理 class BlackVoteState : public VoteState { public: void vote(string user, string voteItem) { //黑名單 //記入黑名單,禁止登錄系統 cout << "進入黑名單,將禁止登錄和使用本系統" << endl; } }; VoteState* VoteManager::normalVoteState = new NormalVoteState(); VoteState* VoteManager::repeatVoteState = new RepeatVoteState(); VoteState* VoteManager::spiteVoteState = new SpiteVoteState(); VoteState* VoteManager::blackVoteState = new BlackVoteState(); int main() { VoteManager vm; for(int i=0; i<8; i++) { vm.vote("u1", "A"); } return 0; } /*輸出結果 恭喜你投票成功 請不要重複投票 請不要重複投票 請不要重複投票 你有惡意刷票行爲,取消投票資格 你有惡意刷票行爲,取消投票資格 你有惡意刷票行爲,取消投票資格 進入黑名單,將禁止登錄和使用本系統 */
3. 狀態的維護和轉換控制
(1)在上下文中維護
因狀態本身通常被實現爲上下文對象的狀態,因此可以在上下文中進行狀態進行集中的轉換,但這裏一般會出現較多的if…else語句。這種方法適合那種狀態轉換的規則是一定的,一般不需要進行什麼擴展規則的情況。(如投票管理的例子)
(2)在狀態的處理類中維護
當每個狀態處理對象處理完自身狀態所對應的功能後,可以根據需要指定後繼狀態,以便讓應用能正確處理後續的請求。這種方法適合應用在那種狀態的轉換取決於前一個狀態動態處理的結果,或者依賴於外部數據的情況。(如電梯狀態管理的例子)
4. 狀態模式的優缺點
(1)優點
①簡化應用邏輯控制。因使用單獨的類來封裝一個狀態的處理,使得代碼結構化和意圖更清晰,從而簡化應用的邏輯控制。對於依賴狀態的if-else,理論上來說,都可以使用狀態模式來實現,把每個if或else塊定義一個狀態來代表。
②更好地分離狀態和行爲。狀態模式通過設置所有狀態類的公共接口,使得應用程序只需關心狀態的切換,而不用關心這個狀態對應的真正處理。
③更好的擴展性。引入狀態處理的公共接口後,使得擴展新的狀態變得非常容易,只需增加一個實現類即可。
(2)缺點:子類膨脹問題,一個狀態對應一個狀態處理類,會使得程序引入太多的狀態類。
【編程實驗】模擬工作流(請假審批流程)
//1.請假流程示意圖
//2.UML類圖
//行爲型模式——狀態模式 //場景:模擬工作流(請假申批流程) /* 三種狀態: 1.提交項目經理審覈狀態:同意(<3天)或不同意. ->審覈結束 2.提交部門經理審覈狀態:同意(>3天)或不同意 ->審覈結束 3.審覈結束 */ #include <iostream> #include <string> #include <map> using namespace std; typedef void Object; class State; //前向聲明 //************************輔助類***************************** class LeaveRequesModel { private: string user; //請假人 string beginDate; //請假開始時間 int leaveDays; //請假天數 string result; //審覈結果 public: string& getUser(){return user;} void setUser(string value){user = value;} string& getBeginDate(){return beginDate;} void setBeginDate(string value) { beginDate = value; } int& getLeaveDays(){return leaveDays;} void setLeaveDays(int value) { leaveDays = value; } string& getResult(){return result;} void setResult(string value) { result = value; } }; //***********************狀態機(抽象環境類)******************** class StateMachine { protected: //持有一個狀態對象 State* state; //包含流程處理需要的業務數據對象,這個對象 //會被傳到具體的狀態對象中 Object* businessVO; public: virtual void doWork() = 0; State* getState(){return state;} void setState(State* value) { state = value; } Object* getBusinessVO(){return businessVO;} void setBusinessVO(Object* value) { businessVO = value; } }; //*****************************************狀態接口******************************** //公共狀態接口 class State { public: //執行狀態對應的功能處理 //參數爲上下文的實例對象 virtual void doWork(StateMachine* ctx) = 0; virtual ~State(){} }; //請假流程的狀態接口 class LeaveRequestState : public State { //這裏可以擴展跟自己流程相關的處理 }; //****************************************上下文環境類******************** //處理客戶端請求的上下文(相當於Context角色) class LeaveRequesContext : public StateMachine { public: void doWork() { state->doWork(this); } }; //***************************************具體的狀態類************************************** //審覈結束狀態的類 class AuditOverState : public LeaveRequestState { public: void doWork(StateMachine* ctx) { LeaveRequesModel* lrm = (LeaveRequesModel*)(ctx->getBusinessVO()); cout << lrm->getUser() << ",你的請假申請己經審覈結束,結果是:" << lrm->getResult() << endl; } }; //部門經理審覈的狀態類 class DepManagerState : public LeaveRequestState { private: LeaveRequestState* auditOverState; public: DepManagerState() { auditOverState = new AuditOverState(); } ~DepManagerState() { delete auditOverState; } void doWork(StateMachine* ctx) { LeaveRequesModel* lrm = (LeaveRequesModel*)(ctx->getBusinessVO()); cout << "部門經理審覈中,請稍候..." << endl; //模擬用戶處理界面,通過控制檯來讀取數據 cout <<lrm->getUser() <<"申請從" << lrm->getBeginDate() <<"開始請假" << lrm->getLeaveDays() <<"天,請部門經理審覈(1爲同意,2爲不同意)" <<endl; //讀取控制檯輸入的數據 int a =0; cin >> a; string result = (a ==1)?"同意":"不同意"; lrm->setResult("部門經理審覈結果:" + result); //由項目經理審覈以後,轉向審覈結束狀態 ctx->setState(auditOverState); ctx->doWork(); } }; //項目經理的審覈類,處理後可能是部門經理 //審覈 或審覈結束之中的一種。 class ProjectManagerState : public LeaveRequestState { private: LeaveRequestState* depState; LeaveRequestState* auditOverState; public: ProjectManagerState() { depState = new DepManagerState(); auditOverState = new AuditOverState(); } ~ProjectManagerState() { delete depState; delete auditOverState; } void doWork(StateMachine* ctx) { LeaveRequesModel* lrm = (LeaveRequesModel*)(ctx->getBusinessVO()); cout << "項目經理審覈中,請稍候..." << endl; //模擬用戶處理界面,通過控制檯來讀取數據 cout <<lrm->getUser() <<"申請從" << lrm->getBeginDate() <<"開始請假" << lrm->getLeaveDays() <<"天,請項目經理審覈(1爲同意,2爲不同意)" <<endl; //讀取控制檯輸入的數據 int a =0; cin >> a; string result = (a ==1)?"同意":"不同意"; lrm->setResult("項目經理審覈結果:" + result); //根據選擇的結果和條件來設置一下步 if (a == 1) { if(lrm->getLeaveDays() > 3) { //如果請假天數大於3天,而且項目經理同意了,就提交 //給部門經理。 ctx->setState(depState); ctx->doWork(); //繼續執行下一步工作 } else { //請假在3天以內的,由項目經理做主,轉向審覈結束狀態 ctx->setState(auditOverState); ctx->doWork(); } } else { //由項目經理不同意,轉向審覈結束狀態 ctx->setState(auditOverState); ctx->doWork(); } } }; int main() { //創建業務對象,並設置業務數據 LeaveRequesModel lrm; lrm.setUser("小李"); lrm.setBeginDate("2016-07-03"); lrm.setLeaveDays(5); //創建上下文對象 LeaveRequesContext ctx; //爲上下文對象設置業務數據對象 ctx.setBusinessVO(&lrm); ProjectManagerState pms; ctx.setState(&pms); //向項目經理請假 //請求上下文,讓上下文開始處理工作 ctx.doWork(); return 0; } /*輸出結果 項目經理審覈中,請稍候... 小李申請從2016-07-03開始請假5天,請項目經理審覈(1爲同意,2爲不同意) 1 部門經理審覈中,請稍候... 小李申請從2016-07-03開始請假5天,請部門經理審覈(1爲同意,2爲不同意) 1 小李,你的請假申請己經審覈結束,結果是:部門經理審覈結果:同意 */
5. 狀態模式的使用場景
(1)條件、分支判斷語句的替代者。如果一個操作中含有龐大的if-else分支語句時,而且這些分支依賴於該對象的狀態可以考慮使用狀態模式。
(2)行爲隨狀態改變而改變的場景。這也是狀態模式的根本出發點,例如權限設計,人員的狀態不同即使執行相同的行爲結果也會不同,在這種情況下可以考慮使用狀態模式。
6. 相關模式
(1)狀態模式和策略模式
留在策略模式一章去講。可見後面的章節。
(2)狀態模式和觀察者模式
①這兩個模式都是在狀態發生改變時觸發行爲,只不過觀察者模式的行爲是固定的,那就是通知所有的觀察者;狀態模式是根據狀態來選擇不同的處理。
②觀察者模式是當被觀察者對象的狀態發生改變的時候,觸發觀察者聯動,具體如何處理觀察者模式是不管的。而狀態模式的主要目的是在於根據狀態和選擇行爲。
③這兩個模式可以結合使用,比如在觀察者模式的觀察者部分,當被觀察對象的狀態發生了改變,觸發通知了所有觀察者後,觀察者可以使用狀態模式,也根據通知過來的狀態選擇相應的處理。
(3)狀態模式和單例模式(或享元模式)
①這兩者可結合使用,把狀態模式中的狀態處理類實現成單例,
②也可以結合享元模式使用,由於狀態模式把狀態對應的行爲分散到多個狀態對象中,會造成很多細粒度的狀態,可以把這些狀態處理對象通過享元模式來共享,從而節省資源。