# 状态模式
# 现实案例
- 我们需要实现一个糖果机;
- 糖果机有很多不同的工作状态:
- 在 “没有 25 分钱的状态” 下,放进 25 分钱的硬币,就会进入 “有 25 分钱的状态”;
- 在处于 “有 25 分钱的状态” 下,转动曲柄可以改变到 “售出糖果状态”;
- 或者退回硬币回到 “没有 25 分钱状态”;
- 糖果售出后依旧回到 “没有 25 分钱状态”;
- 但如果剩余糖果数目为 0,那么就进入到 “糖果售罄” 状态。并且退回 25 分钱硬币同时进入 “没有 25 分钱状态”;
# 实现上面的设计
先列出所有的状态:
- 没有 25 分钱;
- 有 25 分钱;
- 售出糖果;
- 糖果售罄;
然后创建一堆常量来代表各个状态,并且定义一个实例变量来保存当前的状态;
- 从状态图中可以看出,系统中会出现如下几个操作:
- 投入 25 分钱;
- 返回 25 分钱;
- 转动曲柄;
- 发放糖果;
- 这些是糖果机需要实现的接口;
- 我们创建一个糖果机类,然后每个动作对应一个方法;
- 在方法中用条件语句来决定在每个状态内,什么行为是恰当的;
展开查阅代码
public class GumballMachine {
final static int SOLD_OUT = 0;
final static int NO_QUARTER = 1;
final static int HAS_QUARTER = 2;
final static int SOLD = 3;
int state = SOLD_OUT;
int count = 0;
//存储糖果数量
public GumballMachine(int count) {
this.count = count;
if (count > 0) {
state = NO_QUARTER;
}
}
//当有25分钱投入,就会执行这个方法
public void insertQuarter() {
if (state == HAS_QUARTER) {
System.out.println("如果已投入过25分钱,我们就告诉顾客不要再投");
} else if (state == NO_QUARTER) {
state = HAS_QUARTER;
System.out.println("如果是在“没有25分钱”的状态下,你应该投25分钱");
} else if (state == SOLD_OUT) {
System.out.println("如果糖果已经售罄,我们就拒绝收钱");
} else if (state == SOLD) {
System.out.println("如果顾客刚才买了糖果,就需要稍等一些,好让状态转换完毕,恢复到“没有25分钱”的状态");
}
}
//先在,如果顾客试着退回25分钱就执行这个方法
public void ejectQuarter() {
if (state == HAS_QUARTER) {
System.out.println("如果有25分钱,我们就把钱退出来,回到“没有25分钱”的状态");
state = NO_QUARTER;
} else if (state == NO_QUARTER) {
System.out.println("如果没有25分钱的话,当然不能退出25分钱");
} else if (state == SOLD) {
System.out.println("顾客已经转动曲柄就不能再退钱了,他已经拿到糖果了");
} else if (state == SOLD_OUT) {
System.out.println("如果糖果售罄,就不能接受25分钱,当然也不可能退钱");
}
}
//顾客试着转动曲柄
public void turnCrank() {
if (state == SOLD) {
System.out.println("别想骗过机器拿两次糖果");
} else if (state == NO_QUARTER) {
System.out.println("我们需要先投入25分钱");
} else if (state == SOLD_OUT) {
System.out.println("我们不能给糖果,已经没有任何糖果了");
} else if (state == HAS_QUARTER) {
System.out.println("成功,他们拿到糖果了," + "改变状态到“售出糖果”然后条用机器的dispense()方法");
state = SOLD;
dispense();
}
}
//调用此方法,发放糖果
public void dispense() {
if (state == SOLD) {
System.out.println("我们正在”出售糖果“状态,给他们糖果");
count = count - 1;
/* 我们在这里处理“糖果售罄的情况,如果这是最后一个糖果,将机器的状态设置到“糖果售罄”” 否则就回到“没有25分钱”的状态 */
if (count == 0) {
System.out.println();
state = SOLD_OUT;
} else {
state = NO_QUARTER;
}
} else if (state == NO_QUARTER) {
System.out.println("这些都不应该发生,但是如果做了,就得到错误提示");
} else if (state == SOLD_OUT) {
System.out.println("这些都不应该发生,但是如果做了,就得到错误提示");
} else if (state == HAS_QUARTER) {
System.out.println("这些都不应该发生,但是如果做了,就得到错误提示");
}
}
@Override
public String toString() {
return String.valueOf(this.count);
}
}
# 上面 👆 代码实现的缺点
- 如果我想加一个新功能:顾客转动曲柄的时候有 10% 的几率掉出来两个糖果;
- 那么上面的代码就要添加一堆新的 if/else 条件判断。毫无疑问,这会让代码越来越复杂,也很难维护;
- 这份代码没有遵守 “开放-关闭原则”;
- 这个设计不符合面向对象;
- 我们没有把会变化的那部分抽离出来;
- 扩展时会很容易出现 BUG;
# 状态模式
- 状态模式:允许对象在内部状态改变时,改变它的行为;
- 状态模式将状态封装成独立的类,类中定义了各自的行为;
- 客户想要执行某些行为时,把动作委托给当前客户所处的状态的实例对象;
- 状态实例的对应行为就会被调用;
# 状态模式的实现
- 我们首先定义出一个 State 接口,它是所有具体状态类的共同接口;
- 之后去实现一堆具体状态,每个状态各自定义了属于自己的行为(接口中方法的实现);
- 之后我们有一个客户,它会内部用一个变量维护当前自身的状态。并且拥有一个方法改变当前状态;
- 当客户调用一个 State 接口中定义的行为时,当前保存的状态实例的对应行为就会被调用;
# 用状态模式改写糖果机
现在让我们用状态模式来改写糖果机:
- 首选定义 State 接口:
public interface State {
public void insertQuater();
public void ejectQuater();
public void turnCrank();
public void dispense();
}
- 下面分别去实现各个状态类;
- 注意状态类中需要有一个糖果机的实例引用;
- 为了使用糖果机实例的
setState
方法改变糖果机的当前状态;
- 这里只展示一种状态的写法;
- 下面是糖果机类的实现;
- 里面需要一堆实例属性去保存状态实例;
- 为了实现转动曲柄时有 10% 几率得到两个糖果;
- 那么我们就要改变 HasQuarterState 有 25 分钱状态类中的
turnCrank
方法; - 让曲柄转动时,进行随机数测试,来决定是否顾客中奖了;
- 与此同时,我们还要添加一个 WinnerState 类,来表示中奖状态;
# 状态模式 vs 策略模式
- 状态模式和策略模式的很相近,都是通过组合的形式,把具体的行为委托给其他的一组类;
- 但是两个模式的区别在于它们的 “意图”;
- 状态模式:把一堆行为封装到状态对象中,状态对象会自己根据情况改变客户对象中保存的状态。但是客户对此浑然不知,一切都是自动的;
- 策略模式:客户主动指定,当前的行为委托给哪个策略对象执行;
- 一般而言,我们把策略模式当成除了继承之外,另一种弹性的获得行为的方案。我们通过组合不同的对象,来让一个对象获取其他对象的行为;
- 而状态模式,一般是很多 if/else 条件判断的替代方案。不同状态下,客户有不同的行为,通过创建状态对象,我们把行为委托给状态对象;