状态模式用于解决系统中复杂对象的状态转换以及不同状态下行为的封装问题
模式动机
很多情况下,一个对象的行为取决于一个或多个动态变化的属性,这样的属性叫做状态。一个对象可以拥有多个状态,这些状态可以相互转换,当对象状态不同时,其行为也有所差异。
假设一个人就是对象,人根据心情不同会有很多状态,比如开心和伤心,这两种状态可以相互转换。开心的人可能会突然接到女朋友的分手电话,然后哭得稀里哗啦(醒醒!你哪来的女朋友?),过了一段时间后,又可能因为中了一百万彩票而欢呼雀跃。而且不同状态下人的行为也不同,有些人伤心时会通过运动、旅行、听音乐来缓解心情,而开心时则可能会唱歌、跳舞、请客吃饭等等。
再来考虑软件系统中的情况,如某酒店订房系统,可以将房间设计为一个类,房间对象有已预订、空闲、已入住等情况,这些状态之间可以相互转换,并且不同状态的对象可能具有不同的行为,如已预订或已入住的房间不能再接收其他顾客的预订,而空闲的房间可以接受预订。
在过去我们遇到这种情况,可以使用复杂的条件判断来进行状态判断和转换操作,这会导致代码的可维护性和灵活性下降,当出现新的状态时必须修改源代码,违反了开闭原则。在状态模式中,可以将对象状态从包含该状态的类中分离出来,做成一个个单独的状态类,如人的两种情绪可以设计成两个状态类:
将开心与伤心两种情绪从“人”中分离出来,从而避免在“人”中进行状态转换和判断,将拥有状态的对象和状态对应的行为分离,这就是状态模式的动机。
模式定义
允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。其别名为状态对象(Objects for States),状态模式是一种对象行为型模式。
Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.
模式结构与分析
我们把拥有状态的对象称为环境类,也叫上下文类。再引入一个抽象状态类来专门表示对象的状态,对象的每一种具体状态类都继承该抽象类,不同具体状态类实现不同状态的行为,包括各种状态之间的转换。在环境类中维护一个抽象状态类 State 的实例,用来定义当前状态。
得到状态模式结构类图如下:
环境类中的 request() 方法处理业务逻辑,根据状态去调用对应的 handle() 方法,如果需要切换状态,还提供了 setState() 用于设置当前房间状态。如果我们希望执行操作后状态自动发生改变,那么我们还需要在 State 中定义一个 Context 对象,实现一个双向依赖关系。
考虑前面提到的订房系统,如果不使用状态模式,可能就会存在如下代码:
if (state == "空闲") {
if (预订房间) {
预订操作;
state = "已预订";
} else if (住进房间) {
入住操作;
state = "已入住";
}
} else if(state == "已预订") {
if (住进房间) {
入住操作;
state = "已入住";
} else if (取消预订) {
取消操作;
state = "空闲";
}
}
上述代码需要做频繁且复杂的判断操作,可维护性很差。因此考虑使用状态模式将房间类的状态分离出来,将与每种状态有关的操作封装在独立的状态类中。
我们来写一个完整的示例
环境类(Room)
public class Room {
// 维护一个状态对象
private State state;
public Room() {
// 默认为空闲状态
this.state = new IdleState(this);
}
public State getState() {
return state;
}
public void setState(State state) {
this.state = state;
}
public void reserve() {
state.reserve();
}
public void checkIn() {
state.checkIn();
}
public void cancelReserve() {
state.cancelReserve();
}
public void checkOut() {
state.checkOut();
}
}
抽象状态类(State)
public abstract class State {
// 用于状态转换
protected Room room;
public State(Room room) {
this.room = room;
}
public abstract void reserve();
public abstract void checkIn();
public abstract void cancelReserve();
public abstract void checkOut();
}
具体状态类(IdleState)
public class IdleState extends State {
public IdleState(Room room) {
super(room);
}
@Override
public void reserve() {
System.out.println("房间预订成功");
// 切换状态
room.setState(new ReservedState(room));
}
@Override
public void checkIn() {
System.out.println("房间入住成功");
room.setState(new InhabitedState(room));
}
@Override
public void cancelReserve() {
System.out.println("无法取消预订,房间处于空闲状态");
}
@Override
public void checkOut() {
System.out.println("无法退房,房间处于空闲状态");
}
}
具体状态类(ReservedState)
public class ReservedState extends State {
public ReservedState(Room room) {
super(room);
}
@Override
public void reserve() {
System.out.println("无法预订,房间处于已预订状态");
}
@Override
public void checkIn() {
System.out.println("房间入住成功");
room.setState(new InhabitedState(room));
}
@Override
public void cancelReserve() {
System.out.println("取消预订成功");
room.setState(new IdleState(room));
}
@Override
public void checkOut() {
System.out.println("无法退房,房间处于已预订状态");
}
}
具体状态类(InhabitedState)
public class InhabitedState extends State {
public InhabitedState(Room room) {
super(room);
}
@Override
public void reserve() {
System.out.println("无法预订,房间处于入住状态");
}
@Override
public void checkIn() {
System.out.println("无法入住,房间处于入住状态");
}
@Override
public void cancelReserve() {
System.out.println("无法取消预订,房间处于入住状态");
}
@Override
public void checkOut() {
System.out.println("退房成功");
room.setState(new IdleState(room));
}
}
客户端测试类(Client)
public class Client {
public static void main(String[] args) {
Room room = new Room();
room.cancelReserve();
room.checkOut();
room.reserve();
System.out.println("--------------------------");
room.reserve();
room.checkOut();
room.checkIn();
System.out.println("--------------------------");
room.reserve();
room.checkIn();
room.cancelReserve();
room.checkOut();
}
}
运行结果
模式优缺点
状态模式的优点:
- 封装了转换规则,将不同状态之间的转换状态封装在状态类中,避免了冗长的条件判断,提高了代码的可维护性
- 将所有与某个规则有关的行为放到一个类,可以很方便地增加新的状态
- 可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数
状态模式的缺点:
- 增加了系统类和对象的个数
- 结构较为复杂,使用不当将导致代码混乱
- 对于可以切换状态的状态模式,增加新的状态类需要修改负责状态转换的代码