生活中的设计模式之状态模式
定义
lets an object alter its behavior when its internal state changes. It appears as if the object changed its class.
当一个对象的内部状态转变时让它改变其行为,这看起来像是对象改变了其类。
实列
生活中,有很多类似于电视、电梯、售卖机这样的对象,对它们的同一操作会因对象当前所处的状态的不同而表现出不同的行为。
比如说,一部处于空闲状态的电梯,当我们按下打开这个操作按钮时,电梯会表现出开门的行为;
而当它处于运行状态时,同样的操作却不会表现出相同的行为。
再比如,一个人当他处于愉悦状态时,我们拿他开玩笑,他可能还笑嘻嘻的;
但是,如果他处于郁闷状态时,我们在拿他开玩笑,他还可能笑嘻嘻的吗?甚至有可能把我们揍一顿。
所以,可以看出,有些对象的行为依赖于它当前所处的状态,状态不一样同样的操作表现出的行为看起来像变了一个人似的。
故事
让我们用代码简单模拟一下电脑处于不同状态下,我们按下键盘时,电脑所表现出的行为。
我们的这台电脑有打开、关机、锁屏、打字等四项操作,也有启动中、运行中、睡眠中,关闭等四种状态。
当电脑处于启动状态时,所有操作都无效,直到自动进入运行状态;
当电脑处于运行状态时,关机、睡眠、打字是有效的,而打开操作是无效的;
当电脑处于睡眠状态时,关机,打开是有效的,而锁屏、打字是无效的;
当电脑处于关机状态时,打开是有效的,其它关机、锁屏、打字是无效的。
下面是代码实现:
/**电脑*/
public class Computer {
/**启动中状态*/
protected final static int STARTING_STATE=1;
/**运行中*/
protected final static int RUNNING_STATE=2;
/**睡眠中状态*/
protected final static int SLEEPING_STATE=3;
/**关闭状态*/
protected final static int CLOSED_STATE=4;
/**当前状态,初始状态为关闭状态*/
protected int currentState=CLOSED_STATE;
/**打开操作*/
public void open(){
switch (this.currentState){
case STARTING_STATE:
//已经在启动,什么都不做
break;
case RUNNING_STATE:
//已经运行,什么都不做
break;
case SLEEPING_STATE:
System.out.println("睡眠中===>运行中");
currentState=CLOSED_STATE;
break;
case CLOSED_STATE:
System.out.println("关闭===>打开中");
currentState=STARTING_STATE;
//启动完成后自动进入运行状态
Thread.sleep(10000);
System.out.println("10秒后,打开中===>运行中");
currentState=RUNNING_STATE;
break;
}
}
/**关机操作*/
public void close(){
switch (this.currentState){
case STARTING_STATE:
//启动中,不能关闭
break;
case RUNNING_STATE:
System.out.println("运行中===>关闭状态");
currentState=CLOSED_STATE;
break;
case SLEEPING_STATE:
System.out.println("睡眠中===>关闭状态");
currentState=CLOSED_STATE;
break;
case CLOSED_STATE:
//已经关闭,什么都不做
break;
}
}
/**锁屏操作*/
public void lockScreen(){
switch (this.currentState){
case STARTING_STATE:
//启动中,不能锁屏
break;
case RUNNING_STATE:
System.out.println("运行中===>睡眠中");
currentState=SLEEPING_STATE;
break;
case SLEEPING_STATE:
//已经是睡眠中,什么都不做
break;
case CLOSED_STATE:
//关闭状态,操作无效
break;
}
}
/**打字操作*/
public void print(String input){
switch (this.currentState){
case STARTING_STATE:
//启动中,不能打字
break;
case RUNNING_STATE:
System.out.println("打印客户输入的信息:"+input);
break;
case SLEEPING_STATE:
//睡眠状态,操作无效
break;
case CLOSED_STATE:
//关闭状态,操作无效
break;
}
}
}
问题
从故事中可以看出,一个操作被执行时,会不会发生状态轮转要基于当前状态和轮转规则。
比如,当锁屏lockScreen操作被执行时,当前状态如果是启动中那么不会发生状态轮转,如果是运行中那么会轮转到睡眠状态。
而这些轮转规则都是通过条件语句case硬编码实现的,这就导致了状态的轮转规则和上下文类Computer紧密耦合。
如果要为对象扩展一种新的状态,那么硬编码的轮转规则,很可能会导致我们要修改所有操作中的轮转规则。
即使不是扩展而是要修改某个状态的轮转规则,也可能需要修改所有操作中的轮转规则。还有,随着项目迭代不断地增加条件语句,势必会造成代码难以维护。
甚至在某些对动态性有要求的项目中,会要求轮转规则可以动态增加和移除,那这有怎么办呢?
所以,有没有一种方式可以解决轮转规则的扩展、修改、维护问题,这便是状态模式——当状态切换时同时切换其行为。
方案
在状态模式中,有状态的对象被称之为上下文,它的状态不再是用一个简单的基本数据类型int来表示,而是使用一个单独的对象即状态对象来表示。
也就是说,它会把上下文中所有可能的状态都定义成一个状态对象,并把相关的转换规则和对象行为封装到状态对象中,而且它们都实现至同一个接口。
这样,当上下文的操作被触发时,它会将具体的任务委托给状态对象进行处理,状态对象要么根据当前状态以及封装在其中的轮转规则来切换上下文中的当前状态,要么表现出具体的行为,其中每一个状态对象表现出的行为可能都不一样。
应用
接下来,我们使用状态模式重构一下故事中的程序。
首先,声明一个统一的抽象状态类,它的操作与上下文中涉及状态变化的操作相对应,这样就可以使上下文具有不同的行为表现。
/**状态抽象类*/
public abstract class State {
public void open(Computer context){}
public void close(Computer context){}
public void lockScreen(Computer context){}
public void print(Computer context,String input){}
}
然后,让所有的具体状态都实现状态接口,并将行为封装到该状态对象中。
/**关闭状态*/
public class ClosedSate extends State{
@Override
public void open(Computer context) {
//状态轮转到启动状态
context.setCurrentState(Computer.OPENING_SATE);
}
}
/**启动状态*/
public class OpeningSate extends State{
@Override
public void open(Computer context) {
//10s后进入运行状态
context.setCurrentState(context.RUNNING_STATE);
}
}
/**运行状态*/
public class RunningSate extends State{
@Override
public void close(Computer context) {
context.setCurrentState(Computer.CLOSED_STATE);
}
@Override
public void lockScreen(Computer context) {
context.setCurrentState(Computer.SLEEPING_STATE);
}
@Override
public void print(Computer context,String input) {
System.out.println("打印客户输入的信息:"+input);
}
}
/**睡眠状态*/
public class SleepingSate extends State{
@Override
public void open(Computer context) {
context.setCurrentState(Computer.RUNNING_STATE);
}
@Override
public void close(Computer context) {
context.setCurrentState(Computer.CLOSED_STATE);
}
}
最后,我们修改一下Computer,让它将任务委托给当前状态对象进行处理。
/**电脑*/
public class Computer {
/**启动中状态*/
public final static State OPENING_SATE=new OpeningSate();
/**运行中*/
public final static State RUNNING_STATE=new RunningSate();
/**睡眠中状态*/
public final static State SLEEPING_STATE=new SleepingSate();
/**关闭状态*/
public final static State CLOSED_STATE=new ClosedSate();
/**当前状态,初始状态为关闭状态*/
protected State currentState=CLOSED_STATE;
public void setCurrentState(State currentState) {
this.currentState = currentState;
}
/**打开操作*/
public void open(){
currentState.open(this);
}
/**关机操作*/
public void close(){
currentState.close(this);
}
/**锁屏操作*/
public void lockScreen(){
currentState.lockScreen(this);
}
/**打字操作*/
public void print(String input){
currentState.open(this);
}
}
结构
avatar抽象状态角色(State) :它声明了一个状态对应的行为接口,该接口中的操作是对上下文中行为的抽象。
具体状态角色(ConcreteState):它封装了特定状态下对应的行为,负责执行上下文委派的任务。
上下文角色(Context) :它持有一个指向当前状态对象的引用,当它的操作被触发时,它会将任务委托给当前状态对象进行处理。
总结
当一个有状态对象,因其状态不同会表现出不同的行为时,为了避免状态轮转规则与该对象耦合,应该将状态轮转规则抽象成一个单独的对象并将相关的行为封装到状态对象对应的操作中。
这样,可以避免转换规则重复、代码难以维护、冗长等常见问题,也能运行时动态增加或移除某个状态,还能在少量修改原有代码的情况下扩展新的状态。