关于状态模式的定义,我就直接引用Head First了:状态模式允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。状态模式的意图是封装状态,将状态封装成独立的类,并将动作委托给当前的状态对象,让当前状态在状态对象间游走改变,从而使我们能够通过组合的方式引用不同的状态对象来改变类的行为。
以地铁站自动售票机为例(与真实流程会有出入,此处重点在于封装状态的思想),用户可执行如下操作:投币、退款、购买、出票4个动作,而执行这些动作会因用户处于不同状态而具有不同的行为。如:
1、在未投币状态,用户可选择投币并进入已投币状态,而不能退款、购票、打印车票;
2、在已投币状态,用户可继续投币,或退款回到未投币状态,或购票进入购票状态,但不能直接打印车票。
3、在购票状态,也可理解成提交订单状态,购票成功则自动出票,并回到未投币状态。
如果我们不使用状态模式,直接将流程描述成业务代码,我们会发现每个动作都会伴随着大量的 if 判断,假设我们又扩展一个“非运营时间状态”呢?甚至多个呢?是不是非常不弹性,不符合开闭原则。
public class TicketMachine {
static final NoSlotStateType = 0;
static final SlotStateType = 1;
static final BuyingTicketStateType = 2;
/**
* 投币的动作。
*
* 退款、购票、打印车票的动作就不展示了。
* 同样需要判断当前是何种状态。
*
*
如果我们再增加其他状态呢?
* 是不是每个方法都需要重新添加判断。
*/
public void toSlot(int money) {
// 当前是未投币状态
if (curState == NoSlotStateType)
curState = SlotState;
// 当前是已投币状态
else if (curState == SlotStateType)
System.out.println("已再次投币!");
// 当前是购票状态
else if (curState == BuyingTicketStateType)
System.out.println("此状态不支持投币!");
}
... // 其他方法逻辑
}
而状态模式就是一种可以当作不需要多条件判断的替代方案。我们将每个状态单独抽离成类,而在每个状态对象中,各自响应着用户的动作。下面采用状态模式实现的自动售票机类:
public class TicketMachine {
/**
* 这是状态类的具体实现。
* 详情见下{@code NoSlotState}。
*/
NoSlotState noSlotState; // 未投币状态
SlotState slotState; // 已投币状态
BuyingTicketState btState; // 购票中状态
TicketState curState; // 当前状态
/**
* 在构造中初始化当前状态为未投币状态。
*/
public TicketMachine() {
this.curState = noSlotState;
noSlotState = new NoSlotState(this);
slotState = new SlotState(this);
btState = new BuyingTicketState(this);
}
/**
* 将动作委托给当前的状态对象处理。
*/
public void toSlot(int money) {
curState.toSlot();
}
public void refund() {
curState.refund();
}
public void tickets() {
curState.tickets();
// 当购买请求成功后,会自动出票
printTicket();
}
public void printTicket() {
curState.printTicket();
}
/**
* 当前状态在状态对象间游走改变。
*/
public void setState(TicketState state) {
this.curState = state;
}
/**
* 提供了get方法,防止状态对象间彼此耦合。
*/
public TicketState getNoSlotState() {
return noSlotState;
}
public TicketState getSlotState() {
return slotState;
}
public TicketState getBuyingState() {
return btState;
}
}
这是我们的状态接口类。之所以使用抽象类,是因为我们可以将每个状态内复用的处理逻辑,放在超类中。
public abstract class TicketState {
/**
* 这是投币动作。
*
* 我们将用户的操作抽离出来。
* 其实状态模式就是多条件判断的替代方案。
* 在每个状态下,用户都有可能会执行下面
* 的动作,在不合规时要提醒用户。
*/
abstract void toSlot(int money);
/**
* 这是退款动作。
*/
abstract void refund();
/**
* 这是购票动作。
*/
abstract void tickets();
/**
* 这是打印车票动作。
*/
abstract void printTicket();
... // 复用逻辑省略
}
这是我们的未投币首页类。可以看到在此类中,处理着在“未投币状态”下用户所有可能会触发的操作,并随时更新自动售票机的状态。
public NoSlotState implements TicketState {
TicketMachine tm;
public NoSlotState(TicketMachine tm) {
this.tm = tm;
}
/**
* 在未投币状态,执行投币操作后,变成已投币状态。
*/
@Override public void toSlot(int money) {
tm.setState(tm.getSlotState());
}
/**
* 在未投币状态,不能退款等操作。
*/
@Override public void refund() {
System.out.println("您还未投币!");
}
@Override public void tickets() {
System.out.println("需投币后操作!");
}
@Override public void printTicket() {
System.out.println("需投币后操作!");
}
}
这是我们的已投币状态类:
public SlotState implements TicketState {
TicketMachine tm;
public SlotState(TicketMachine tm) {
this.tm = tm;
}
/**
* 在购买前支持多次投币。
*/
@Override public void toSlot(int money) {
tm.setState(tm.getSlotState());
}
/**
* 购票前可以申请退款。
*/
@Override public void refund() {
tm.setState(tm.getNoSlotState());
}
/**
* 如果金额足够,则发起购票请求。
*/
@Override public void tickets() {
if (isEnough)
tm.setState(tm.getBuyingState());
else
System.out.println("金额不够!");
}
@Override public void printTicket() {
System.out.println("够票后可操作!");
}
}
这是我们的购票状态类:
public BuyingTicketState implements TicketState {
TicketMachine tm;
public BuyingTicketState(TicketMachine tm) {
this.tm = tm;
}
/**
* 出票中,不支持投币、退款、重复提交等操作。
*/
@Override public void toSlot(int money) {
System.out.println("出票中,不支持投币!");
}
@Override public void refund() {
System.out.println("出票中,不支持退款!");
}
@Override public void tickets() {
System.out.println("不支持重复提交打印请求");
}
/**
* 出票成功后,切换状态为未投票状态。
*/
@Override public void printTicket() {
System.out.println("已出票!");
tm.setState(tm.getNoSlotState());
}
}
通过上面代码我们能发现,我们不仅避免了在售票机类TicketMachine 中大量的 if 判断,还将“主要的变化”封装并抽离了出来,让当前已有的状态对修改关闭。当然状态模式也会造成大量的小类,这是为了弹性而付出的代价,但这绝对是值得的。其实真正重要的是我们暴露给客户的类数目,而不是我们自己的类数目,我们完全可以将这些额外的状态类对外隐藏起来。