行为型模式——状态模式
学习软件设计,向OO高手迈进!
设计模式(Design pattern)是软件开发人员在软件开发过程中面临的一般问题的解决方案。
这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。
是前辈大神们留下的软件设计的"招式"或是"套路"。
什么是状态模式
在本文末尾会给出解释,待耐心看完demo再看定义,相信你会有更深刻的印象
实例讲解
背景
假设我们正在为客户开发一款糖果机产品,客户不是软件专家,他们只甩给了我们一个图,他们认为糖果机的控制器需要如下图般这样工作
赶紧脑补一下糖果机是什么样子的?
客户给的是一个状态图,一共有4个状态:没有1块钱、有1块钱、售出糖果、糖果售罄
很容易想到的就是,写一个 CandyMachine 类,4个状态分别用一个整数代表,把系统中所有可能发生的动作整合起来
Version 1.0
CandyMachine 类
class CandyMachine {
public:
CandyMachine(int i_nCount) : m_nState(SOLD_OUT) {
m_nCount = i_nCount;
if(m_nCount > 0) {
m_nState = NO_ONE_YUAN;
}
}
// 顾客试着投入1块钱
virtual void UserInsertOneYuan(void);
// 顾客试着退回1块钱
virtual void UserEjectOneYuan(void);
// 顾客试着转动手柄
virtual void UserTurnCrank(void);
// 发放糖果
virtual void Dispense(void);
virtual void ShowCurrentState(void);
private:
static const int NO_ONE_YUAN = 0; // 没有1块钱
static const int HAS_ONE_YUAN = 1; // 有1块钱
static const int SOLD = 2; // 售出糖果
static const int SOLD_OUT = 3; // 糖果售罄
int m_nState;
int m_nCount;
};
对每一个动作,都创建一个对应的方法,这些方法利用条件语句来决定在每个状态内什么行为是恰当的。比如对“投入1块钱”这个动作来说,可以把对应方法写成下面的样子:
顾客试着投入1块钱
void CandyMachine::UserInsertOneYuan(void) {
if(m_nState == NO_ONE_YUAN) {
printf("InsertOneYuan: You inserted one yuan\n");
m_nState = HAS_ONE_YUAN;
} else if(m_nState == HAS_ONE_YUAN) {
printf("InsertOneYuan: You can't insert another one yuan\n");
} else if(m_nState == SOLD) {
printf("InsertOneYuan: Please wait, we're already gaving you a candy\n");
} else if(m_nState == SOLD_OUT) {
printf("InsertOneYuan: You can't insert one yuan, the machine is sold out\n");
}
}
顾客试着退回1块钱
void CandyMachine::UserEjectOneYuan(void) {
if(m_nState == NO_ONE_YUAN) {
printf("EjectOneYuan: You haven't inserted one yuan\n");
} else if(m_nState == HAS_ONE_YUAN) {
printf("EjectOneYuan: One yuan returned\n");
m_nState = NO_ONE_YUAN;
} else if(m_nState == SOLD) {
printf("EjectOneYuan: You can't eject, you haven't inserted one yuan yet\n");
} else if(m_nState == SOLD_OUT) {
printf("EjectOneYuan: You can't eject, you haven't inserted one yuan yet\n");
}
}
顾客试着转动手柄
void CandyMachine::UserTurnCrank(void) {
if(m_nState == NO_ONE_YUAN) {
printf("TurnCrank: You turned, but there's no money\n");
} else if(m_nState == HAS_ONE_YUAN) {
printf("TurnCrank: You turned...\n");
m_nState = SOLD;
Dispense();
} else if(m_nState == SOLD) {
printf("TurnCrank: Turning twice doesn't get you another candy!\n");
} else if(m_nState == SOLD_OUT) {
printf("TurnCrank: You turned, but there are no candys\n");
}
}
发放糖果
void CandyMachine::Dispense(void) {
if(m_nState == NO_ONE_YUAN) {
printf("You need to pay first\n");
} else if(m_nState == HAS_ONE_YUAN) {
printf("No candy dispensed\n");
} else if(m_nState == SOLD) {
printf("A candy comes rolling out the slot...\n");
m_nCount--;
if(m_nCount == 0) {
printf("Oops, out of candys!\n");
m_nState = SOLD_OUT;
} else {
m_nState = NO_ONE_YUAN;
}
} else if(m_nState == SOLD_OUT) {
printf("No candy dispensed\n");
}
}
main函数
int main(int argc, char *argv[])
{
CandyMachine *p_objCandyMachine = new CandyMachine(5);
p_objCandyMachine->ShowCurrentState();
p_objCandyMachine->UserInsertOneYuan();
p_objCandyMachine->UserTurnCrank();
p_objCandyMachine->ShowCurrentState();
p_objCandyMachine->UserInsertOneYuan();
p_objCandyMachine->UserEjectOneYuan();
p_objCandyMachine->UserTurnCrank();
p_objCandyMachine->ShowCurrentState();
p_objCandyMachine->UserInsertOneYuan();
p_objCandyMachine->UserTurnCrank();
p_objCandyMachine->UserInsertOneYuan();
p_objCandyMachine->UserTurnCrank();
p_objCandyMachine->UserEjectOneYuan();
p_objCandyMachine->ShowCurrentState();
p_objCandyMachine->UserInsertOneYuan();
p_objCandyMachine->UserInsertOneYuan();
p_objCandyMachine->UserTurnCrank();
p_objCandyMachine->UserInsertOneYuan();
p_objCandyMachine->UserTurnCrank();
p_objCandyMachine->UserInsertOneYuan();
p_objCandyMachine->UserTurnCrank();
p_objCandyMachine->ShowCurrentState();
return 0;
}
运行结果
m_nState = NO_ONE_YUAN
InsertOneYuan: You inserted one yuan
TurnCrank: You turned...
A candy comes rolling out the slot...
m_nState = NO_ONE_YUAN
InsertOneYuan: You inserted one yuan
EjectOneYuan: One yuan returned
TurnCrank: You turned, but there's no money
m_nState = NO_ONE_YUAN
InsertOneYuan: You inserted one yuan
TurnCrank: You turned...
A candy comes rolling out the slot...
InsertOneYuan: You inserted one yuan
TurnCrank: You turned...
A candy comes rolling out the slot...
EjectOneYuan: You haven't inserted one yuan
m_nState = NO_ONE_YUAN
InsertOneYuan: You inserted one yuan
InsertOneYuan: You can't insert another one yuan
TurnCrank: You turned...
A candy comes rolling out the slot...
InsertOneYuan: You inserted one yuan
TurnCrank: You turned...
A candy comes rolling out the slot...
Oops, out of candys!
InsertOneYuan: You can't insert one yuan, the machine is sold out
TurnCrank: You turned, but there are no candys
m_nState = SOLD_OUT
测试正常
该来的躲不掉…变更需求!
客户认为,他们想将购买糖果这件事变成一个游戏,当手柄被转动时,有10%的概率掉下来的是两颗糖果(多送你一个,你就是赢家),相信这个改变可以大大增加他们的销售量!
思考改进
Version 1.0的代码,看起来不太好扩展。我们要添加一个赢家的状态,然后在下面每个方法中加入一个新的条件判断来处理“赢家”状态,这听起来就挺麻烦的。而且未来因新需求而加入的代码也很可能会导致 bug
void UserInsertOneYuan(void);
void UserEjectOneYuan(void);
void UserTurnCrank(void);
void Dispense(void);
如果我们将每个状态的行为都放在各自的类中(而不是放在 CandyMachine 类中),那么每个状态只要实现它自己的动作就可以了。然后糖果机(CandyMachine 类)只需要委托给代表当前状态的对象去做事情即可,这不正是**“多用组合,少用继承”**的体现吗?具体是:
- 首先我们定义一个 State 接口,在这个接口内,糖果机的每个动作(UserInsertOneYuan、UserEjectOneYuan、UserTurnCrank、Dispense)都有一个对应的方法
- 然后为机器中的每个状态实现状态类(NoOneYuanState、HasOneYuanState、SoldState、SoldOutState),这些类将负责在对应的状态下进行机器的行为
- 最后,我们要摆脱旧的条件代码,取而代之的是,将动作委托给状态类
类图
我们先来完成替换 Version 1.0 的代码,后面再来处理“赢家”的事
Version 2.0
State 接口
class State {
public:
// 顾客试着投入1块钱
virtual void InsertOneYuan(void) = 0;
// 顾客试着退回1块钱
virtual void EjectOneYuan(void) = 0;
// 顾客试着转动手柄
virtual void TurnCrank(void) = 0;
// 发放糖果(属于糖果机内部行为, 顾客不可控)
virtual void Dispense(void) = 0;
};
我们要做的事情,是去实现这个状态的所有行为。在某些条件下,这个行为会让糖果机(CandyMachine 类)的状态改变
NoOneYuanState 类
class NoOneYuanState : public State {
public:
NoOneYuanState(CandyMachine *i_pCandyMachine) {
m_pCandyMachine = i_pCandyMachine;
}
virtual void InsertOneYuan(void) {
printf("InsertOneYuan: You inserted one yuan\n");
m_pCandyMachine->SetState(m_pCandyMachine->GetHasOneYuanState());
}
virtual void EjectOneYuan(void) {
printf("EjectOneYuan: You haven't inserted one yuan\n");
}
virtual void TurnCrank(void) {
printf("TurnCrank: You turned, but there's no money\n");
}
virtual void Dispense(void) {
printf("You need to pay first\n");
}
private:
CandyMachine *m_pCandyMachine;
};
HasOneYuanState 类
class HasOneYuanState : public State {
public:
HasOneYuanState(CandyMachine *i_pCandyMachine) {
m_pCandyMachine = i_pCandyMachine;
}
virtual void InsertOneYuan(void) {
printf("InsertOneYuan: You can't insert another one yuan\n");
}
virtual void EjectOneYuan(void) {
printf("EjectOneYuan: One yuan returned\n");
m_pCandyMachine->SetState(m_pCandyMachine->GetNoOneYuanState());
}
virtual void TurnCrank(void) {
printf("TurnCrank: You turned...\n");
m_pCandyMachine->SetState(m_pCandyMachine->GetSoldState());
}
virtual void Dispense(void) {
printf("No candy dispensed\n");
}
private:
CandyMachine *m_pCandyMachine;
};
SoldState 类
class SoldState : public State {
public:
SoldState(CandyMachine *i_pCandyMachine) {
m_pCandyMachine = i_pCandyMachine;
}
virtual void InsertOneYuan(void) {
printf("InsertOneYuan: Please wait, we're already gaving you a candy\n");
}
virtual void EjectOneYuan(void) {
printf("EjectOneYuan: You can't eject, you haven't inserted one yuan yet\n");
}
virtual void TurnCrank(void) {
printf("TurnCrank: Turning twice doesn't get you another candy!\n");
}
virtual void Dispense(void) {
m_pCandyMachine->ReleaseCandy();
if(m_pCandyMachine->GetCount() > 0) {
m_pCandyMachine->SetState(m_pCandyMachine->GetNoOneYuanState());
} else {
printf("Oops, out of candys!\n");
m_pCandyMachine->SetState(m_pCandyMachine->GetSoldOutState());
}
}
private:
CandyMachine *m_pCandyMachine;
};
SoldOutState 类
class SoldOutState : public State {
public:
SoldOutState(CandyMachine *i_pCandyMachine) {
m_pCandyMachine = i_pCandyMachine;
}
virtual void InsertOneYuan(void) {
printf("InsertOneYuan: You can't insert one yuan, the machine is sold out\n");
}
virtual void EjectOneYuan(void) {
printf("EjectOneYuan: You can't eject, you haven't inserted one yuan yet\n");
}
virtual void TurnCrank(void) {
printf("TurnCrank: You turned, but there are no candys\n");
}
virtual void Dispense(void) {
printf("No candy dispensed\n");
}
private:
CandyMachine *m_pCandyMachine;
};
接着还有 CandyMachine 类
class CandyMachine {
public:
CandyMachine(int i_nCount);
// 现在这3个动作变得很容易实现了, 我们只需委托到当前状态即可
virtual void UserInsertOneYuan(void);
virtual void UserEjectOneYuan(void);
virtual void UserTurnCrank(void);
virtual void ReleaseCandy(void);
virtual void SetState(State *i_pState);
virtual State *GetNoOneYuanState(void);
virtual State *GetHasOneYuanState(void);
virtual State *GetSoldState(void);
virtual State *GetSoldOutState(void);
virtual int GetCount(void);
virtual void ShowCurrentState(void);
private:
State *m_pNoOneYuanState;
State *m_pHasOneYuanState;
State *m_pSoldState;
State *m_pSoldOutState;
State *m_pState;
int m_nCount;
};
CandyMachine 的具体实现,变得简单了,糖果机将动作行为直接委托给当前的状态就可以了
CandyMachine::CandyMachine(int i_nCount) {
m_pNoOneYuanState = new NoOneYuanState(this);
m_pHasOneYuanState = new HasOneYuanState(this);
m_pSoldState = new SoldState(this);
m_pSoldOutState = new SoldOutState(this);
m_pState = m_pSoldOutState;
m_nCount = i_nCount;
if(m_nCount > 0) {
m_pState = m_pNoOneYuanState;
}
}
void CandyMachine::UserInsertOneYuan(void) {
m_pState->InsertOneYuan();
}
void CandyMachine::UserEjectOneYuan(void) {
m_pState->EjectOneYuan();
}
void CandyMachine::UserTurnCrank(void) {
m_pState->TurnCrank();
m_pState->Dispense();
}
void CandyMachine::ReleaseCandy(void) {
printf("A candy comes rolling out the slot...\n");
if(m_nCount != 0) {
m_nCount--;
}
}
void CandyMachine::SetState(State *i_pState) {
m_pState = i_pState;
}
State *CandyMachine::GetNoOneYuanState(void) {
return m_pNoOneYuanState;
}
State *CandyMachine::GetHasOneYuanState(void) {
return m_pHasOneYuanState;
}
State *CandyMachine::GetSoldState(void) {
return m_pSoldState;
}
State *CandyMachine::GetSoldOutState(void) {
return m_pSoldOutState;
}
int CandyMachine::GetCount(void) {
return m_nCount;
}
main函数,跟 Version 1.0 一样,不用改
运行结果,也跟 Version 1.0 一样
检查一下,到目前为止我们已经做了哪些事情…
你现在有了一个糖果机的实现,它在结构上和前一个版本差异颇大,但是功能上却是一样的。通过从结构上改变实现,你已经做到了以下几点:
-
将每个状态的行为局部化到它自己的类中
-
将容易产生问题的 if 语句删除,以方便日后的维护
-
让每一个状态“对修改关闭”,让糖果机“对扩展开放”,因为可以方便的加入新的状态类
Version 2.1
我们立刻加入新的状态类——“赢家”状态,应该很简单了
类图
首先,我们要在 CandyMachine 类中加入一个成员:State *m_pWinnerState;
然后来实现 WinnerState 类,其实它跟 SoldState 类很像,只是多了一行代码,多释放一个糖果:
m_pCandyMachine->ReleaseCandy();
class WinnerState : public State {
public:
WinnerState(CandyMachine *i_pCandyMachine) {
m_pCandyMachine = i_pCandyMachine;
}
virtual void InsertOneYuan(void) {
printf("InsertOneYuan: Please wait, we're already gaving you a candy\n");
}
virtual void EjectOneYuan(void) {
printf("EjectOneYuan: You can't eject, you haven't inserted one yuan yet\n");
}
virtual void TurnCrank(void) {
printf("TurnCrank: Turning twice doesn't get you another candy!\n");
}
virtual void Dispense(void) {
printf("YOU'RE A WINNER! You get two candys\n");
m_pCandyMachine->ReleaseCandy();
m_pCandyMachine->ReleaseCandy();
if(m_pCandyMachine->GetCount() > 0) {
m_pCandyMachine->SetState(m_pCandyMachine->GetNoOneYuanState());
} else {
printf("Oops, out of candys!\n");
m_pCandyMachine->SetState(m_pCandyMachine->GetSoldOutState());
}
}
private:
CandyMachine *m_pCandyMachine;
};
最后,我们要增加一个进入 WinnerState 状态的转换,由前面的代码可知,进入 SoldState 状态的是在 HasOneYuanState 状态的 TurnCrank 方法里面,所以接下来要修改一下 HasOneYuanState 类。同时把产生 10% 概率的方法也放在 HasOneYuanState 类中
virtual void TurnCrank(void) {
printf("TurnCrank: You turned...\n");
if(IsWinner() && (m_pCandyMachine->GetCount() > 1)) {
m_pCandyMachine->SetState(m_pCandyMachine->GetWinnerState());
} else {
m_pCandyMachine->SetState(m_pCandyMachine->GetSoldState());
}
}
// 10%的概率返回true
virtual bool IsWinner(void) {
int nRandom = 0;
srand((int)time(0));
nRandom = rand() % 100;
if(nRandom < 10) {
return true;
}
return false;
}
main 函数
int main(int argc, char *argv[])
{
CandyMachine *p_objCandyMachine = new CandyMachine(5);
p_objCandyMachine->ShowCurrentState();
p_objCandyMachine->UserInsertOneYuan();
p_objCandyMachine->UserTurnCrank();
p_objCandyMachine->ShowCurrentState();
sleep(1);
p_objCandyMachine->UserInsertOneYuan();
p_objCandyMachine->UserTurnCrank();
p_objCandyMachine->ShowCurrentState();
sleep(1);
p_objCandyMachine->UserInsertOneYuan();
p_objCandyMachine->UserTurnCrank();
p_objCandyMachine->ShowCurrentState();
return 0;
}
运行结果
m_pState = NO_ONE_YUAN
InsertOneYuan: You inserted one yuan
TurnCrank: You turned...
A candy comes rolling out the slot...
m_pState = NO_ONE_YUAN
InsertOneYuan: You inserted one yuan
TurnCrank: You turned...
YOU'RE A WINNER! You get two candys
A candy comes rolling out the slot...
A candy comes rolling out the slot...
m_pState = NO_ONE_YUAN
InsertOneYuan: You inserted one yuan
TurnCrank: You turned...
A candy comes rolling out the slot...
m_pState = NO_ONE_YUAN
运行了几次,还是有机会成为“赢家”的!
状态模式定义
现在,我们来说下什么是状态模式?没错,我们上面的实例用的就是状态模式。
状态模式允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类
这个描述的第一部分是什么意思呢?状态模式将状态封装成为独立的类,并将动作委托到代表当前状态的对象,所以行为会随着内部状态而改变
例如:当糖果机是在 NoOneYuanState 或 HasOneYuanState 两种不同的状态时,顾客投入1块钱,就会得到不同的行为(机器接受1块钱或机器拒绝1块钱)
这个描述的第二部分又是什么意思呢?从客户的视角来看:如果你使用的对象能够完全改变它的行为,那么你会觉得,这个对象实际上是从别的类实例化而来的。但实际上,我们是在使用组合并利用多态来引用不同的状态对象来实现的
看一下状态模式的类图
没错,策略模式的图和这张图是一样的!
基本常识:策略模式和状态模式是双胞胎,在出生时才分开。你已经知道了,策略模式是围绕可以互换的算法来创建业务的,然而,状态走的是更崇高的路,它通过改变对象内部的状态来帮助对象控制自己的行为。这两个模式的差别在于它们的“意图”
以状态模式而言,我们将一群行为封装在状态对象中,Context 的行为随时可委托到那些状态对象中的一个。随着时间的流逝,当前状态在状态对象集合中游走改变,以反映出 Context 内部的状态,因此,Context 的行为也会跟着改变。但是Context 的客户(main 函数)对于状态对象了解不多,甚至根本是浑然不觉
而以策略模式而言,客户(main 函数)通常主动指定 Context (僵尸例子中的红头僵尸)所要组合的策略对象(僵尸例子中的速度和攻击方式)是哪一个。固然策略模式让我们具有弹性,能够在运行时改变策略,但对于某个 Context 对象来说,通常都只有一个最适当的策略对象(一般不会像状态模式里的 Context 对象的状态变来变去)
一般来说,我们把策略模式想成是除了继承之外的一种弹性替代方案。如果你使用继承定义了一个类的行为,你将被这个行为困住,甚至要修改它都很难。有了策略模式,你可以通过组合不同的对象来改变行为
我们把状态模式想成是不用在 Context 中放置许多条件判断的替代方案。通过将行为包装进状态对象中,你可以通过在 Context 内简单的改变对象来改变 Context 的行为
状态模式的优缺点
无论哪种模式都有其优缺点,当然我们每次在编写代码的时候需要考虑下其利弊
状态模式的优点:
- 每个状态都是一个子类,只要增加状态就要增加子类,修改状态,只修改一个子类即可
- 结构清晰,避免了过多的 switch…case 或 if…else 语句的使用,避免了程序的复杂性,提高可维护性
- 外界调用不知道 Context 内部的状态改变,只要调用其方法即可
状态模式的缺点:
- 状态模式的使用必然会增加系统类和对象的个数。由于所有的状态都是一个类,有的 Context 对象可能会有非常多的状态,这个时候使用状态模式就会导致类特别多,不利于维护
总结
在软件开发过程中,应用程序可能会根据不同的情况作出不同的处理。最直接的解决方案是将这些所有可能发生的情况全都考虑到。然后使用 if ellse 语句来做状态判断进行不同情况的处理。但是对复杂状态的判断就显得“力不从心了”。随着增加新的状态或者修改一个状态(if else 或 switch case 语句的增多或者修改)可能会引起很大的修改,而程序的可读性,扩展性也会变得很弱,维护也会很麻烦,这时就要考虑只修改自身状态的模式
在状态模式中,Context 是持有状态的对象,但是 Context 自身并不处理跟状态相关的行为,而是把处理状态的行为委托给了对应的状态处理类来处理
在具体的状态处理类中经常需要获取 Context 自身的数据,甚至在必要的时候会回调 Context 的方法,因此,通常将 Context 自身当作一个参数传递给具体的状态处理类(如 NoOneYuanState 的构造函数所需的参数就是 CandyMachine 的指针)
客户(main 函数)一般只和 Context 交互。客户可以用状态对象来配置一个 Context,一旦配置完毕,就不再需要和状态对象打交道了。客户通常不负责运行期间状态的维护,也不负责决定后续到底使用哪一个具体的状态对象
参考资料
https://blog.csdn.net/qq_31984879/article/details/85199258
Head+First设计模式(中文版).pdf