GoF23:State-状态
1、case:糖果机
1.1、业务背景
设计一个糖果机,工作原理如下:
- 圆圈:代表一个状态;
- 箭头:代表一个动作,即状态的转换;
整理
-
状态
-
动作
1.2、CandyMachine
- 成员变量
- state:代表状态;
- count:代表糖果数目;
- 静态常量:代表状态值;
- 方法:代表动作。
public class CandyMachine {
private final static int NO_COIN = 0;
private final static int HAS_COIN = 1;
private final static int SOLD_OUT = 2;
private final static int SOLD = 3;
private int state;
private int count;
public CandyMachine(int count) {
this.count = count;
if (count > 0) {
state = NO_COIN;
} else {
state = SOLD_OUT;
}
}
public void insertCoin() {
switch (state) {
case NO_COIN:
state = HAS_COIN;
System.out.println("Inserted successfully");
break;
case HAS_COIN:
System.out.println("Already inserted");
break;
case SOLD_OUT:
System.out.println("Sold out, can't insert");
break;
case SOLD:
System.out.println("Waiting for a candy");
break;
default:
}
}
public void ejectQuarter() {
switch (state) {
case NO_COIN:
System.out.println("No coin");
break;
case HAS_COIN:
state = NO_COIN;
System.out.println("Coin returned");
break;
case SOLD_OUT:
System.out.println("Sold out, no coin");
break;
case SOLD:
System.out.println("Already turned the crank");
break;
default:
}
}
// turnCrank()
// dispense()
}
1.3、分析
缺点
- 代码重复
- 每个方法,必须对状态的所有情况进行判断(多重 if-else 或 switch-case 语句);
- 每个方法,对状态的情况有相同的处理;
- 开闭原则
- 加入新的状态,需要修改所有方法代码;
- 业务流程改变,需要修改现有代码逻辑;
- 面向过程:该设计实际是面向过程,而不是面向对象。
改进
- 封装变化
- 定义状态类,将状态的行为封装在类中;
- 局部化每个状态的行为,修改一个状态不会影响其它状态;
- 组合 & 委托
- 作为 状态 的客户,CandyMachine 在类中定义成员变量(即状态);
- 将请求委托给状态的方法处理。
2、State
2.1、设计方案
- 定义 State 接口
- 成员变量:状态所属的客户(即糖果机)
- 方法:对应客户的动作。
- 实现 State 类,实现该状态下的行为。
- 客户(CandyMachine)将动作委托给状态对象。
2.2、State
接口
- 成员变量:状态所属的客户,构造器注入;
- 方法默认实现:抛出 UnsupportedOperationException 异常。
public class State {
protected CandyMachine machine;
public State(CandyMachine machine) {
this.machine = machine;
}
public void insertCoin() {
throw new UnsupportedOperationException("Unsupported Operation in current state");
}
public void ejectCoin() {
throw new UnsupportedOperationException("Unsupported Operation in current state");
}
public void turnCrank() {
throw new UnsupportedOperationException("Unsupported Operation in current state");
}
public void dispense() {
throw new UnsupportedOperationException("Unsupported Operation in current state");
}
}
实现类
继承 State
- 对本状态有意义的操作方法,则重写;
- 无意义的操作方法,使用默认实现即可。
实现
-
NoCoinState
public class NoCoinState extends State { public NoCoinState(CandyMachine machine) { super(machine); } @Override public void insertCoin() { machine.setState(machine.getHasCoinState()); System.out.println("Inserted successfully"); } }
-
HasCoinState
public class HasCoinState extends State { public HasCoinState(CandyMachine machine) { super(machine); } @Override public void ejectCoin() { machine.setState(machine.getNoCoinState()); System.out.println("Coin returned"); } @Override public void turnCrank() { machine.setState(machine.getSoldState()); System.out.println("Crank turned"); } }
-
SoldState
public class SoldState extends State { public SoldState(CandyMachine machine) { super(machine); } @Override public void dispense() { if (machine.getCount() > 0) { machine.releaseCandy(); machine.setState(machine.getNoCoinState()); } else { machine.setState(machine.getSoldOutState()); System.out.println("Sorry, out of candy"); } } }
-
SoldOutState
public class SoldOutState extends State { public SoldOutState(CandyMachine machine) { super(machine); } }
2.3、客户:CandyMachine
- 状态变量:修改为 State 接口类型,构造器初始化;
- 动作方法:委托给状态类;
- PS
- 糖果机的用户转动曲柄后,机器自动分发糖果;
- 因此,不需要定义turnCrank() 和 dispense() 两个方法,而是在 turnCrank() 中委托状态类的两个方法。
public class CandyMachine {
private final State noCoinState;
private final State hasCoinState;
private final State soldOutState;
private final State soldState;
private State state;
private int count;
public CandyMachine(int count) {
this.count = count;
noCoinState = new NoCoinState(this);
hasCoinState = new HasCoinState(this);
soldOutState = new SoldOutState(this);
soldState = new SoldState(this);
if (count > 0) {
state = noCoinState;
} else {
state = soldOutState;
}
}
public void insertCoin() {
state.insertCoin();
}
public void ejectCoin() {
state.ejectCoin();
}
public void turnCrank() {
state.turnCrank();
state.dispense();
}
public void releaseCandy() {
if (count > 0) {
System.out.println("A candy comes...");
count--;
}
}
// setter:状态
// getter:状态值、count
}
2.4、测试
@Test
public void test() {
CandyMachine machine = new CandyMachine(5);
machine.insertCoin();
machine.ejectCoin();
System.out.println("=======");
machine.insertCoin();
machine.turnCrank();
}
3、状态模式
3.1、定义
状态模式:允许对象在内部状态改变时,改变其行为。
- 将状态封装成独立的类,并将动作委托给当前状态对象;
- 通过组合和委托机制,引用不同的状态对象;
- 在状态改变时修改对象的行为,对客户而言就像改变了类。
- OOP原则
- 开闭
- 单一职责:负责持有状态实例,提供必要方法。而状态的改变由状态类内部完成。
3.2、类图
- Context:上下文类
- 内部持有 状态实例;
- 当 Context类 的方法被请求时,委托给状态类处理;
- State:状态
- 处理来自 Context 的请求;
- 每个 ConcreteState 都提供了本状态对请求的实现;
- 当 Context 状态改变时,行为也随之改变。
3.3、策略 vs 状态
策略模式 和 状态模式 的类图完全相同,但模式的意图不同。
策略模式 | 状态模式 | |
---|---|---|
思想 | 封装算法 | 封装状态行为 |
组合对象 | 客户在运行时动态设置策略对象(主动) | 状态对象按预先的设定改变(预设) |
使用场景 | 作为继承的弹性替代方案 | 避免在 context 中使用大量的条件判断 |
3.4、分析
- 状态转换
- 当状态转换是固定时,适合放在 Context 中;
- 当状态转换是动态时,适合放在 状态类 中。(会导致状态类之间产生依赖)
- 客户不会直接与状态交互
- 客户指 Context 类的用户;
- 状态被组合在 Context 类中,用来表示它的内部状态和行为;
- 只有 Context 类会对 状态类 发出请求。
- 使用状态模式,导致类的数目增加,但提高了系统弹性。
- 使用 interface 或 class,都可以达到目的。但是 class 可以对方法默认实现。
- State对象 可以被多个 Context实例 共享
- 前提:State对象 不能持有它们自己的内部状态;
- 做法:将每个状态,指定到静态实例变量中。
4、小结
状态模式:允许对象在内部状态改变时,改变其行为。在状态改变时修改对象的行为,对客户而言就像改变了类。
- 封装变化:将状态封装成独立的类。局部化每个状态的行为,修改一个状态不会影响其它状态。
- 委托:将 客户 的请求,委托给当前状态对象。
- OOP原则:开闭、单一职责。
- 模式对比
- 策略:封装算法;
- 模板方法:封装算法结构(子步骤);
- 状态:封装状态行为。
- 状态转变:根据状态转变的动态性,可由 Context类 或 State类 控制。
- State对象 可以被多个 Context实例 共享。