状态模式

状态模式

项目组在使用观察者模式完成气象站项目之后,名声在外,这次又接到了来自于无人售货行业的万能糖果公司的订单。

自助糖果售货机

该公司要设计一款自动糖果售货机,开始时,售货机的状态是"没钱",投入钱后状态变为"有钱",然后转动曲柄,糖果机会吐出糖果。
用一张状态图来表现如下:
状态模式_第1张图片

从这张图中,我们可以识别出售货机的状态:没钱、有钱、出售中、售罄。导致状态发生迁移的动作有:投钱、退钱、转动曲柄、发放糖果。

V1版本

按照直观的方式,我们来实现V1版本的糖果机。

/**
 * 自助售货机
 */
public class GumballMachineV1 {

    final static int SALE_OUT       = 0;    //售罄
    final static int NO_QUARTER     = 1;    //没有硬币
    final static int HAS_QUARTER    = 2;    //有硬币
    final static int SOLD           = 3;    //出售中

    private int state = SALE_OUT;
    private int count;

    public GumballMachineV1(int count) {
        this.count = count;
        if(this.count > 0){
            state = NO_QUARTER;
        }
    }

    //投入硬币的动作
    public void insertQuarter(){
        if(state == NO_QUARTER){
            //状态变化
            state = HAS_QUARTER;
            System.out.println("你投入了硬币");
        }else if(state == SALE_OUT){
            System.out.println("售罄状态下,不能投入硬币");
        }else if(state == HAS_QUARTER){
            System.out.println("你已经投入硬币,不需要继续投入");
        }else if (state == SOLD){
            System.out.println("请稍等,正在出糖果");
        }else{
            System.out.println("未知状态,怕是出错比较好。");
        }
    }

    //退钱的动作
    public void ejectQuarter(){
        if(state == NO_QUARTER){
            System.out.println("没有投入硬币,退不了");
        }else if(state == SALE_OUT){
            System.out.println("售罄状态下,不能投入硬币,当然也不能退钱了");
        }else if(state == HAS_QUARTER){
            //状态变化
            state = NO_QUARTER;
            System.out.println("退钱给你了");
        }else if (state == SOLD){
            System.out.println("正在出糖果中,不能退钱");
        }else{
            System.out.println("未知状态,怕是出错比较好。");
        }
    }

    //按下购买按钮(转动曲柄)
    public void turnCrank(){
        if(state == NO_QUARTER){
            System.out.println("还没有投入钱,转动也没用");
        }else if(state == SALE_OUT){
            System.out.println("售罄状态下,不能投入硬币,转动没用");
        }else if(state == HAS_QUARTER){
            //状态变化
            state = SOLD;
            System.out.println("接受转动,出糖中...");
            dispense();
        }else if (state == SOLD){
            System.out.println("正在出糖果中,等着吧你。");
        }else{
            System.out.println("未知状态,怕是出错比较好。");
        }
    }

    //发放糖果(机器内部自动触发)
    public void dispense(){
        if(state == NO_QUARTER){
            System.out.println("先付钱,才能给糖果");
        }else if(state == SALE_OUT){
            System.out.println("都没糖果了,给不了..");
        }else if(state == HAS_QUARTER){
            System.out.println("先转动下曲柄,才能给你糖果哦");
        }else if (state == SOLD){
            System.out.println("来,给你糖果。。");
            //状态变化
            count--;
            if(count > 0){
                state = NO_QUARTER;
            }else{
                state = SALE_OUT;
            }
        }else{
            System.out.println("未知状态,怕是出错比较好。");
        }
    }

    @Override
    public String toString() {
        final StringBuffer sb = new StringBuffer("GumballMachineV1{");
        sb.append("state=").append(state);
        sb.append(", count=").append(count);
        sb.append('}');
        return sb.toString();
    }
}

该版本中,机器的每个动作,我们使用if-else判断机器当前状态,从而做出正确的行为。
v1版本的测试

/**
 * 自助售货机测试
 */
public class GumballMachineMainV1 {
    public static void main(String[] args) {
        GumballMachineV1 machine = new GumballMachineV1(3);

        System.out.println(machine);

        machine.insertQuarter();
        machine.turnCrank();

        System.out.println(machine);

        machine.insertQuarter();
        machine.ejectQuarter();
        machine.turnCrank();

        System.out.println(machine);

        machine.insertQuarter();
        machine.turnCrank();
        machine.insertQuarter();
        machine.turnCrank();

        System.out.println(machine);
        machine.insertQuarter();
    }
}

V1版实现对应对变化的能力有限,比如现在万能糖果公司老板觉得可以将购买糖果当做一个游戏,每次购买有10%的几率中奖,可以获得两颗糖果。
如果要在V1上对此进行扩展,除了要添加状态外(这个还好说),每个动作中的if-else都要新增加对新状态的判断,这回让代码一团糟的,不满足开闭原则。

V2版本

既然状态,甚至导致状态变化的动作是变化的,那么我们能不能将变化抽取出来,封装到单独的类中,然后通过组合使用?
沿着这个思路,我们使用V2来重构

首先抽象出来状态接口,其提供了状态机的所有行为。

/**
 * 状态接口
 *  定义自助售货机的所有方法,每种状态都有一种实现,分别实现在该种状态下的行为。
 */
public interface StateV2 {

    void insertQuarter();

    void ejectQuarter();

    void turnCrank();

    void dispense();
}

接下来,实现4中具体的状态。具体状态的实现依据是我们的状态图。

/**
 * 没币状态
 */
public class NoQuarterStateV2 implements StateV2 {
    GumballMachineV2 machine;

    public NoQuarterStateV2(GumballMachineV2 machine) {
        this.machine = machine;
    }

    @Override
    public void insertQuarter() {
        System.out.println("你投入了硬币");
        //状态变更
        machine.changeState(machine.getHasQuarter());
    }

    @Override
    public void ejectQuarter() {
        System.out.println("没有投入硬币,退不了");
    }

    @Override
    public void turnCrank() {
        System.out.println("还没有投入钱,转动也没用");
    }

    @Override
    public void dispense() {
        System.out.println("先付钱,才能给糖果");
    }
}

/**
 * 有币状态
 */
public class HasQuarterStateV2 implements StateV2 {
    GumballMachineV2 machine;

    public HasQuarterStateV2(GumballMachineV2 machine) {
        this.machine = machine;
    }

    @Override
    public void insertQuarter() {
        System.out.println("你已经投入硬币,不需要继续投入");

    }

    @Override
    public void ejectQuarter() {
        //状态变更
        System.out.println("退钱给你了");
        machine.changeState(machine.getNoQuarter());
    }

    @Override
    public void turnCrank() {
        //状态变化
        System.out.println("接受转动,出糖中...");
        machine.changeState(machine.getSold());
    }

    @Override
    public void dispense() {
        System.out.println("先转动下曲柄,才能给你糖果哦");
    }
}

/**
 * 出货状态
 */
public class SoldStateV2 implements StateV2 {
    GumballMachineV2 machine;

    public SoldStateV2(GumballMachineV2 machine) {
        this.machine = machine;
    }

    @Override
    public void insertQuarter() {
        System.out.println("请稍等,正在出糖果。。。。");
    }

    @Override
    public void ejectQuarter() {
        System.out.println("正在出糖果中,不能退钱");
    }

    @Override
    public void turnCrank() {
        System.out.println("正在出糖果中,等着吧你。");
    }

    @Override
    public void dispense() {
        System.out.println("来,给你糖果。。");
        //状态变化
        machine.countDEC();
        if(machine.getCount() > 0){
            machine.changeState(machine.getNoQuarter());
        }else{
            machine.changeState(machine.getSaleOut());
        }
    }
}

/**
 * 售罄状态
 */
public class SoldOutStateV2 implements StateV2 {
    GumballMachineV2 machine;

    public SoldOutStateV2(GumballMachineV2 machine) {
        this.machine = machine;
    }

    @Override
    public void insertQuarter() {
        System.out.println("售罄状态下,不能投入硬币");
    }

    @Override
    public void ejectQuarter() {
        System.out.println("售罄状态下,不能投入硬币,当然也不能退钱了");
    }

    @Override
    public void turnCrank() {
        System.out.println("售罄状态下,不能投入硬币,转动没用");
    }

    @Override
    public void dispense() {
        System.out.println("都没糖果了,给不了..");
    }
}

此时,我们的糖果售货机就简单了,它将行为委托它的当前状态来实现。

/**
 * 自助售货机
 */
public class GumballMachineV2 {

    private StateV2 saleOut       ;    //售罄
    private StateV2 noQuarter     ;    //没有硬币
    private StateV2 hasQuarter    ;    //有硬币
    private StateV2 sold           ;    //出售中

    private StateV2 state;
    private int count;

    public GumballMachineV2(int count) {
        saleOut = new SoldOutStateV2(this);
        noQuarter = new NoQuarterStateV2(this);
        hasQuarter = new HasQuarterStateV2(this);
        sold = new SoldStateV2(this);
        this.count = count;
        if(this.count > 0){
            state = noQuarter;
        }

    }

    //投入硬币的动作
    public void insertQuarter(){
        state.insertQuarter();
    }

    //退钱的动作
    public void ejectQuarter(){
        state.ejectQuarter();
    }

    //按下购买按钮(转动曲柄)
    public void turnCrank(){
        state.turnCrank();
        dispense();
    }

    //发放糖果(机器内部自动触发)
    public void dispense(){
        state.dispense();
    }

    //状态变更。
    public void changeState(StateV2 newState){
        state = newState;
    }

    //减库存
    public void countDEC(){
        this.count--;
    }

    public StateV2 getSaleOut() {
        return saleOut;
    }

    public void setSaleOut(StateV2 saleOut) {
        this.saleOut = saleOut;
    }

    public StateV2 getNoQuarter() {
        return noQuarter;
    }

    public void setNoQuarter(StateV2 noQuarter) {
        this.noQuarter = noQuarter;
    }

    public StateV2 getHasQuarter() {
        return hasQuarter;
    }

    public void setHasQuarter(StateV2 hasQuarter) {
        this.hasQuarter = hasQuarter;
    }

    public StateV2 getSold() {
        return sold;
    }

    public void setSold(StateV2 sold) {
        this.sold = sold;
    }

    public StateV2 getState() {
        return state;
    }

    public void setState(StateV2 state) {
        this.state = state;
    }

    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        this.count = count;
    }

    @Override
    public String toString() {
        final StringBuffer sb = new StringBuffer("GumballMachineV2{");
        sb.append(", state=").append(state);
        sb.append(", count=").append(count);
        sb.append('}');
        return sb.toString();
    }
}

最后,测试下v2代码

/**
 * 自助售货机测试
 */
public class GumballMachineMainV2 {
    public static void main(String[] args) {
        GumballMachineV2 machine = new GumballMachineV2(3);

        System.out.println(machine);

        machine.insertQuarter();
        machine.turnCrank();

        System.out.println(machine);

        machine.insertQuarter();
        machine.ejectQuarter();
        machine.turnCrank();

        System.out.println(machine);

        machine.insertQuarter();
        machine.turnCrank();
        machine.insertQuarter();
        machine.turnCrank();

        System.out.println(machine);
        machine.insertQuarter();
    }
}

V2代码中,我们为每个状态单独封装起来,并且在各自状态下对行为做了自己的实现。
糖果机通过组合方式使用状态,其当前状态在不同的状态之间转换,对外表现就像,糖果机的同一方法在不停的变化一样。

V2版本的UML
状态模式_第2张图片

现在,我们在V2的基础上来加入前面提到的变化,即新加入一个新的中奖状态的V3版实现。

V3版本

首先,我们要提供中奖状态的实现

/**
 * 中奖状态
 */
public class WinStateV3 implements StateV3 {
    GumballMachineV3 machine;

    public WinStateV3(GumballMachineV3 machine) {
        this.machine = machine;
    }

    @Override
    public void insertQuarter() {
        System.out.println("请稍等,正在出糖果。。。。");
    }

    @Override
    public void ejectQuarter() {
        System.out.println("正在出糖果中,不能退钱");
    }

    @Override
    public void turnCrank() {
        System.out.println("正在出糖果中,等着吧你。");
    }

    @Override
    public void dispense() {
        System.out.println("恭喜你,你中奖了买一送一。。");
        oneGumball();
        if (machine.getState() != machine.getSaleOut()){
            oneGumball();
        }
    }

    private void oneGumball() {
        System.out.println("来,给你糖果。。");
        //状态变化
        machine.countDEC();
        if(machine.getCount() > 0){
            machine.changeState(machine.getNoQuarter());
        }else{
            machine.changeState(machine.getSaleOut());
        }
    }
}

然后,需要修改 有币状态 中的状态流转,让10%的几率流转到 中奖状态。

/**
 * 有币状态
 */
public class HasQuarterStateV3 implements StateV3 {
    GumballMachineV3 machine;
    Random random;

    public HasQuarterStateV3(GumballMachineV3 machine) {
        this.machine = machine;
        this.random = new Random();
    }

    @Override
    public void insertQuarter() {
        System.out.println("你已经投入硬币,不需要继续投入");
    }

    @Override
    public void ejectQuarter() {
        //状态变更
        System.out.println("退钱给你了");
        machine.changeState(machine.getNoQuarter());
    }

    @Override
    public void turnCrank() {
        //状态变化
        System.out.println("接受转动,出糖中...");
        int winRand = random.nextInt(10);
        System.out.println("---"+winRand);
        if(winRand == 0){
            machine.changeState(machine.getWin());
        }else{
            machine.changeState(machine.getSold());
        }

    }

    @Override
    public void dispense() {
        System.out.println("先转动下曲柄,才能给你糖果哦");
    }
}

当然,售货机中新加入中奖状态的实例

/**
 * 自助售货机
 */
public class GumballMachineV3 {

    private StateV3 saleOut       ;    //售罄
    private StateV3 noQuarter     ;    //没有硬币
    private StateV3 hasQuarter    ;    //有硬币
    private StateV3 sold           ;    //出售中
    private StateV3 win           ;    //中奖

    private StateV3 state;
    private int count;

    public GumballMachineV3(int count) {
        saleOut = new SoldOutStateV3(this);
        noQuarter = new NoQuarterStateV3(this);
        hasQuarter = new HasQuarterStateV3(this);
        sold = new SoldStateV3(this);
        win = new WinStateV3(this);
        this.count = count;
        if(this.count > 0){
            state = noQuarter;
        }

    }

    //投入硬币的动作
    public void insertQuarter(){
        state.insertQuarter();
    }

    //退钱的动作
    public void ejectQuarter(){
        state.ejectQuarter();
    }

    //按下购买按钮(转动曲柄)
    public void turnCrank(){
        state.turnCrank();
        dispense();
    }

    //发放糖果(机器内部自动触发)
    public void dispense(){
        state.dispense();
    }

    //状态变更。
    public void changeState(StateV3 newState){
        state = newState;
    }

    //减库存
    public void countDEC(){
        this.count--;
    }

    public StateV3 getSaleOut() {
        return saleOut;
    }

    public void setSaleOut(StateV3 saleOut) {
        this.saleOut = saleOut;
    }

    public StateV3 getNoQuarter() {
        return noQuarter;
    }

    public void setNoQuarter(StateV3 noQuarter) {
        this.noQuarter = noQuarter;
    }

    public StateV3 getHasQuarter() {
        return hasQuarter;
    }

    public void setHasQuarter(StateV3 hasQuarter) {
        this.hasQuarter = hasQuarter;
    }

    public StateV3 getSold() {
        return sold;
    }

    public void setSold(StateV3 sold) {
        this.sold = sold;
    }

    public StateV3 getState() {
        return state;
    }

    public void setState(StateV3 state) {
        this.state = state;
    }

    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        this.count = count;
    }

    public StateV3 getWin() {
        return win;
    }

    public void setWin(StateV3 win) {
        this.win = win;
    }

    @Override
    public String toString() {
        final StringBuffer sb = new StringBuffer("GumballMachineV3{");
        sb.append("state=").append(state);
        sb.append(", count=").append(count);
        sb.append('}');
        return sb.toString();
    }
}

测试代码和V2一样。

可以看出,通过封装变化,多用组合的原则之后,我们的代码扩展性明显提高了。

V3版的UML
状态模式_第3张图片

上面的V2和V3,就是使用了状态模式来解决问题。

定义

状态模式,允许对象内部的状态发生变化时,改变它的行为。从外面看起来好像修改了它的类。

这个模式将对象的状态封装为独立的类,调用对象的行为委托给当前状态的行为,这就导致了当状态发生变化时,其行为也会跟着发生变化。

用代码来实现一波(context中的状态变更) 状态A–(action1)–>状态B–(action2)–>状态C–(action3)–>状态A

/**
 * 状态接口
 */
public interface State {
    void action1();
    void action2();
    void action3();
}

具体状态实现(内含状态转换)

/**
 * 状态A
 */
public class ConcreteStateA implements State {
    private Context context;

    public ConcreteStateA(Context context) {
        this.context = context;
    }

    @Override
    public void action1() {
        System.out.println("状态A时,action1动作,状态切换到B...");
        context.changeState(context.getStateB());
    }

    @Override
    public void action2() {
    }

    @Override
    public void action3() {
    }
}

/**
 * 状态B
 */
public class ConcreteStateB implements State {
    private Context context;

    public ConcreteStateB(Context context) {
        this.context = context;
    }
    @Override
    public void action1() {
    }

    @Override
    public void action2() {
        System.out.println("状态B时,action2动作...状态变更到C");
        context.changeState(context.getStateC());
    }

    @Override
    public void action3() {

    }
}

/**
 * 状态C
 */
public class ConcreteStateC implements State {
    private Context context;

    public ConcreteStateC(Context context) {
        this.context = context;
    }

    @Override
    public void action1() {

    }

    @Override
    public void action2() {

    }

    @Override
    public void action3() {
        System.out.println("状态C时,action3动作...状态变更到A");
        context.changeState(context.getStateA());
    }
}

状态上下文,组合状态的状态机。

/**
 * 状态上下文
 */
public class Context {

    private State stateA ;
    private State stateB ;
    private State stateC ;

    private State state;

    public Context() {
        stateA = new ConcreteStateA(this);
        stateB = new ConcreteStateB(this);
        stateC = new ConcreteStateC(this);

        this.state = stateA;
    }

    public void request1(){
        state.action1();
    }

    public void request2(){
        state.action2();
    }

    public void request3(){
        state.action3();
    }

    public void changeState(State state){
        this.state = state;
    }

    public State getStateA() {
        return stateA;
    }

    public State getStateB() {
        return stateB;
    }

    public State getStateC() {
        return stateC;
    }

    public State getState() {
        return state;
    }
}

测试

/**
 * 测试
 */
public class StateMain {
    public static void main(String[] args) {
        Context context = new Context();

        context.request1();
        context.request2();
        context.request3();

    }
}

状态模式UML
状态模式_第4张图片

如果看过策略模式的UML的话,你会发现状态模式的类结构和策略模式一模一样,同样的类组织方式,解决问题的思路是一样的,只不过关注点(目的)不一样而已。
策略模式抽象了算法接口,提供不同的算法实现并封装起来,在策略上下文中组合某一种算法实现。
状态模式抽象了状态接口,提供不同的状态实现并封装起来,在状态上下文中组合状态类,状态类的具体类型会在运行过程中迁移,从而让状态上下文表现出不同的行为。

扩展示例

示例参考 https://www.journaldev.com/1751/state-design-pattern-java。
实现电视遥控板的电源按钮功能,当电视状态是关机时,按下电源按钮的动作会打开电视,状态迁移到开机状态;当电视状态是开机状态时,按下电源按钮的动作会关闭电视,状态迁移到关机状态。

首先还是看下没有使用状态模式下的常规实现

/**
 * 通过IF-ELSE来实现基于状态的不同行为。
 */
public class TVRemoteBasic {
    private String state="OFF";

    public void setState(String state){
        this.state=state;
    }

    public void pressPowerButton(){
        if(state.equalsIgnoreCase("ON")){
            System.out.println("TV is turned OFF");
            setState("OFF");
        }else if(state.equalsIgnoreCase("OFF")){
            System.out.println("TV is turned ON");
            setState("ON");
        }
    }

    public static void main(String args[]){
        TVRemoteBasic remote = new TVRemoteBasic();
        remote.pressPowerButton();
        remote.pressPowerButton();
    }
}

这个不用多说,和糖果机的例子一样。

接下来,我们使用状态模式来改造一波。

首先,定义状态抽象(接口或者抽象来都可以)。


/**
 * 电视机状态
 */
public interface TvState {
    void pressPowerButton();
}

开机和关机状态实现:

/**
 * 开机状态
 */
public class TVStartState implements TvState {

    private TVContext context;

    public TVStartState(TVContext context) {
        this.context = context;
    }

    @Override
    public void pressPowerButton() {
        System.out.println("TV is turned OFF");
        context.setCurrentState(context.getOffState());
    }
}

/**
 * 关机状态
 */
public class TVStopState implements TvState {
    private TVContext context;

    public TVStopState(TVContext context) {
        this.context = context;
    }

    @Override
    public void pressPowerButton() {
        System.out.println("TV is turned ON");
        context.setCurrentState(context.getOnState());
    }
}

最后来实现状态上下文,也就是来组合我们状态的状态机,它会将收到的请求委托给当前状态类去实现。

/**
 * 电视机
 */
public class TVContext {

    private TvState onState ;
    private TvState offState;

    private TvState currentState;

    public TVContext() {
        onState = new TVStartState(this);
        offState = new TVStopState(this);
        currentState = offState;
    }

    public void pressPowerButton(){
        currentState.pressPowerButton();
    }

    public TvState getOnState() {
        return onState;
    }

    public void setOnState(TvState onState) {
        this.onState = onState;
    }

    public TvState getOffState() {
        return offState;
    }

    public void setOffState(TvState offState) {
        this.offState = offState;
    }

    public TvState getCurrentState() {
        return currentState;
    }

    public void setCurrentState(TvState currentState) {
        this.currentState = currentState;
    }
}

测试

/**
 * 测试
 */
public class TVRemote {
    public static void main(String[] args) {
        TVContext context = new TVContext();
        context.pressPowerButton();
        context.pressPowerButton();
        context.pressPowerButton();
    }
}

输出结果

TV is turned ON
TV is turned OFF
TV is turned ON

源码

https://gitee.com/cq-laozhou/design-pattern

你可能感兴趣的:(设计模式)