# 状态模式

# 现实案例

  • 我们需要实现一个糖果机;
  • 糖果机有很多不同的工作状态:
    • 在 “没有 25 分钱的状态” 下,放进 25 分钱的硬币,就会进入 “有 25 分钱的状态”;
    • 在处于 “有 25 分钱的状态” 下,转动曲柄可以改变到 “售出糖果状态”;
    • 或者退回硬币回到 “没有 25 分钱状态”;
    • 糖果售出后依旧回到 “没有 25 分钱状态”;
    • 但如果剩余糖果数目为 0,那么就进入到 “糖果售罄” 状态。并且退回 25 分钱硬币同时进入 “没有 25 分钱状态”;

2020-2-22-17-48-58.png

# 实现上面的设计

  • 先列出所有的状态:

    • 没有 25 分钱;
    • 有 25 分钱;
    • 售出糖果;
    • 糖果售罄;
  • 然后创建一堆常量来代表各个状态,并且定义一个实例变量来保存当前的状态;

2020-2-22-17-57-46.png

  • 从状态图中可以看出,系统中会出现如下几个操作:
    • 投入 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 接口中定义的行为时,当前保存的状态实例的对应行为就会被调用;

2020-2-22-18-20-19.png

# 用状态模式改写糖果机

现在让我们用状态模式来改写糖果机:

  • 首选定义 State 接口:
public interface State {
  public void insertQuater();
  public void ejectQuater();
  public void turnCrank();
  public void dispense();
}
  • 下面分别去实现各个状态类;
    • 注意状态类中需要有一个糖果机的实例引用;
    • 为了使用糖果机实例的 setState 方法改变糖果机的当前状态;
  • 这里只展示一种状态的写法;

2020-2-22-18-26-19.png

  • 下面是糖果机类的实现;
  • 里面需要一堆实例属性去保存状态实例;

2020-2-22-18-30-36.png

  • 为了实现转动曲柄时有 10% 几率得到两个糖果;
  • 那么我们就要改变 HasQuarterState 有 25 分钱状态类中的 turnCrank 方法;
  • 让曲柄转动时,进行随机数测试,来决定是否顾客中奖了;

2020-2-22-18-38-42.png

  • 与此同时,我们还要添加一个 WinnerState 类,来表示中奖状态;

2020-2-22-18-39-39.png

# 状态模式 vs 策略模式

  • 状态模式和策略模式的很相近,都是通过组合的形式,把具体的行为委托给其他的一组类;
  • 但是两个模式的区别在于它们的 “意图”;
    • 状态模式:把一堆行为封装到状态对象中,状态对象会自己根据情况改变客户对象中保存的状态。但是客户对此浑然不知,一切都是自动的;
    • 策略模式:客户主动指定,当前的行为委托给哪个策略对象执行
  • 一般而言,我们把策略模式当成除了继承之外,另一种弹性的获得行为的方案。我们通过组合不同的对象,来让一个对象获取其他对象的行为;
  • 状态模式,一般是很多 if/else 条件判断的替代方案。不同状态下,客户有不同的行为,通过创建状态对象,我们把行为委托给状态对象;
上次更新: 8/22/2020, 8:14:09 AM