状态模式解决了什么问题

352 阅读7分钟

状态模式State Pattern

状态模式解决什么问题

在《Java开发手册-华山版》的编程规约-控制语句中有讲:超过 3 层的 if-else 的逻辑判断代码可以使用卫语句、策略模式、状态模式等来实现。

当控制一个对象状态转换的条件表达式过于复杂时,把相关“判断逻辑”提取出来,用各个不同的类进行表示,系统处于哪种情况。直接使用相应的状态类进行处理,这样能把原来复杂的逻辑判断简单化,消除了 if-else、switch-case 等冗余语句。这样的代码更有层次性,并且具备良好的扩展力。

什么是状态模式

定义:

允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。 (如:人都有高兴和伤心的时候,不同的情绪有不同的行为。当然外界也会影响其情绪变化。)

类结构图:

3b8a6f6834e4df7136c7ddf59e62c852f7a9ae88.gif

状态模式包含以下主要角色。

  1. 环境类(Context) :也称为上下文,它定义了客户端需要的接口,内部维护一个当前状态,并负责具体状态的切换。
  2. 抽象状态(State) :定义一个接口,用以封装环境对象中的特定状态所对应的行为,可以有一个或多个行为。
  3. 具体状态(Concrete State) :实现抽象状态所对应的行为,并且在需要的情况下进行状态切换。

状态模式的优缺点

优点:
  • 封装了转换规则。
  • 枚举可能的状态,在枚举状态之前需要确定状态种类。
  • 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。
  • 允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。
  • 可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。
缺点:
  • 状态模式的使用必然会增加系统类和对象的个数。
  • 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
  • 状态模式对"开闭原则"的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态,而且修改某个状态类的行为也需修改对应类的源代码。

状态模式简单例子

假设我们在开发一款糖果机游戏。

2022-05-22-17-12-21-image.png

图中可以发现,它有4个状态(没有硬币已经投币糖果售馨售出糖果),以及有4个动作(投入硬币退回硬币转动曲柄派发糖果

public class CandyMachine {

    final static int SOLD_OUT = 0; // 售馨状态
    final static int NO_COIN = 1; // 没硬币状态
    final static int HAS_COIN = 2; // 已投币状态
    final static int SOLD = 3;  // 售出状态

    private int state = SOLD_OUT;
    private int count = 0;

    public CandyMachine(int count) {
        this.count = count;
        if (count > 0) {
            state = NO_COIN;
        }
    }

    /**
     * 投入硬币
     */
    public void insertCoin() {
        switch (state) {
            case SOLD_OUT:
                System.out.println("糖果已售馨,无法投币");
                break;
            case NO_COIN:
                state = HAS_COIN;
                System.out.println("您已经投币,请转动曲柄");
                break;
            case HAS_COIN:
                System.out.println("您已经投币,无法继续投币");
                break;
            case SOLD:
                System.out.println("糖果正在发放中,请稍后");
                break;
        }
    }

    /**
     * 退回硬币
     */
    public void ejectCoin() {
        switch (state) {
            case SOLD_OUT:
                System.out.println("您还没有投币");
                break;
            case NO_COIN:
                System.out.println("您没有投过币");
                break;
            case HAS_COIN:
                System.out.println("退回硬币");
                state = NO_COIN;
                break;
            case SOLD:
                System.out.println("您已经转动过曲柄,无法退回硬币");
                break;
        }
    }

    /**
     * 转动曲柄
     */
    public void turnCrank() {
        switch (state) {
            case SOLD_OUT:
                System.out.println("糖果已售馨");
                break;
            case NO_COIN:
                System.out.println("您没有投过币,无法转动曲柄");
                break;
            case HAS_COIN:
                System.out.println("正在发放糖果");
                state = SOLD;
                dispense();
                break;
            case SOLD:
                System.out.println("多次转动曲柄无效");
                break;
        }
    }

    /**
     * 派发糖果
     */
    private void dispense() {
        switch (state) {
            case SOLD_OUT:
                System.out.println("程序错误");
                break;
            case NO_COIN:
                System.out.println("您得先投币");
                break;
            case HAS_COIN:
                System.out.println("程序错误");
                break;
            case SOLD:
                count = count - 1;
                System.out.println("您得到了一个糖果");
                if (count > 0) {
                    state = NO_COIN;
                } else {
                    state = SOLD_OUT;
                }
                break;
        }
    }

    public void printState() {
        switch (state) {
            case SOLD_OUT:
                System.out.println("***SOLD_OUT***");
                break;
            case NO_COIN:
                System.out.println("***NO_QUARTER***");
                break;
            case HAS_COIN:
                System.out.println("***HAS_QUARTER***");
                break;
            case SOLD:
                System.out.println("***SOLD***");
                break;
        }
    }
}

需求变动,在当前的逻辑上,加入有10%的概率可以拿到2粒糖果。这样便会多一个状态"中奖状态"。

在当前的逻辑上修改,首先,必须加上一个新的状态“WINNER”,然后给insertCoin ejectCoin turnCrank三个方法增加WINNER的判断,最后在turnCrank编写中奖状态的逻辑。(明显违反了,对扩展开放,对修改关闭的开闭原则,也不符合面对对象的设计。)

状态模式实现

为了让代码更容易维护,我们需要将当前的设计改成状态模式,重构需做的事情有:

  • 定义一个State接口。在接口内,糖果机的每个动作都有一个对应的方法。
  • 实现糖果的每个状态。将糖果机的动作委托到状态类。
public interface State {
    //插入硬币
    public void insertCoin();
    //退回硬币
    public void ejectCoin();
    //转动曲柄
    public void turnCrank();
    //派发糖果
    public void dispense();
}

SoldOutStateHasCoinStateNoCoinStateSoldStateWinnerState 各状态实现State接口

public class HasCoinState implements State {
    //Random randomWinner = new Random(System.currentTimeMillis());
    private final CandyMachine candyMachine;

    public HasCoinState(CandyMachine candyMachine) {
        this.candyMachine = candyMachine;
    }

    @Override
    public void insertCoin() {
        System.out.println("您已经投币,无法继续投币,请转动曲柄");
    }

    @Override
    public void ejectCoin() {
        System.out.println("退回硬币");
        candyMachine.setState(candyMachine.noCoinState);
    }

    @Override
    public void turnCrank() {
        System.out.println("正在发放糖果");
        // 加入WinnerState需要改变HasCoinState.turnCrank的逻辑。这里暴露了状态模式的一个缺点
        int winner = randomWinner.nextInt(10);
        if (winner == 0 && candyMachine.getCount() > 1)
            candyMachine.setState(candyMachine.winnerState);
        else
            // 没有WinnerState时,只需切换soldState
            candyMachine.setState(candyMachine.soldState);
    }

    @Override
    public void dispense() {
        System.out.println("没有糖果可派发");
    }
}

重构CandyMachine。并加入新的状态,中奖状态。

public class CandyMachine {

    final State soldOutState; // 售馨状态
    final State noCoinState; // 没硬币状态
    final State hasCoinState; // 已投币状态
    final State soldState;  // 售出状态
    final State winnerState;  // 中奖状态

    private State state = null;
    private int count = 0;

    public CandyMachine(int count) {
        soldOutState = new SoldOutState(this);
        noCoinState = new NoCoinState(this);
        hasCoinState = new HasCoinState(this);
        soldState = new SoldState(this);
        winnerState = new WinnerState(this);
        this.count = count;
        if (count > 0) {
            state = noCoinState;
        }
    }

    /**
     * 投入硬币
     */
    public void insertCoin() {
        state.insertCoin();
    }

    /**
     * 退回硬币
     */
    public void ejectCoin() {
        state.ejectCoin();
    }

    /**
     * 转动曲柄
     */
    public void turnCrank() {
        state.turnCrank();
        state.dispense();
    }

    /**
     * 派发糖果
     */
    void releaseCandy() {
        if (count != 0) {
            count = count - 1;
            System.out.println("您得到了一个糖果");
        }
    }

    void setState(State state) {
        this.state = state;
    }

    public int getCount() {
        return count;
    }

    public State getState() {
        return state;
    }

    public void printState() {
        System.out.println(state.toString());
    }
}

完成重构后,糖果机即使要加入一个新的状态,只需实例状态类。剩下的逻辑都委托给了状态类。

再理解状态模式定义的涵义

通过上面的例子在来理解状态类的定义。

"允许对象在内部状态发生改变时改变它的行为",因为这个模式将状态封装成为独立的类,并将动作委托到代表当前的对象,我们知道行为会随着内部状态而变化。糖果机的例子中,当糖果机在NoCoinState或HasCoinState两种不同的状态是,当投入硬币,就会得到不同的行为(机器接受硬币和机器拒绝硬币)。

"对象看起来好像修改了它的类",从客户的视角来看;如果说你使用的对象能够完全改变它的行为,那么就会觉得,这个对象实际上是从别的类实例化而来的。然而,实际上,我们是在使用组合通过简单引用不同的“状态对象”来造成类改变的假象。

为什么需要WinnerState,为什么不直接在SoldState中发放两颗糖果?

两个状态几乎一样,唯一的区别在于,WinnerState会派发两颗糖果。将派发两颗糖果的代码放到SoldState中,当然是可以的。这么做也会有缺点,因为这样等于是将两个状态用一个状态类来代表。这样做是牺牲了状态类的清晰易懂来减少一些冗余代码。但也违背了一个类,一个责任的原则。将WinnerState的责任放进SoldState中,等于是让SoldState具有两个责任。那么促销方案结束之后或者赢家的机率改变之后,又该怎么办呢?

参考

【游戏设计模式】之三 状态模式、有限状态机

浅谈状态模式和状态机 - 掘金

状态模式 | 菜鸟教程

策略模式 VS 状态模式