设计模式——10、状态模式
  • 作者:ZJWave
  • 分类: 设计模式
  • 发表:2019-06-26 09:53
  • 围观:5722
  • 评论:0

基本常识:策略模式和状态模式是双胞胎,在出生时才分开。你已经知道了,策略模式是围绕可以互换的算法来创建成功业务的。然而,状态走的是更崇高的路,它通过改变对象内部的状态来帮助对象控制自己的行为。

1.状态机101

糖果机已经进入了高科技时代。糖果机的主要制造厂商发现,只要把CPU放进机器中,就可以增加销售量、通过网络监测库存,并且能进准地得知客户的满意度。

但是这些制造商都是糖果机的专家,并非软件专家,他们需要你的帮助:

Anne:这张图像是一张状态图。

Joe:没错,每个圆圈都是一个状态.

Anne:……而每个箭头都是状态的转换。

Frank:慢点,你们两个。我已经好久没有接触状态图,忘得一干二净了!你们能提醒我状态图是干什么的吗?

Anne:当然可以了。你看到的圆圈,就是状态。“没有25分钱”大概就是糖果机的开始状态,等着你把钱放进来。每一个状态都代表机器不同的配置以某种方式行动,需要某些动作将目前的状态转换到另一个状态。

Joe:没错。看,要进入另一个状态,必须做某些事情,例如将25分钱的硬币放进机器中。所以你看到有一个箭头从“没有25分钱”指向“有25分钱”。

Frank:是的……

Joe:这就表示如果糖果机在“没有25分钱”的状态下,放进25分钱的硬币,就会进入“有25分钱”的状态。这就是状态的转换。

Frank:噢!我懂了!如果我是在“有25分钱”的状态,就可以转动曲柄改变到“售出糖果”状态,或者硬币回到“没有25分钱”状态。

Anne:就是这样!

Frank:这个状态图看起来并不太难。很明显我们有四个状态,分别为:“投入25分钱”、“退回25分钱”、“转动曲柄”和“发放糖果”。但是……当我们发放是时候要在“售出糖果”的状态中测试,是否糖果数目已经为零,来决定是否要进入到“糖果售罄”状态,或是进入“没有25分钱”状态。所以实际上,我们有五个状态转换。

Anne:测试糖果数目是否为零,也意味着我们必须持续地追踪糖果的数目。任何时候只要机器给出一颗糖果,都有可能是最后一颗糖果,如果是的haul,我们就需要转换到“糖果售罄”状态。

Joe:也请不要忘了可以做没有意义的是,例如,当糖果机在“没有25分钱”状态的时候,试着去退回25分钱,或者是在糖果机内同时放进两个25分钱。

Frank:噢!这我倒没想到我们也要注意到这部分。

Joe:对于任何一个可能的动作,我们都要检查,看看我们所处的状态和动作是否合适。这没问题!让我们开始将状态图映射成代码……

状态机101

我们如何从状态图得到真正的代码呢?下面是一个实现状态机(state machine)的简单介绍。

首先,找出所有的状态:

 接下来,创建一个实例变量来持有目前的状态,然后定义每个状态的值:

//将“糖果售罄”简称为“售罄”(Sold out)
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;

③ 现在,我们将所有系统中可以发生的动作整合起来:

④ 现在,我们创建了一个类,它的作用就像是一个状态机。对每一个动作,我们都创建了一个对应的方法,这些方法利用条件语句来决定在每个状态内什么行为是恰当的。比如对“投入25分钱”这个动作来说,我们可以把对应方法写成下面的样子:

public void insertQuarter(){
    //每一个可能的状态都需要用条件语句检查
    //然后对每一个可能的状态展现适当的行为
    if(state == HAS_QUARTER){
        System.out.println("You can't insert another quarter");
    }else if (state == SOLD_OUT){
        System.out.println("You can't insert a quarter, the machine is sold out");
    }else if(state == SOLD){
        System.out.println("Please wait, we're already giving you a gumball");
    }else if(state == NO_QUARTER){
        //但是也可以转换到另一个状态
        //像状态图描绘的那样
        state = HAS_QUARTER;
        System.out.println("You inserted a quarter");
    }
}

我们在这里所谈论的是一个通用的技巧:如何对对象内的状态建模——通过创建一个实例变量来持有状态值,并在方法内书写条件代码来处理不同状态。

写下代码

现在我们来实现糖果机。我们知道要利用实例变量持有当前的状态,然后需要处理所有可能发生的动作、行为和状态的转换。我们需要实现的动作包括:投入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;

    /**
     * 构造器需要初始糖果库存量当做参数。
     * 如果库存量不为零的haul,机器就会进入“没有25分钱”的状态,
     * 也就是说它等着别人投入25分钱。如果糖果数目为0的话,
     * 机器就会保持在“糖果售罄”的状态
     *
     * @param count
     */
    public GumballMachine(int count) {
        this.count = count;
        if (count > 0) {
            state = NO_QUARTER;
        }
    }

    /**
     * 现在我们开始将这些动作实现成方法
     * 当有25分钱透进来,就会执行这里……
     */
    public void insertQuarter() {
        if (state == HAS_QUARTER) {
            //如果已经投入过25分钱,我们就告诉顾客
            System.out.println("You can't insert another quarter");
        } else if (state == SOLD_OUT) {
            //如果糖果已经售罄,我们就拒绝收钱
            System.out.println("You can't insert a quarter, the machine is sold out");
        } else if (state == SOLD) {
            //如果顾客刚刚才买了糖果,就需要稍等一下
            //好让状态转换完毕,恢复到“没有25分钱”的状态
            System.out.println("Please wait, we're already giving you a gumball");
        } else if (state == NO_QUARTER) {
            //如果是在“没有25分钱”的状态下,我们就接受25分钱
            //并将状态转换到“有25分钱”的状态
            state = HAS_QUARTER;
            System.out.println("You inserted a quarter");
        }
    }

    /**
     * 顾客试着转动曲柄……
     */
    public void turnCrank() {
        if (state == SOLD) {
            //别想骗过机器拿两次糖果
            System.out.println("Turning twice doesn't get you another gumball!");
        } else if (state == NO_QUARTER) {
            //我们需要先投入25分钱。
            System.out.println("You turned but there's no quarter");
        } else if (state == SOLD_OUT) {
            //我们不能给糖果————已经没有任何糖果了
            System.out.println("You turned, but there are no gumballs");
        } else if (state == HAS_QUARTER) {
            System.out.println("You turned...");
            state = SOLD;
            dispense();
        }
    }

    /**
     * 调用此方法,发放糖果
     */
    public void dispense() {
        if (state == SOLD) {
            //我们正在“售出糖果”状态,给他们糖果
            System.out.println("A gumball comes rolling out the slot");
            count = count - 1;
            if (count == 0) {
                //我在这里处理“糖果售罄”的情况。如果这是最后一颗糖果,
                //就将机器的状态设置到“糖果售罄”
                //否则,就回到“没有25分钱”状态
                System.out.println("Oops, out of gumballs");
                state = SOLD_OUT;
            } else {
                state = NO_QUARTER;
            }
        } else if (state == NO_QUARTER) {
            //这些都不应该发生,但如果顾客这么做了,
            //他们得到的是错误消息,而不是得到糖果。
            System.out.println("You need to pay first");
        } else if (state == SOLD_OUT) {
            System.out.println("No gumball dispensed");
        } else if (state == HAS_QUARTER) {
            System.out.println("No gumball dispensed");
        }
    }

    /**
     * 退钱方法
     */
    public void ejectQuarter() {
        if (state == HAS_QUARTER) {
            state = NO_QUARTER;
            System.out.println("Quarter returned");
        } else {
            System.out.println("You haven't inserted a quarter");
        }
    }

    @Override
    public String toString() {
        String ret = "Mighty Gumball, Inc.\n";
        ret += "Java-enabled Standing Gumball Model #2004\n";
        ret += "Inventory: " + count + " gumballs\n";
        if(state == NO_QUARTER){
            ret += "Machine is waiting for quarter";
        }else if(state == SOLD_OUT){
            ret += "Machine is sold out";
        }
        ret += "\n";
        return ret;
    }
}

内部测试

感觉它像是使用思虑周密的方法学构造的牢不可破的设计,你不觉得吗?在我们将它交给万能糖果公司,安装到实际的糖果机器内之前,让我们先做一个小小的内部测试。测试程序是这样的:

public class GumballMachineTestDrive {
    public static void main(String[] args) {
        //总共装了5颗糖果
        GumballMachine gumballMachine = new GumballMachine(5);

        //打印出机器的状态
        System.out.println(gumballMachine);

        //投入一枚25分钱硬币
        gumballMachine.insertQuarter();
        //转动曲柄:我们应该拿到糖果
        gumballMachine.turnCrank();

        //再一次打印出机器的状态
        System.out.println(gumballMachine);
        //投入一枚25分钱硬币
        gumballMachine.insertQuarter();
        //要求机器退钱
        gumballMachine.ejectQuarter();
        //转动曲柄,我们应该拿不到糖果
        gumballMachine.turnCrank();

        //再一次打印出机器的状态
        System.out.println(gumballMachine);

        //投入一枚25分钱硬币
        gumballMachine.insertQuarter();
        //转动曲柄,我们应该拿到糖果。
        gumballMachine.turnCrank();
        //投入一枚25分钱硬币
        gumballMachine.insertQuarter();
        //转动曲柄,我们应该拿到糖果。
        gumballMachine.turnCrank();
        //要求机器退钱
        gumballMachine.ejectQuarter();

        //再一次打印出机器的状态
        System.out.println(gumballMachine);

        //放进两枚25分钱硬币
        gumballMachine.insertQuarter();
        gumballMachine.insertQuarter();
        //转动曲柄,我们应该拿到糖果
        gumballMachine.turnCrank();
        //现在做压力测试
        gumballMachine.insertQuarter();
        gumballMachine.turnCrank();
        gumballMachine.insertQuarter();
        gumballMachine.turnCrank();

        //再一次打印出机器的状态
        System.out.println(gumballMachine);
    }
}

2.设计新的状态机

万能糖果公司已经将你的代码放进他们的新机器中,然后让他们的质保专家进行测试。就目前为止,在他们看来一切都很顺利。

事实上,实在是太顺利了,所以他们想要变点花样……

我们认为,将“购买糖果”变成是一个游戏,可以大大地增加我们的销售量。我们要在没一台机器上面贴上这张贴纸。我们很高兴当初决定采用Java,因为这会让一切变得很简单,对吧?

混乱的状态……

使用一种考虑周详的方法学写糖果机的代码,并不意味着这份代码就容易扩展。事实上,当你回顾这些代码,并开始考虑要如何修改它时……

final static int SOLD_OUT = 0;
final static int NO_QUARTER = 1;
final static int HAS_QUARTER = 2;
final static int SOLD = 3;
//首先,你必须加上一个新的状态,称为“赢家”。这还不算太麻烦……

/**
 * 然后呢,你必须在每个方法中加入一个新的条件判断来处理
 * “赢家”状态,这可有你忙的了。
 */
public void insertQuarter() {
    //这里加入投币代码
}

public void ejectQuarter() {
    //这里加入退币代码
}

/**
 * turnCrack()尤其会变得一团乱,因为你必须加上代码
 * 来检查目前的顾客是否是赢家,然后再决定是切换到赢家状态
 * 还是售出糖果状态。
 */
public void turnCrank() {
    //这里加入转动曲柄的代码
}

public void dispense() {
    //这里加入发放糖果代码
}

Anne:这样子不妙。我认为我们的第一个版本很不错,但是随着万能糖果公司所要求的的新行为的出现,这个版本已经不再使用了。程序中BUG的几率增大可能会给我们带来麻烦,更不用说这回让CEO把我们逼疯。

Joe:你说的没错!我们需要重构这份代码,以便我们能容易地维护和修改它。

Anne:我们应该试着局部化每个状态的行为,这样一来,如果我们针对某个状态做了改变,就不会把其他的代码给搞乱了。

Joe:没错,换句话说,遵守“封装变化”原则。

Anne:正是如此。

Joe:如果我们将每个状态的行为都放在各自的类中,那么每个状态只要实现它自己的动作就可以了。

Anne:对。或许糖果机只需要委托给代表当前状态的状态对象。

Joe:哇!你真行:这不正是“多用组合,少用继承”吗?我们应用了更多的原则。

Anne:呵呵!我并没有百分百确定就要这么做,但是我想我们已经有正确的方向了。

Joe:我正在想着是否可以使添加新状态更容易呢?

Anne:我认为可以……我们还是需要改变代码,但是改变将局限在小范围内。因为加入一个新的状态,就意味着我们要加入一个新的状态,就意味着我们要加入一个新的类还有可能要改变一些转换。

Joe:听起来不错。让我们动手进行新的设计吧!

新的设计

我们的计划是这样的:不要维护我们现有的代码,我们重写它以便于将状态对象封装在各自的类中,然后在动作发生时委托给当前状态。

我们在这里遵照我们的设计原则,所以最后应该得到一个容易维护的设计。我们要做的事情是:

  1. 首先,我们定义一个State接口。在这个接口内,糖果机的每个动作都有一个对应的方法。
  2. 然后为机器中的每个状态实现状态类。这些类将负责在对应的状态下进行机器的行为。
  3. 最后,我们要摆脱旧的条件代码,取而代之的方式是,将动作委托到状态类。

你将会看到,我们不仅遵守了设计原则,实际上我们还实现了状态模式。在重新完成代码之后我们再来了解状态模式的正式定义……

现在我们要把一个状态的所有行为放在一个类中。这么一来我们将行为局部化了,并使得事情更容易改变和理解。

定义状态接口和类

首先,让我们创建一个State接口,所有的状态都必须实现这个接口:

实现我们的状态类

现在是实现一个状态的时候了:我们知道我们要的行为是什么,我们只需要把它变成代码。我们打算完全遵守所写下的状态机代码,但是这一次是分散在不同的类中。

让我们从NoQuarterState开始:

public class NoQuarterState implements State {

    GumballMachine gumballMachine;

    /**
     * 我们通过构造器得到糖果机的引用,
     * 然后将它记录在实例变量中
     * @param gumballMachine
     */
    public NoQuarterState(GumballMachine gumballMachine) {
        this.gumballMachine = gumballMachine;
    }

    @Override
    public void insertQuarter() {
        //如果有人投入了25分钱,我们就打印出一条消息,
        //说我们接受了25分钱,然后改变机器的状态到HasQuarterState
        System.out.println("You inserted a quarter");
        gumballMachine.setState(gumballMachine.getHasQuarterState());

    }

    @Override
    public void ejectQuarter() {
        System.out.println("You have'nt inserted a quarter");
    }

    @Override
    public void turnCrank() {
        System.out.println("You turned, but there's no quarter");
    }

    @Override
    public void dispense() {
        System.out.println("You need to pay first");
    }
}

重新改造糖果机

在完成这些状态之前,我们要重新改造糖果机——好让你了解这一切的原理。我们从状态相关的实例变量开始动手,然后把原来使用整数代表的状态改为状态对象:

public class GumballMachine {

    State soldOutState;
    State noQuarterState;
    State hasQuarterState;
    State soldState;

    State state;

    int count = 0;

    public GumballMachine(int numberGumballs) {
        soldOutState = new SoldOutState(this);
        noQuarterState = new NoQuarterState(this);
        hasQuarterState = new HasQuarterState(this);
        soldState = new SoldState(this);
        state = soldOutState;
        this.count = numberGumballs;
        if(numberGumballs > 0){
            state = noQuarterState;
        }
    }

    /**
     * 现在这些动作变得很容易实现了。我们只是委托到当前状态
     */
    public void insertQuarter(){
        state.insertQuarter();
    }

    public void ejectQuarter(){
        state.ejectQuarter();
    }

    /**
     * 请注意,我们不需要在GumballMachine中准备一个dispense()的动作方法,
     * 因为这只是一个内部的动作,用户不可以直接要求机器发放糖果。
     * 但我们是在状态对象的turnCrank()方法中调用dispense()方法的
     */
    public void turnCrank(){
        state.turnCrank();
        state.dispense();
    }

    /**
     * 这个方法允许其他的对象(像我们的状态对象)将机器的状态转换到不同的状态
     * @param state
     */
    public void setState(State state) {
        this.state = state;
    }

    /**
     * 这个机器提供了一个releaseBall()的辅助方法来释放出糖果,
     * 并将count实例变量的值减1
     */
    public void releaseBall(){
        System.out.println("A gumball comes rolling out the slot...");
        if(count != 0){
            count = count - 1;
        }
    }

    public State getSoldOutState() {
        return soldOutState;
    }

    public State getNoQuarterState() {
        return noQuarterState;
    }

    public State getHasQuarterState() {
        return hasQuarterState;
    }

    public State getSoldState() {
        return soldState;
    }

    public State getState() {
        return state;
    }

    public int getCount() {
        return count;
    }
}

实现更多的状态

现在你应该开始对糖果机和状态之间是如何配合的有点儿感觉了。让我们实现HasQuarterState(有25分钱)和SoldState(售出糖果)类……

public class HasQuarterState implements State {

    GumballMachine gumballMachine;

    public HasQuarterState(GumballMachine gumballMachine) {
        this.gumballMachine = gumballMachine;
    }


    @Override
    public void insertQuarter() {
        //这是一个对此状态不恰当的动作
        System.out.println("You can't insert another quarter");
    }

    /**
     * 退出顾客的25分钱,并将状态转换到NoQuarterState状态
     */
    @Override
    public void ejectQuarter() {
        System.out.println("Quarter returned");
        gumballMachine.setState(gumballMachine.getNoQuarterState());
    }

    /**
     * 当曲柄被转动时,我们就调用它的setState()方法,
     * 并传入SoldState对象作为参数,将机器的状态转换到SoldState状态
     */
    @Override
    public void turnCrank() {
        System.out.println("You turned...");
        gumballMachine.setState(gumballMachine.getSoldState());
    }

    /**
     * 这是此状态的另一个不恰当的动作
     */
    @Override
    public void dispense() {
        System.out.println("No gumball dispensed");
    }
}

让我们来看看SoldState类:

public class SoldState implements State {
    GumballMachine gumballMachine;

    public SoldState(GumballMachine gumballMachine) {
        this.gumballMachine = gumballMachine;
    }

    /**
     * 对此状态来说,这些都是不恰当的动作
     */
    @Override
    public void insertQuarter() {
        System.out.println("Please wait, we're already giving you a gumball");
    }

    @Override
    public void ejectQuarter() {
        System.out.println("Sorry, you already turned the crank");
    }

    @Override
    public void turnCrank() {
        System.out.println("Turning twice doesn't get you another gumball!");
    }

    @Override
    public void dispense() {
        gumballMachine.releaseBall();
        //我们现在是在SoldState状态,也就是说客户已经付钱了。
        //所以我们首先需要机器发放糖果
        if (gumballMachine.getCount() == 0) {
            //我们问机器糖果的剩余数目是多少,然后将状态转换到NoQuarterState或者SoldOutState
            System.out.println("Oops, out of gumballs");
            gumballMachine.setState(gumballMachine.getSoldOutState());
        } else {
            gumballMachine.setState(gumballMachine.getNoQuarterState());
        }
    }
}

检查一下,到目前为止我们已经做了哪些事情……

你现在有了一个糖果机的实现,它在结构上和前一版本差异颇大,但是功能上确实一样的。通过从结构上改变实现,你已经做到了以下几点:

  • 将每个状态的行为局部化到它自己的类中。
  • 将容易产生问题的if语句删除,以方便日后的维护。
  • 让每一个状态“对修改关闭”,让糖果机“对扩展开放”,因为可以加入新的状态类(我们马上就这么做)。
  • 创建一个新的代码基和类结构,这更能映射万能糖果公司的图,而且更容易阅读和理解。

现在,再多检查一些我们所做的功能面:

3.定义状态模式

是的,这是真的,我们刚刚实现了状态模式!现在,让我们来看看它是怎么一回事:

状态模式允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。

这个描述中的第一部分附有相当多的涵义,是吧?因为这个模式将状态封装称为独立的类,并将动作委托到代表当前状态的对象,我们知道行为会随着内部状态而改变。糖果机提供了一个很好的例子:当糖果机是在NoQuarterState或HasQuarterState两种不同的状态时,你投入25分钱,就会得到不同的行为(机器接受25分钱和机器拒绝25分钱)。

而这个定义中的第二部分呢?一个对象“看起来好像修改了它的类”是什么意思呢?从客户的视角来看:如果说你使用的对象能够完全改变它的行为,那么你会觉得,这个对象实际上是从别的类实例化而来的。然而,实际上,你知道我们是在使用组合通过简单引用不同的状态对象来造成类改变的假象。

好了,现在就让我们检查状态模式的类图:

等一下,在我的记忆中,策略模式和这张类图根本就是一模一样。

好眼力!是的,类图是一样的,但是这两个模式的差别在于它们的“意图”。

以状态模式而言,我们将一群行为封装在状态对象中,context的行为随时可委托到那些状态对象中的一个。随着时间的流逝,当前状态在状态对象集合中游走改变,以反映出context内部的状态,因此,context的行为也会跟着改变。但是context的客户对于状态对象了解不多,甚至根本是浑然不觉。

而已策略模式而言,客户通常主动指定Context所要组合的策略对象是哪一个。现在,固然策略模式让我们具有弹性,能够在运行时改变策略,但对于某个context对象来说,通常都只有一个最适当的策略对象。比方说,有些鸭子(如绿头鸭)被设置成利用典型的飞翔行为进行飞翔,而有些鸭子(例如橡皮鸭和诱饵鸭)使用的飞翔行为只能让他们紧贴地面。

一般来说,我们把策略模式想成是除了继承之外的一种弹性替代方案。如果你使用继承定义了一个类的行为,你将被这个行为困住,甚至要修改它都很难。有了策略模式,你可以通过组合不同的对象来改变行为。

我们把状态模式想成是不用在context中放置许多条件判断的替代方案。通过将行为包装进状态对象中,你可以通过在context内简单地改变状态对象来改变context的行为。

Q&A

在GumballMachine中,状态决定了下一个状态应该是什么。ConcreteState总是决定接下来的状态是什么吗?

答:不,并非总是如此,Context也可以决定状态转换的流向。

一般来讲,当状态转换是固定的时候,就适合放在Context中;然而,当状态装换是更动态的时候,通常就会放在状态类中(例如,在GumballMachine中,由运行时糖果的数目来决定状态要转换到NoQuarter还是SoldOut)。将状态转换放在状态类中的缺点是:状态类之间产生了依赖。在我们的GumballMachine视线中,我们试图通过使用Context上的getter方法把依赖减到最小,而不是显式硬编码具体状态类。

请注意,在做这个决策的同事,也等于是在为另一件事情做决策:当系统进化时,究竟哪个类是对修改封闭(Context还是状态类)的。

问:客户会直接和状态交互吗?

答:不会。状态是用在Context中来代表它的内部状态以及行为的,所以只有Context才会对状态提出请求。客户不会直接改变Context的状态。全盘了解状态是Context的工作,客户根本不了解,所以不会直接和状态联系。

问:如果在我的程序中Context有许多实例,这些实例之间可以共享状态对象吗?

答:是的,绝对可以,事实上这是很常见的做法。但唯一的前提是,你的状态对象不能持有它们自己的内部状态,否则就不能共享。

想要共享状态,你需要把每个状态都指定到静态的实例变量中。如果你的状态需要利用到Context中的方法或者实例变量,你还必须在每个handler()方法内传入一个context的引用。

问:使用状态模式似乎总是增加我们设计中类的数目。请看GumballMachine的例子,新版本比旧版本多出了许多类!

答:没错,在个别的状态类中封装状态行为,结果总是增加这个设计中类的数目。这就是为了要获取弹性而付出的代价。除非你的代码是一次性的,可以用完就扔掉(是呀!才怪!),那么其实状态模式的设计是绝对值得的。其实真正重要的是你暴露给客户的类数目,而且我们有办法将这些额外的状态类全都隐藏起来。

让我们看一下另一种做法:如果你有一个应用,它有很多状态,但是你决定不将这些状态封装在不同的赌侠凝重,那么你就会得到巨大的、整块的条件语句。这会让你的代码不容易维护和理解。通过使用许多对象,你可以让状态变得很干净,在以后理解和维护它们时,就可以省下很多的工夫。

问:状态模式类图显示State是一个抽象类,但是你不是使用接口实现糖果机状态的吗?

答:是的。如果我们没有共同的功能可以放进抽象类中,就会使用接口。在你实现状态模式时,很可能想使用抽象类。这么一来,当你以后需要在抽象类中加入新的方法时就很容易,不需要打破具体状态的实现。

4.在状态机中加入新的状态

别忘了,我们还没有完事呢。我们还有一个游戏在等待实现。然而,我们已经实现了状态模式,所以实现这个游戏应该易如反掌。首先,我们要在GumballMachine类中加入一个状态:

public class GumballMachine {

    State soldOutState;
    State noQuarterState;
    State hasQuarterState;
    State soldState;
    //你需要在这里加进一个新的WinnerState状态,然后在构造器中将它初始化。
    State winnerState;

    State state;

    int count = 0;
    
    //这里有一些方法
}

现在让我们实现WinnerState类本身,其实它很想SoldState类: 

public class WinnerState implements State {

    GumballMachine gumballMachine;

    public WinnerState(GumballMachine gumballMachine) {
        this.gumballMachine = gumballMachine;
    }

    /**
     * 和SoldState一样
     * 对此状态来说,这些都是不恰当的动作
     */
    @Override
    public void insertQuarter() {
        System.out.println("Please wait, we're already giving you a gumball");
    }

    @Override
    public void ejectQuarter() {
        System.out.println("Sorry, you already turned the crank");
    }

    @Override
    public void turnCrank() {
        System.out.println("Turning twice doesn't get you another gumball!");
    }

    @Override
    public void dispense() {
        System.out.println("YOU'RE A WINNER! You get two gumballs for your quarter");
        gumballMachine.releaseBall();
        if(gumballMachine.getCount() == 0){
            gumballMachine.setState(gumballMachine.getSoldOutState());
        }else {
            //如果还有第二课糖果的话,我们就把它释放出来
            gumballMachine.releaseBall();
            if(gumballMachine.getCount() > 0){
                gumballMachine.setState(gumballMachine.getNoQuarterState());
            }else {
                System.out.println("Oops, out of gumballs!");
                gumballMachine.setState(gumballMachine.getSoldOutState());
            }
        }
    }
}

我们还要再做一个改变:我们需要实现机会随机数,还要增加一个进入WinnerState状态的转换。这两件事情都要加进HasQuaterState,因为顾客会从这个状态中转动曲柄:

public class HasQuarterState implements State {

    //首先我们增加一个随机数产生器,产生10%赢的机会……
    Random randomWinner = new Random(System.currentTimeMillis());
    GumballMachine gumballMachine;

    public HasQuarterState(GumballMachine gumballMachine) {
        this.gumballMachine = gumballMachine;
    }

    @Override
    public void insertQuarter() {
        System.out.println("You can't insert another quarter");
    }

    @Override
    public void ejectQuarter() {
        System.out.println("Quarter returned");
        gumballMachine.setState(gumballMachine.getNoQuarterState());
    }


    @Override
    public void turnCrank() {
        System.out.println("You turned...");
        int winner = randomWinner.nextInt(10);
        if((winner == 0) && (gumballMachine.getCount() > 1)){
            //如果赢了,而且又足够的糖果可以让他一次得到两颗的话,我们就进入WinnerState状态
            gumballMachine.setState(gumballMachine.getWinnerState());
        }else {
            //否则,就进入SoldState状态(就跟平常一样)
            gumballMachine.setState(gumballMachine.getSoldState());
        }
    }

    @Override
    public void dispense() {
        System.out.println("No gumball dispensed");
    }
}

哇!实现起来真是容易!我们刚刚为GumballMachine增加了一个新的状态,并实现了这个新的状态。要做的事情只是实现我们的机会游戏,并转换到正确的状态。看来我们新的代码策略已经奏效了……

向万能糖果公司的CEO作展示

万能糖果公司的CEO来访,来看看我们的新糖果机代码的演示。希望这些状态都没问题!我们要让这个展示简短而甜蜜,但希望时间能够足够长,至少让我们赢一次!

/**
 * 这个代码其实没有改,我们只是把它缩短了一些。
 */
public class GumballMachineTestDrive {
    public static void main(String[] args) {
        //再来一次,让糖果机一开始就装了5颗糖果
        GumballMachine gumballMachine = new GumballMachine(5);

        System.out.println(gumballMachine);

        //我们希望能赢,所以一直投钱并转动曲柄。
        //然后打印出糖果机的状态……
        gumballMachine.insertQuarter();
        gumballMachine.turnCrank();

        System.out.println(gumballMachine);

        gumballMachine.insertQuarter();
        gumballMachine.turnCrank();

        gumballMachine.insertQuarter();
        gumballMachine.turnCrank();

        System.out.println(gumballMachine);
    }
}

Q&A

问:我们为什么需要WinnerState?为什么不直接在SoldState中发放两颗糖果?

答:这是一个好问题。这两个状态几乎一样,唯一的差别在于,WinnerState状态会发放两颗糖果。你当然可以将发放两颗糖果的代码放在SoldState中,当然这么做有缺点,因为你等于是将两个状态用一个状态类来代表。这样做你牺牲了状态类的清晰易懂来减少一些冗余代码。你也应该考虑到面向对象的原则:一个类,一个责任。将WinnerState状态的责任放进SoldState状态中,你等于是让SoldState状态具有两个责任。那么促销方案结束之后或者赢家的几率改变之后,你又该怎么办呢?所以,这必须用你的智慧来做折衷。

在推出我们的正式版本之前,让我们再检查看看GumballMachine还有哪些方面需要改进:

  • 我们在售出糖果和赢家状态中,有许多重复的代码。我们必须把这部分清理一下。要怎么做呢?我们可以把State设计成抽象类,然后把方法的默认行为放在其中;毕竟,像是“你已经投入25分钱”这类的消息,不会被顾客看见。所以,所有的“错误响应”行为都可以写得更具通用性,并放在抽象的State类中供子类继承。
  • dispense()方法即使是在没有25分钱时曲柄被转动的情况下也总是会被调用。我们可以轻易地修改这部分,做法是让turnCrank()返回一个布尔值,或者引入异常。你认为哪一种做法比较好?
  • 状态转换的所有智能被放在状态类中,这可能导致什么问题?我们要将逻辑移进糖果机中嘛?这有什么优缺点?
  • 你会实例化许多的GumballMachine对象吗?如果是的话,你可能想要将状态的实例移到静态的实例变量中年共享。这需要对GumballMachine和State做怎样的改变?

5.状态模式和策略模式的比较

策略模式:

  • 可以使类在不同的状态中展现不同的行为。
  • Context对象被创建之后,会随着时间而改变自己的状态,而任何状态改变都是定义好的。

状态模式:

  • 将可以互换的行为封装起来,允许对象能够通过组合和委托来拥有不同的行为或算法。
  • 使用委托的方法,决定使用哪一个行为,会控制对象使用哪一种策略。

6.总结

OO原则:

  • 封装变化
  • 多用组合,少用集成
  • 针对接口编程,不针对实现编程
  • 为交互对象之间的松耦合设计和努力
  • 类应该对扩展开放,对修改关闭
  • 依赖抽象,不要依赖具体类
  • 只和朋友交谈
  • 别找我,我会找你
  • 类应该只有一个改变的理由

OO模式:

状态模式——允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。

本文要点:

  • 状态模式允许一个对象基于内部状态而拥有不同的行为。
  • 和程序状态机(PSM)不同,状态模式用类代表状态。
  • Context会将行为委托给当前状态对象。
  • 通过将每个状态封装进一个类,我们把以后需要做的任何改变局部化了。
  • 状态模式和策略模式有相同的类图,但是它们的意图不同。
  • 策略模式通常会用行为或算法来配置Context类。
  • 状态模式允许Context随着状态的改变而改变行为。
  • 使用状态模式通常会导致设计中类的数目大量增加。
  • 状态类可以被多个Context实例共享。

转载请注明原文链接:ZJ-Wave

Top