目录
本文的结构如下:
- 引言
- 什么是状态模式
- 模式的结构
- 典型代码
- 代码示例
- 状态模式和策略模式的区别
- 优点和缺点
- 适用环境
- 模式应用
一、引言
要说现在的生活真的是蛮方便的,就拿现在的共享单车来说,下班时间,主要线路都堵爆了,以往只能龟行在车海中,现在只需找一辆单车骑回去,省时还健身。
但现在的共享单车基本上都需要先扫码开锁。如果找到一辆单车,但是却坏掉了,会显示等待维修之类的状态,扫不开,也骑不了;然后要换下一辆,刚好有一辆上面挂着塑料袋,旁边还有人扶着,你过去扫码,肯定是显示已占用之类的状态,同样是扫不开,骑不了;只有正常的单车才能扫开,然后让你骑行。这里面有几种状态,正常等待扫码状态,损坏等待维修状态,正在骑行状态......
在软件开发中,同样有这样的情况,有些对象也具有多种状态,这些状态在某些情况下能够相互转换,而且对象在不同的状态下也将具有不同的行为。为了更好地对这些具有多种状态的对象进行设计,可以使用一种被称之为状态模式的设计模式。
二、什么是状态模式
状态模式用于解决系统中复杂对象的状态转换以及不同状态下行为的封装问题。当系统中某个对象存在多个状态,这些状态之间可以进行转换,而且对象在不同状态下行为不相同时可以使用状态模式。
状态模式将一个对象的状态从该对象中分离出来,封装到专门的状态类中,使得对象状态可以灵活变化,对于客户端而言,无须关心对象状态的转换以及对象所处的当前状态,无论对于何种状态的对象,客户端都可以一致处理。
状态模式定义如下:
状态模式(State Pattern):允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。其别名为状态对象(Objects for States),状态模式是一种对象行为型模式。
“允许一个对象在其内部状态改变时改变它的行为”应该比较好理解,“对象看起来似乎修改了它的类”表达不是很清楚,大概的意思是对象的状态转变对客户端是不可知的,但却表现出不同的行为,就像修改了类一样。
三、模式的结构
状态模式的UML类图如下:
可以发现状态模式的类图和策略模式是一模一样的,那这两个模式有什么区别,这个后面再说,先继续看状态模式的结构。
在状态模式结构图中包含如下几个角色:
- Context(环境类):环境类又称为上下文类,它是拥有多种状态的对象。由于环境类的状态存在多样性且在不同状态下对象的行为有所不同,因此将状态独立出去形成单独的状态类。在环境类中维护一个抽象状态类State的实例,这个实例定义当前状态,在具体实现时,它是一个State子类的对象。
- State(抽象状态类):它用于定义一个接口以封装与环境类的一个特定状态相关的行为,在抽象状态类中声明了各种不同状态对应的方法,而在其子类中实现类这些方法,由于不同状态下对象的行为可能不同,因此在不同子类中方法的实现可能存在不同,相同的方法可以写在抽象状态类中。
- ConcreteState(具体状态类):它是抽象状态类的子类,每一个子类实现一个与环境类的一个状态相关的行为,每一个具体状态类对应环境的一个具体状态,不同的具体状态类其行为有所不同。
四、典型代码
状态模式将对象在不同状态下的行为封装到不同的状态类中,为了让系统具有更好的灵活性和可扩展性,同时对各状态下的共有行为进行封装,需要对状态进行抽象,引入了抽象状态类角色,其典型代码如下:
public abstract class State {
//不同的状态有不同的实现,也可是是不同的状态有不同的方法
public abstract void handle();
}
不同的具体状态类可以提供完全不同的方法实现,也可能包含多个业务方法,如果需要,应该将通用的方法放到抽象层,其典型代码如下:
public class ConcreteStateA extends State {
public void handle() {
//todo
}
}
public class ConcreteStateB extends State {
public void handle() {
//todo
}
}
环境类中需维持一个对抽象状态类的引用,可以通过调用setState()方法改变其状态(即不同的状态实现),再在环境类的业务方法中调用状态对象的方法,典型代码如下所示:
public class Context {
private State state;
private int value; //某个属性值,对象状态发生变化由该值引发
public void setState(State state){
this.state = state;
}
public void request(){
this.state.handle();
}
}
环境类实际上是真正拥有状态的对象,状态模式遵循“封装变化”的指导思想,将环境类中与状态有关的代码提取出来封装到专门的状态类中,之所以说是变化的,因为一个对象的状态之间可以进行相互转换。
这种转换是通过改变state的具体指向达到的,多是通过暴露的公有setState()方法实现,一般有两种方式,一种是在Context类中,根据value值的变化,改变状态;另一种就是在具体状态类中根据value值变化,改变状态,Context应该提供一个getValue()的方法。
五、代码示例
当今社会,论坛贴吧很多,我们也会加入感兴趣的论坛,偶尔进行发言,但有时却会发现不能发帖了,原来是昨天的某个帖子引发了口水战,被举报了。这里就用论坛发帖为例,简单用代码描述一下:
假设有三种状态,normal(正常),restricted(受限),closed(封号),判断依据是一个健康值(这里只是假设)。
5.1、不用状态模式
public class Account {
public static final int NORMAL = 1;
public static final int RESTRICTED = 2;
public static final int CLOSED = 3;
private int healthValue;
private int state;
/**
* 看帖
*/
public void view(){
System.out.println("正常看帖");
//todo healthValue改变算法
changeState();
}
/**
* 评论
*/
public void comment(){
if (state == NORMAL || state == RESTRICTED){
System.out.println("正常评论");
//todo healthValue改变算法
changeState();
}else if (state == CLOSED){
System.out.println("抱歉,你的健康值小于-10,不能评论");
}
}
/**
* 发帖
*/
public void post(){
if (state == NORMAL){
//todo 一些health值改变算法
changeState();
System.out.println("正常发帖");
}else if (state == RESTRICTED || state == CLOSED){
System.out.println("抱歉,你的健康值小于0,不能发帖");
}
}
public void changeState(){
if (healthValue <= -10){
state = CLOSED;
}else if (-10 < healthValue && healthValue<= 0){
state = RESTRICTED;
}else if (healthValue > 0){
state = CLOSED;
}
}
public int getHealthValue() {
return healthValue;
}
public void setState(int state) {
this.state = state;
}
}
上面的代码很简单,能够实现需要的功能,但是却有几个问题:
- 看帖和发帖方法中都包含状态判断语句,以判断在该状态下是否具有该方法以及在特定状态下该方法如何实现,导致代码非常冗长,可维护性较差;
- 拥有一个较为复杂的changeState()方法,包含大量的if...else...语句用于进行状态转换,代码测试难度较大,且不易于维护;
- 系统扩展性较差,如果需要增加一种新的状态,如hot状态(活跃用户,该状态用户发帖积分增加更多),需要对原有代码进行大量修改,扩展起来非常麻烦。
5.2、使用状态模式
状态模式可以在一定程度上解决上述问题,在状态模式中将对象在每一个状态下的行为和状态转移语句封装在一个个状态类中,通过这些状态类来分散冗长的条件转移语句,让系统具有更好的灵活性和可扩展性。
抽象状态和具体状态类:
public abstract class State {
protected Account account;
public void view(){
System.out.println("正常看帖");
//todo healthValue改变算法
changeState();
}
public abstract void comment();
public abstract void post();
public void changeState(){
if (account.getHealthValue() <= -10){
account.setState(account.getClosedState());
}else if (account.getHealthValue() > -10 && account.getHealthValue() <= 0){
account.setState(account.getRestrictedState());
}else if (account.getHealthValue()>0){
account.setState(account.getNormalState());
}
}
}
public class NormalState extends State{
public NormalState(Account account){
this.account = account;
}
public void comment(){
System.out.println("正常评论");
//todo healthValue改变算法
changeState();
}
public void post() {
System.out.println("正常发帖");
//todo healthValue改变算法
changeState();
}
}
public class RestrictedState extends State {
public RestrictedState(Account account){
this.account = account;
}
public void comment(){
System.out.println("正常评论");
//todo healthValue改变算法
changeState();
}
public void post() {
System.out.println("抱歉,你的健康值小于0,不能发帖");
}
}
public class ClosedState extends State {
public ClosedState(Account account){
this.account = account;
}
public void comment(){
System.out.println("抱歉,你的健康值小于-10,不能评论");
}
public void post() {
System.out.println("抱歉,你的健康值小于-10,不能发帖");
}
public void changeState() {
if (account.getHealthValue() <= -10){
account.setState(account.getClosedState());
}else if (account.getHealthValue() > -10 && account.getHealthValue() <= 0){
account.setState(account.getRestrictedState());
}
}
}
环境类(账户):
public class Account {
private State normalState, restrictedState, closedState;
private String name;
private int healthValue;
private State state;
public Account(String name){
normalState = new NormalState(this);
restrictedState = new RestrictedState(this);
closedState = new ClosedState(this);
this.healthValue = 1;//新账号默认为1
this.state = normalState;
this.name = name;
}
/**
* 看帖
*/
public void view(){
System.out.println(name + "正在看帖");
state.view();
System.out.println("当前账户状态为:" + state.getClass().getName());
}
/**
* 评论
*/
public void comment(){
System.out.println(name + "正在评论");
state.comment();
System.out.println("当前账户状态为:" + state.getClass().getName());
}
/**
* 发帖
*/
public void post(){
System.out.println(name + "正在发帖");
state.post();
System.out.println("当前账户状态为:" + state.getClass().getName());
}
public int getHealthValue() {
return healthValue;
}
public void setState(State state) {
this.state = state;
}
public State getNormalState() {
return normalState;
}
public State getRestrictedState() {
return restrictedState;
}
public State getClosedState() {
return closedState;
}
}
六、状态模式和策略模式的区别
前面说了,它们的UML类图一模一样,但却是两种设计模式,这里面肯定是有原因的。
- 策略(如促销一种商品的策略)和状态(如同一个按钮来控制一个电梯的状态,又如手机界面中一个按钮来控制手机)是两种完全不同的思想。对状态模式而言,状态的转换是一个核心内容,而且这种转换是对客户端不可见的;然而于选择策略,转换与此毫无关系,它允许一个客户选择或提供一种策略。
- 策略是一组方案,可以相互替换,策略模式用于随不同外部环境而主动采取不同行为的场合。
- 状态模式处理的核心问题是状态的转换,把各个状态和相应的实现步骤封装成一组简单的继承自一个接口或抽象类的类,通过另外的一个Context来操作他们之间的自动状态转换,通过event来自动实现各个状态之间的跳转。在整个生命周期中存在一个状态的转换曲线,这个转换曲线对客户是透明的。
- 在状态模式中,状态的转换是由对象的内部条件决定,外界只需关心其接口,不必关心其状态对象的创建和转化;而策略模式里,采取何种策略由外部条件(C)决定。
七、优点和缺点
7.1、优点
状态模式的主要优点如下:
- 封装了状态的转换规则,在状态模式中可以将状态的转换代码封装在环境类或者具体状态类中,可以对状态转换代码进行集中管理,而不是分散在一个个业务方法中。
- 将所有与某个状态有关的行为放到一个类中,只需要注入一个不同的状态对象即可使环境对象拥有不同的行为。
- 允许状态转换逻辑与状态对象合成一体,而不是提供一个巨大的条件语句块,状态模式可以避免使用庞大的条件语句来将业务方法和状态转换代码交织在一起。
- 可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。
7.2、缺点
状态模式的主要缺点如下:
- 状态模式的使用必然会增加系统中类和对象的个数,导致系统运行开销增大。
- 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱,增加系统设计的难度。
- 状态模式对“开闭原则”的支持并不太好,增加新的状态类需要修改那些负责状态转换的源代码,否则无法转换到新增状态;而且修改某个状态类的行为也需修改对应类的源代码。
八、适用环境
在以下情况下可以使用状态模式:
- 对象的行为依赖于它的状态(属性)并且可以根据它的状态改变而改变它的相关行为。
- 代码中包含大量与对象状态有关的条件语句,这些条件语句的出现,会导致代码的可维护性和灵活性变差,不能方便地增加和删除状态,使客户类与类库之间的耦合增强。
九、模式应用
状态模式在工作流或游戏等类型的软件中得以广泛使用,甚至可以用于这些系统的核心功能设计,如在政府OA办公系统中,一个批文的状态有多种:尚未办理;正在办理;正在批示;正在审核;已经完成等各种状态,而且批文状态不同时对批文的操作也有所差异。使用状态模式可以描述工作流对象(如批文)的状态转换以及不同状态下它所具有的行为。