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)状态模式和单例模式(或享元模式)
①这两者可结合使用,把状态模式中的状态处理类实现成单例,
②也可以结合享元模式使用,由于状态模式把状态对应的行为分散到多个状态对象中,会造成很多细粒度的状态,可以把这些状态处理对象通过享元模式来共享,从而节省资源。