行为型设计模式——状态模式
文章目录
- 状态模式
- 结构
- 实现
- 特点
状态模式
场景在软件系统中,有些对象也像水一样具有多种状态,这些状态在某些情况下能够相互转换,而且对象在同的状态下也将具有不同的行为。如果使用复杂的条件判断语句(如if或switch)来进行状态的判断和换操作,这会导致代码的可维护性和灵活性下降,特别是出现新状态的时候代码的扩展性很差,客户端码也需要进行修改,违反开闭原则。为了更好地对这些具有多种状态的对象进行设计,我们可以使用一被称之为状态模式(State Pattern)的设计模式。
- 人在幼年、童年、少年、中年、老年各个使其的形态都是不一样的
- 工作期间,上午、中午、下午、傍晚、深夜的工作状态也不一样
- 人的心情不同时,会有喜、怒、哀、乐
- 手机在待机、通话、充电、游戏时的状态也不一样
- 文章的发表会有草稿、审阅、发布状态
⚠️状态模式和策略模式比较类似,策略模式中的各个策略是独立的不关联的,但是状态模式下的对象的各种状态可以是独立的也可以是相互依赖的,比如上面关于文章的发布的例子:
- 普通用户的文章草稿发表之后被审阅,审阅失败重新变成草稿
- 管理用户的文章操作发布成功变成已发表状态, 发布失败重新变成草稿
结构
在状态模式结构图中包含如下几个角色:
- Context(环境类):环境类又称为上下文类,它是拥有多种状态的对象。由于环境类的状态存在多样性且在不同状态下对象的行为有所不同,因此将状态独立出去形成单独的状态类。在环境类中维护一个抽象状态类State的实例,这个实例定义当前状态,在具体实现时,它是一个State子类的对象。
- State(抽象状态类):它用于定义一个接口以封装与环境类的一个特定状态相关的行为,在抽象状态类中声明了各种不同状态对应的方法,而在其子类中实现类这些方法,由于不同状态下对象的行为可能不同,因此在不同子类中方法的实现可能存在不同,相同的方法可以写在抽象状态类中。
- Concrete State(具体状态类):它是抽象状态类的子类,每一个子类实现一个与环境类的一个状态相关的行为,每一个具体状态类对应环境的一个具体状态,不同的具体状态类其行为有所不同。
在状态模式的使用过程中,一个对象的状态之间还可以进行相互转换,通常有两种实现状态转换的方式:
public:voidchangeState(){/判断属性值,根据属性值进行状态转换if(value=0){this->setState(newConcreteStateA());}elseif(value=1){this->setState(newConcreteStateB());}}public:voidchangeState(Context*ctx){/根据环境对象中的属性值进行状态转换if(ctx->getValue()=1){ctx->setState(newConcreteStateB());}elseif(ctx->getValue()=2){ctx->setState(newConcreteStateC());}}实现
// State.h// 抽象状态classSanji;classAbstractState{public:virtualvoidworking(Sanji*sanji)=0;virtual~AbstractState(){}};// 上午状态classForenoonState:publicAbstractState{public:voidworking(Sanji*sanji)override;};// 中午状态classNoonState:publicAbstractState{public:voidworking(Sanji*sanji)override;};// 下午状态classAfternoonState:publicAbstractState{public:voidworking(Sanji*sanji)override;};// 晚上状态classEveningState:publicAbstractState{public:voidworking(Sanji*sanji)override;};#include<iostream>#include"State.h"#include"Sanji.h"usingnamespacestd;voidForenoonState::working(Sanji*sanji){inttime=sanji->getClock();if(time<8){cout<<"当前时间<"<<time<<">点, 准备早餐, 布鲁克得多喝点牛奶..."<<endl;}elseif(time>8&&time<11){cout<<"当前时间<"<<time<<">点, 去船头钓鱼, 储备食材..."<<endl;}else{sanji->setState(newNoonState);sanji->working();}}voidNoonState::working(Sanji*sanji){inttime=sanji->getClock();if(time<13){cout<<"当前时间<"<<time<<">点, 去厨房做午饭, 给路飞多做点肉..."<<endl;}else{sanji->setState(newAfternoonState);sanji->working();}}voidAfternoonState::working(Sanji*sanji){inttime=sanji->getClock();if(time<15){cout<<"当前时间<"<<time<<">点, 准备下午茶, 给罗宾和娜美制作爱心甜点..."<<endl;}elseif(time>15&&time<18){cout<<"当前时间<"<<time<<">点, 和乔巴去船尾钓鱼, 储备食材..."<<endl;}else{sanji->setState(newEveningState);sanji->working();}}voidEveningState::working(Sanji*sanji){inttime=sanji->getClock();if(time<19){cout<<"当前时间<"<<time<<">点, 去厨房做晚饭, 让索隆多喝点汤..."<<endl;}else{cout<<"当前时间<"<<time<<">点, 今天过得很高兴, 累了睡觉了..."<<endl;}}// Sanji.h#pragmaonce#include"State.h"classSanji{public:Sanji(){m_state=newForenoonState;}//工作函数,在不同的时间状态下,工作的内容也不同voidworking(){m_state->working(this);}//设置山治当前的状态voidsetState(AbstractState*state){if(m_state!=nullptr){deletem_state;}m_state=state;}//设置当前的时间voidsetClock(inttime){m_clock=time;}//得到当前的时间intgetClock(){returnm_clock;}~Sanji(){deletem_state;}private://通过这整形的时钟变量来描述一天中当前这个时刻的时间点intm_clock=0;// 时钟//通过这个状态指针来保存当前描述山治状态的对象AbstractState*m_state=nullptr;};intmain(){Sanji*sanji=newSanji;// 时间点vector<int>data{7,10,12,14,16,18,22};for(constauto&item:data){sanji->setClock(item);sanji->working();}deletesanji;return0;}特点
在实际开发中,状态模式具有较高的使用频率,在工作流和游戏开发中状态模式都得到了广泛的应用,例如公文状态的转换、游戏中角色状态切换等。
主要优点
- 封装了状态的转换规则,在状态模式中可以将状态的转换代码封装在环境类或者具体状态类中,可以对状态转换代码进行集中管理,而不是分散在一个个业务方法中。
- 将所有与某个状态有关的行为放到一个类中,只需要注入一个不同的状态对象即可使环境对象拥有不同的行为。
- 允许状态转换逻辑与状态对象合成一体,而不是提供一个巨大的条件语句块,状态模式可以让我们避免使用庞大的条件语句来将业务方法和状态转换代码交织在一起。
- 可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。
主要缺点
- 状态模式的使用必然会增加系统中类和对象的个数,导致系统运行开销增大。
- 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱,增加系统设计的
难度。
- 状态模式对“开闭原则”的支持并不太好,
- 增加新的状态类需要修改那些负责状态转换的源代码,否则无法转换到新增状态;
- 修改某个状态类的行为也需修改对应类的源代码。
适用环境
- 如果对象需要根据当前自身状态进行不同的行为, 同时状态的数量非常多且与状态相关的代码会频繁变更或者类对象在改变自身行为时需要使用大量的条件语句时,可使用状态模式。
- 如果对象需要根据自身当前状态进行不同行为,同时状态的数量非常多且与状态相关的代码会频繁变更的话,可使用状态模式。
- 如果某个类需要根据成员变量的当前值改变自身行为,从而需要使用大量的条件语句时,可使用该模式。
- 当相似状态和基于条件的状态机转换中存在许多重复代码时,可使用状态模式。
