狀態模式——從鬥地主開始說起

“人有悲歡離合,月有陰晴圓缺”。很多事物在特定條件下轉換成不同的狀態,在不同狀態下表現出不同的行爲。

在軟件系統中,有些對象在不同的條件下也具有不同的狀態,不同狀態之間可以相互轉換。通過判斷不同的條件分支(if...else...或者switch..case...)可以進行狀態的轉換。但這樣勢必使得代碼的判斷邏輯變得複雜,降低系統的可維護性。如果新加入一種狀態,還需要修改判斷邏輯,不符合開閉原則。

爲解決複雜對象的多種狀態轉換問題,並使客戶端代碼與對象狀態之間的耦合度降低,可以使用狀態模式

1.狀態模式簡介

狀態模式將一個對象的狀態從對象中分離出來,封裝到專門的狀態類中,使得對象狀態可以靈活變化。對於客戶端而言,無需關心對象轉態的轉換以及對象所處的當前狀態,無論處於何種狀態的對象,客戶端都可以一致處理。

狀態模式:

允許一個對象在其內部狀態改變時改變它的行爲。對象看起來似乎修改了它的類。

2.狀態模式結構

狀態模式的UML圖如下。

狀態模式引入了抽象層,具有抽象狀態類和具體狀態類,還包括一個上下文境類:

  • Context(上下文類):是擁有多種狀態的對象。上下文類的狀態存在多樣性,並且在不同的狀態下,對象表現出不同的行爲。在上下文類中,維護了一個抽象狀態類的實例。
  • State(抽象狀態類):聲明瞭一個接口,用於封裝與在上下文類中的一個特定狀態相關的行爲,在子類中實現在各種不同狀態對應的方法。不同的子類可能存在不同的實現方法,相同的方法可以寫在抽象狀態類中。
  • ConcreteState(具體狀態類):實現具體狀態下的方法,每一個具體狀態類對應一個具體的狀態。

值得注意的是,上下文中維護了一個狀態類的指針或者引用,可以由上下文類來覺得具體實例化爲哪一個具體的狀態對象,也可以由具體的狀態類來決定轉換爲哪一個實例,所以,上下文類和狀態類之間存在依賴甚至相互引用的關係

// 1.由環境類來決定實例化爲哪一個具體狀態類對象
class Context
{
public:
	void convertState(){
		if (condition1){
			this->state = new ConcreteStateA();
		}
		else if (condition2){
			this->state = new ConcreteStateB();
		}
		else{
			// do something
		}
	}
private:
	// 引用狀態對象
	State *state;
};
// 2.有具體狀態類來決定轉換成哪一個具體狀態類對象
class ConcreteState :public State
{
public:
	void convertState(Context* ctx){
		if (condition1){
			ctx->setState(new ConcreteStateA());
		}
		else if (condition2){
			ctx->setState(new ConcreteStateB());
		}
		else{
			// do something
		}
	}
};

下面是狀態模式的典型用法:

#ifndef __DEMO_H__
#define __DEMO_H__

// 抽象狀態類
class State
{
public:
	// 聲明抽象方法
	virtual void handle() = 0;
};

// 具體狀態類
class ConcreteState :public State
{
public:
	// 實現
	void handle(){
		// ……
	}
};

// 上下文類
class Context
{
public:
	// set方法設置狀態對象
	void setState(State* iState){
		this->state = iState;
	}
	// 對外封裝的方法
	void request(){
		// do something
		state->handle();
	}
private:
	// 引用狀態對象
	State *state;
};

#endif //__DEMO_H__

3.狀態模式代碼實例

接下來Jungle用一個實例來應用狀態模式。

在某紙牌遊戲中,遊戲人物分爲入門級(primary)熟練級(Secondary)高手級(Professional)骨灰級(Final)四種級別,由人物的積分來劃分角色等級,遊戲勝利將增加積分,失敗將扣除積分。入門級有最基本的遊戲功能play(),熟練級增加了遊戲勝利積分加倍功能doubleScore(),高手級在熟練級的基礎上增加了換牌功能changeCards(),骨灰級在高手級的基礎上再增加了偷看他人紙牌的功能peekCards()

積分規則如下:

基礎分:100,遊戲勝利+50分,遊戲失敗+30分;

入門級:0~150;熟練級150~200;高手級:200~250;骨灰級:250以上

本例設計遊戲賬戶GameAccount爲上下文類,維護了一個級別類(Level)的對象實例。GameAccount中定義了一個代表積分的score整型和統一封裝的方法playcard(),在該方法中再調用具體級別的各個技能方法。採用隨機數的方式來隨機判定牌局的輸贏,以增減積分。

級別類Level爲抽象類,聲明瞭play()、doubleScore()、changeCards()、seekCards()的抽象方法,在四個具體級別類Primary、Secondary、Professional和Final類中具體實現了該方法,具體來說是根據該級別是否有權利使用該技能來打印一行話。upgradeLevel()方法用於判斷每局牌結束後該遊戲賬戶的積分是否可以升級或者降級,通過setLevel()方法改變當前賬戶的遊戲級別

該實例的UML圖如下:

3.1.上下文類:遊戲賬戶類

//頭文件
#ifndef __GAMEACCOUNT_H__
#define __GAMEACCOUNT_H__

using namespace std;
#include <iostream>
// 前向聲明
class Level;

class GameAccount
{
public:
	GameAccount();
	GameAccount(string iName);
	string getName();
	void win();
	void lose();
	void playCard();
	void setLevel(Level*);
	int getScore();
	void setScore(int);

private:
	Level* level;
	int score;
	string name;
};

#endif

//源文件
#include "GameAccount.h"
#include "Level.h"
#include <Windows.h>
#include <time.h>
#define  random(x) (rand()%x)

GameAccount::GameAccount(){
	printf("創立遊戲角色,積分:100,級別:PRIMARY\n");
	score = 100;
	name = "none";
	setLevel(new Primary(this));
}

GameAccount::GameAccount(string iName){
	printf("創立遊戲角色,積分:100,級別:PRIMARY\n");
	score = 100;
	name = iName;
	setLevel(new Primary(this));
}

void GameAccount::setLevel(Level* iLevel){
	this->level = iLevel;
}

string GameAccount::getName(){
	return name;
}

void GameAccount::playCard(){
	this->level->playCard();

	Sleep(100);
	srand((int)time(0));
	int res = random(2);
	if (res % 2 == 0){
		this->win();
	}
	else{
		this->lose();
	}

	this->level->upgradeLevel();
}

void GameAccount::win(){
	if (this->getScore() < 200){
		setScore(getScore() + 50);
	}
	else{
		setScore(getScore() + 100);
	}
	printf("\n\t勝利,最新積分爲 %d\n", score);
}

void GameAccount::lose(){
	setScore(getScore() + 30);
	printf("\n\t輸牌,最新積分爲 %d\n", score);
}

int GameAccount::getScore(){
	return this->score;
}

void GameAccount::setScore(int iScore){
	this->score = iScore;
}

3.2.狀態類

3.2.1.抽象狀態類:Level

頭文件:

#include "GameAccount.h"

class Level
{
public :
	Level();
	// 聲明方法
	void playCard();
	void play();
	virtual void doubleScore() = 0;
	virtual void changeCards() = 0;
	virtual void peekCards() = 0;
	// 升級
	virtual void upgradeLevel() = 0;
	GameAccount* getGameAccount();
	void setGameAccount(GameAccount* iGameAccount);
private:
	GameAccount* gameAccount;
};

源文件:

Level::Level(){}

void Level::playCard(){
	this->play();
	this->doubleScore();
	this->changeCards();
	this->peekCards();
}

void Level::play(){
	printf("\t使用基本技能,");
}

void Level::setGameAccount(GameAccount* iGameAccount){
	this->gameAccount = iGameAccount;
}

GameAccount* Level::getGameAccount(){
	return gameAccount;
}

 3.2.2.具體狀態類:Primary

頭文件:

class Primary :public Level
{
public:
	Primary();
	Primary(Level* level);
	Primary(GameAccount* ga);
	void doubleScore();
	void changeCards();
	void peekCards();
	// 升級
	void upgradeLevel();
};

源文件:

Primary::Primary(){}

Primary::Primary(GameAccount* iGameAccount){
	this->setGameAccount(iGameAccount);
}

Primary::Primary(Level* level){
	getGameAccount()->setLevel(level);
}

void Primary::doubleScore(){
	return;
}

void Primary::changeCards(){
	return;
}

void Primary::peekCards(){
	return;
}

void Primary::upgradeLevel(){
	if (this->getGameAccount()->getScore() > 150){
		this->getGameAccount()->setLevel(new Secondary(this));
		printf("\t升級! 級別:SECONDARY\n\n");
	}
	else{
		printf("\n");
	}
}

3.2.3.具體狀態類:Secondary

頭文件:

class Secondary :public Level
{
public:
	Secondary();
	Secondary(Level* level);
	void doubleScore();
	void changeCards();
	void peekCards();
	// 升級
	void upgradeLevel();
};

源文件:

Secondary::Secondary(){

}

Secondary::Secondary(Level* level){
	this->setGameAccount(level->getGameAccount());
	getGameAccount()->setLevel(level);
}

void Secondary::doubleScore(){
	printf("使用勝利雙倍積分技能");
}

void Secondary::changeCards(){
	return;
}

void Secondary::peekCards(){
	return;
}

void Secondary::upgradeLevel(){
	if (this->getGameAccount()->getScore() < 150){
		this->getGameAccount()->setLevel(new Primary(this));
		printf("\t降級! 級別:PRIMARY\n\n");
	}
	else if (this->getGameAccount()->getScore() > 200){
		this->getGameAccount()->setLevel(new Professional(this));
		printf("\t升級! 級別:PROFESSIONAL\n\n");
	}
}

3.2.4.具體狀態類:Professional

頭文件:

class Professional :public Level
{
public:
	Professional();
	Professional(Level* level);
	void doubleScore();
	void changeCards();
	void peekCards();
	// 升級
	void upgradeLevel();
};

源文件:

Professional::Professional(){

}

Professional::Professional(Level* level){
	this->setGameAccount(level->getGameAccount());
	getGameAccount()->setLevel(level);
}

void Professional::doubleScore(){
	printf("使用勝利雙倍積分技能,");
}

void Professional::changeCards(){
	printf("使用換牌技能");
}

void Professional::peekCards(){
	return;
}

void Professional::upgradeLevel(){
	if (this->getGameAccount()->getScore() < 200){
		this->getGameAccount()->setLevel(new Secondary(this));
		printf("\t降級! 級別:SECONDARY\n\n");
	}
	else if (this->getGameAccount()->getScore() > 250){
		this->getGameAccount()->setLevel(new Final(this));
		printf("\t升級! 級別:FINAL\n\n");
	}
}

3.2.5.具體狀態類:Final

頭文件:

class Final :public Level
{
public:
	Final();
	Final(Level* level);
	void doubleScore();
	void changeCards();
	void peekCards();
	// 升級
	void upgradeLevel();
};

源文件:

Final::Final(){

}

Final::Final(Level* level){
	this->setGameAccount(level->getGameAccount());
	getGameAccount()->setLevel(level);
}

void Final::doubleScore(){
	printf("使用勝利雙倍積分技能,");
}

void Final::changeCards(){
	printf("使用換牌技能,");
}

void Final::peekCards(){
	printf("使用偷看卡牌技能");
}

void Final::upgradeLevel(){
	if (this->getGameAccount()->getScore() < 250){
		this->getGameAccount()->setLevel(new Professional(this));
		printf("\t降級! 級別:PROFESSIONAL\n\n");
	}
	else{
		printf("\t%s 已經是最高級\n\n", this->getGameAccount()->getName().c_str());
	}
}

3.3.客戶端代碼示例及結果

客戶端代碼創建了一個遊戲賬戶Jungle,初始積分爲100分,級別爲Primary,即入門級,Jungle一共玩了5局牌。

#include "GameAccount.h"
#include "Level.h"

int main()
{
	GameAccount *jungle = new GameAccount("Jungle");

	for (int i = 0; i < 5; i++){
		printf("第%d局:\n", i + 1);
		jungle->playCard();
	}

	printf("\n\n");
	system("pause");
	return 0;
}

結果如下:

 上面的代碼不管Jungle當前是什麼級別,都統一地調用了上下文類封裝好的方法playcard(),即外界並不知道不同級別內部的具體實現細節。運行結果顯示,Jungle的在不同的狀態(級別)下能夠表現不同的行爲(不同的技能),並且能夠不斷改變自身的狀態(升級或降級)

上述代碼源碼請訪問:https://github.com/FengJungle/DesignPattern

4.總結

優點:

  • 狀態模式封裝了狀態轉換的規則,只給外界暴露了統一的接口,客戶端可以無差別地調用該接口(如上述實例的客戶端代碼)
  • 狀態模式將所有與具體狀態有關的行爲放到一個類(具體狀態類)中,只需要注入(依賴)不同的狀態類對象到上下文類中,即可使上下文中擁有不同的行爲

缺點:

  • 狀態模式增加了系統中類的個數(不同的具體狀態類)
  • 結構相對複雜(如前述實例的UML圖),代碼邏輯也較複雜
  • 如果要增加新的狀態,需要修改負責狀態轉換的代碼,不符合開閉原則(如上述實例,如果增加了一箇中間級別,是不是得修改很多狀態轉換的邏輯?

適用環境:

  • 對象的行爲根據它的狀態的改變而不同
  • 代碼中含有大量與對象狀態有關的判斷邏輯(if……else……或switch……case……)

歡迎關注知乎專欄:Jungle是一個用Qt的工業Robot

歡迎關注Jungle的微信公衆號:Jungle筆記

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章