您的位置:首页 > 其它

设计模式二 状态模式

2015-03-13 16:53 260 查看
之前我们从设计一个简单的RPG游戏应用来讲述了策略模式,现在记录策略模式的双胞胎弟弟状态模式

那什么是状态模式,我们已经知道了,策略模式是围绕着可以动态设置算法来设计的一套牛掰的系统。然而状态模式走的是逼格更高的道路。它通过改变对象内部的状态来帮组对象控制自己的行为,它常常催眠它的对象客户“跟着我走,不用多久,你就会升职加薪,当上总经理,出任CEO,迎娶白富美,走上人生巅峰。 ” 是不是想想还有些小激动呢?

开头为什么说状态模式是策略模式的双胞胎弟弟,这个问题先留在这里供大家思考一下~文章结尾会给出答案。

卧槽,一个没忍住。。。。赶紧入正题,先给出状态模式定义:允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类

凭空给出一个莫名奇妙的定义,太直接粗暴了。还是从一个实际场景来说



这货应该都见过,现在就来模拟一个饮料自动售卖机。大致的饮料售卖流程




我们从这幅简单的流程图能得到如下信息:

饮料自动售卖机共有4个状态:饮料售罄、有5块钱投入、等待5块钱投入(没有5块)、售出饮料。这四个状态;

从一个状态要进入另外一个状态,必须要做某些动作。例如:当前在“没有5块”状态,必须要投入5块,才能进入到“有5块”状态

对于任何一个可能的动作,都需要检查,看看我们所处的状态和动作是否合适。我们总不可能在饮料机“没有5块”的状态下,试着退回5块钱。

根据以上信息我们开始编写我们的代码。

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

[code]    //饮料售罄
    private static int SOLD_OUT = 0;
    //没有5块钱
    private static int NO_MONEY = 1;
    //有5块钱
    private static int HAS_MONEY = 2;
    //售出饮料
    private static int SOLD = 3;

    private int state = SOLD_OUT;//跟踪当前状态,初始化为饮料售罄
    private int count = 0;//记录机器内饮料的数目

    public BeverageMachine(int count) {
        this.count = count;
        //如果机器内有饮料,则切换为等待别如投入5块钱的状态,如果没有饮料,机器保持在"饮料售罄"状态
        if(count > 0) {
            state = NO_MONEY;
        }
    }


接着我们将系统中所有可能发生的动作整合起来:投入5块钱,退回5块钱,选择饮料,发放饮料。我们为这四个动作分别创建对应的方法,这些方法利用条件语句来决定每个状态内部什么行为是恰当的:

[code]/**
     * 投入5块
     */
    public void insertMoney(){
        if(state == HAS_MONEY){
            System.out.println("不能再次投入5块");
        }else if(state == NO_MONEY){
            state = HAS_MONEY;
            System.out.println("您投入了5块");
        }else if(state == SOLD_OUT){
            System.out.println("饮料售罄,不能投入5块");
        }else if(state == SOLD){
            System.out.println("您刚刚才买了一瓶饮料,请等待售卖机恢复为“没有5块”的状态再进行操作");
        }
    }

    /**
     * 退回5块
     */
    public void ejectMoney(){
        if(state == HAS_MONEY){
            state = NO_MONEY;
            System.out.println("返还5块");
        }else if(state == NO_MONEY){
            System.out.println("之前您并没有投入5块钱");
        }else if(state == SOLD){
            System.out.println("对不起,您已经选择了饮料,机器出货中,无法返还5块");
        }else if(state == SOLD_OUT){
            System.out.println("饮料已经售罄,不会接受5块,无法返回5块");
        }
    }

    /**
     * 选择饮料
     */
    public void choiseBeverage(){
        if(state == SOLD){
            System.out.println("选择两次饮料是无效的,别想骗过机器拿到两瓶饮料");
        }else if(state == NO_MONEY){
            System.out.println("机器需要您先投入5块钱");
        }else if(state == SOLD_OUT){
            System.out.println("对不起,饮料已经售罄...");
        }else if(state == HAS_MONEY){
            System.out.println("您选择了一瓶饮料");
            state = SOLD;
            dispense();
        }
    }

    /**
     * 发放饮料
     */
    private void dispense() {
        if(state == SOLD){
            System.out.println("发放一瓶饮料");
            count--;
            if(count == 0){
                System.out.println("饮料售罄");
                state = SOLD_OUT;
            }else{
                state = NO_MONEY;
            }
        }else if(state == NO_MONEY){//不可能发生
            System.out.println("error:机器需要您先投入5块钱");
        }else if(state == SOLD_OUT){//不可能发生
            System.out.println("error:对不起,饮料已经售罄...");
        }else if(state == HAS_MONEY){//不可能发生
            System.out.println("error:您得选择你要购买的饮料");
        }
    }


我们测试看看我们写的饮料售卖机好不好使:

[code]    public static void main(String[] args) {
        BeverageMachine bm = new BeverageMachine(10);
        bm.insertMoney();
        bm.choiseBeverage();

        System.out.println("---------------------------");

        bm.insertMoney();
        bm.ejectMoney();
        bm.choiseBeverage();

        System.out.println("---------------------------");

        bm.insertMoney();
        bm.choiseBeverage();
        bm.insertMoney();
        bm.choiseBeverage();
        bm.ejectMoney();

        System.out.println("---------------------------");

        bm.insertMoney();
        bm.insertMoney();
        bm.choiseBeverage();
        bm.insertMoney();
        bm.choiseBeverage();
        bm.insertMoney();
        bm.choiseBeverage();
    }


控制台打印输出:

[code]您投入了5块
您选择了一瓶饮料
发放一瓶饮料
---------------------------
您投入了5块
返还5块
机器需要您先投入5块钱
---------------------------
您投入了5块
您选择了一瓶饮料
发放一瓶饮料
您投入了5块
您选择了一瓶饮料
发放一瓶饮料
之前您并没有投入5块钱
---------------------------
您投入了5块
不能再次投入5块
您选择了一瓶饮料
发放一瓶饮料
您投入了5块
您选择了一瓶饮料
发放一瓶饮料
您投入了5块
您选择了一瓶饮料
发放一瓶饮料




看来一切正常,这简直是使用思维周密的方法学构造的牢不可破的设计。我们可以去solo两把了···但是我会告诉你这一堆乱七八糟的if-else就是状态模式么?答案显示是不可能。

好吧···新的需求下来了,公司主管觉得我们这些coder太牛掰了,决定给我们来点新花样:客户认为,将“购买饮料”变成一个游戏,可以大大地增加我们的销售量。我们需要加入一种机制。处理购买十次将会赢得再来一瓶的奖励的情况。

现在问题来了,我们该怎样在原有的代码上做变动来满足现有的需求呢?元芳你怎么看?

而事实是,我们使用一种考虑周详,思维缜密的方法学来设计饮料机的代码,并不意味着这份代码就容易扩展:

首先我们必须加上一个新的状态,称为”再来一瓶“

然后,必须在每个动作方法中加入一个新的条件判断来处理”再来一瓶“状态

而choiseBeverage()这个方法会变得惨不忍睹,因为我们必须加上特殊处理的代码来检查目前顾客到底有没有获得”再来一瓶“这个状态,然后再决定是切换到”再来一瓶“的状态还是”售出饮料“状态

未来加入的代码很有可能导致许多bug



有上面如此多的麻烦,看来得重新设计了。我们必须保证我们的系统能够兼备复用、可扩充、可维护三个特性:


定义一个State接口,接口里面,饮料售卖机的每个动作都有一个对应的方法

机器中每个状态都实现State接口,这些类将负责在对应的状态下进行机器的行为

将动作委托给状态类



UML图如下:



代码如下:

[code]/**
 *  定义状态接口,所有的状态类必须实现该接口,接口中的方法直接映射打饮料售卖机上可能发生的动作
 */
public interface State {

    public void insertMoney();

    public void ejectMoney();

    public void choiseBeverage();

    public void dispense();

}


[code]/**
 *  售出饮料状态
 */
public class SoldState implements State {

    private BeverageMachine beverageMachine;

    public SoldState(BeverageMachine beverageMachine) {
        this.beverageMachine = beverageMachine;
    }

    @Override
    public void insertMoney() {
        System.out.println("您刚刚才买了一瓶饮料,请等待售卖机恢复为“没有5块”的状态再进行操作");
    }

    @Override
    public void ejectMoney() {
        System.out.println("对不起,您已经选择了饮料,机器出货中,无法返还5块");
    }

    @Override
    public void choiseBeverage() {
        System.out.println("选择两次饮料是无效的,别想骗过机器拿到两瓶饮料");
    }

    @Override
    public void dispense() {
        beverageMachine.releaseBeverage();
        if(beverageMachine.getCount()  > 0) {
            beverageMachine.setState(beverageMachine.getNoMoneyState());
        } else {
            System.out.println("饮料售罄");
            beverageMachine.setState(beverageMachine.getSoldOutState());
        }
    }

}


[code]/**
 *  饮料售罄状态
 */
public class SoldOutState implements State {

    private BeverageMachine beverageMachine;

    public SoldOutState(BeverageMachine beverageMachine) {
        this.beverageMachine = beverageMachine;
    }

    @Override
    public void insertMoney() {
        System.out.println("饮料售罄,不能投入5块");
    }

    @Override
    public void ejectMoney() {
        System.out.println("饮料已经售罄,不会接受5块,无法返回5块");
    }

    @Override
    public void choiseBeverage() {
        System.out.println("对不起,饮料已经售罄...您不能选择饮料");
    }

    @Override
    public void dispense() {
        System.out.println("error:对不起,饮料已经售罄...");
    }

}


[code]/**
 *  等待投入5块钱(没有5块)状态
 */
public class NoMoneyState implements State {
    private BeverageMachine beverageMachine;//饮料售卖机引用

    public NoMoneyState(BeverageMachine beverageMachine) {
        this.beverageMachine = beverageMachine;
    }

    @Override
    public void insertMoney() {
        System.out.println("您投入了5块");
        beverageMachine.setState(beverageMachine.getHasMoneyState());
    }

    @Override
    public void ejectMoney() {
        System.out.println("之前您并没有投入5块钱");
    }

    @Override
    public void choiseBeverage() {
        System.out.println("机器需要您先投入5块钱");
    }

    @Override
    public void dispense() {
        System.out.println("error:机器需要您先投入5块钱");
    }

}


[code]/**
 *  有5块钱状态
 */
public class HasMoneyState implements State {

    private BeverageMachine beverageMachine;

    Random rand = new Random(System.currentTimeMillis());

    public HasMoneyState(BeverageMachine beverageMachine) {
        this.beverageMachine = beverageMachine;
    }

    @Override
    public void insertMoney() {
        System.out.println("不能再次投入5块");
    }

    @Override
    public void ejectMoney() {
        System.out.println("返还5块");
        beverageMachine.setState(beverageMachine.getNoMoneyState());
    }

    @Override
    public void choiseBeverage() {
        System.out.println("您选择了一瓶饮料");
        int probability = rand.nextInt(10);
        if(probability == 0 && beverageMachine.getCount() > 1){
            beverageMachine.setState(beverageMachine.getAnotherBottleState());
        }else{
            beverageMachine.setState(beverageMachine.getSoldState());
        }
    }

    @Override
    public void dispense() {
        System.out.println("error:您得选择你要购买的饮料");
    }

}


[code]/**
 *  再来一瓶状态
 */
public class AnotherBottleState implements State {

    private BeverageMachine beverageMachine;

    public AnotherBottleState(BeverageMachine beverageMachine) {
        this.beverageMachine = beverageMachine;
    }

    @Override
    public void insertMoney() {
        System.out.println("您刚刚才买了一瓶饮料,请等待售卖机恢复为“没有5块”的状态再进行操作");
    }

    @Override
    public void ejectMoney() {
        System.out.println("对不起,您已经选择了饮料,机器出货中,无法返还5块");
    }

    @Override
    public void choiseBeverage() {
        System.out.println("选择两次饮料是无效的,别想骗过机器拿到两瓶饮料");
    }

    @Override
    public void dispense() {
        System.out.println("恭喜您,获得了再来一瓶,你可以得到两瓶饮料");
        beverageMachine.releaseBeverage();
        if(beverageMachine.getCount() == 0){
            System.out.println("饮料售罄,很抱歉,无法发放第二瓶饮料,您可以通知管理员保证发放您第二瓶饮料");
            beverageMachine.setState(beverageMachine.getSoldOutState());
        }else{
            beverageMachine.releaseBeverage();
            if(beverageMachine.getCount() > 0){
                beverageMachine.setState(beverageMachine.getNoMoneyState());
            }else{
                System.out.println("饮料售罄");
                beverageMachine.setState(beverageMachine.getSoldOutState());
            }
        }
    }

}


以上我们实现了所有的状态对象接下来我们来改造下饮料售卖机,好让饮料售卖机使用这些状态对象:

[code]public class BeverageMachine {

    //饮料售卖机可能出现的所有状态
    private State soldOutState;
    private State noMoneyState;
    private State hasMoneyState;
    private State soldState;
    private State anotherBottleState;
    //跟踪当前状态对象,初始化为饮料售罄
    private State state = soldOutState;
    //记录机器内饮料的数目
    private int count = 0;

    //getter....setter.....
    public void setState(State state) {
        this.state = state;
    }

    public State getSoldOutState() {
        return soldOutState;
    }

    public State getNoMoneyState() {
        return noMoneyState;
    }

    public State getHasMoneyState() {
        return hasMoneyState;
    }

    public State getSoldState() {
        return soldState;
    }

    public State getAnotherBottleState() {
        return anotherBottleState;
    }

    public int getCount() {
        return count;
    }

    public BeverageMachine(int numberBeverages){
        soldOutState = new SoldOutState(this);
        noMoneyState = new NoMoneyState(this);
        hasMoneyState = new HasMoneyState(this);
        soldState = new SoldState(this);
        anotherBottleState = new AnotherBottleState(this);

        this.count = numberBeverages;
        if(count > 0) {
            state = noMoneyState;
        }
    }

    /**
     * 发放饮料
     */
    public void releaseBeverage(){
        System.out.println("发放一瓶饮料");
        if(count != 0){
            count--;
        }
    }

    /**
     * 重新填充饮料
     * @param count
     */
    public void refill(int count){
        this.count = count;
        state = noMoneyState;
    }

    /**
     * 投入5块钱的动作将委托给当前状态对象处理
     */
    public void insertMoney(){
        state.insertMoney();
    }

    /**
     * 退回5块钱的动作将委托给当前状态对象处理
     */
    public void ejectMoney(){
        state.ejectMoney();
    }

    /**
     * 选择饮料的动作将委托给当前状态对象处理
     */
    public void choiseBeverage(){
        state.choiseBeverage();
        state.dispense();
    }

}


我们测下一下

[code]    public static void main(String[] args) {
        BeverageMachine bm = new BeverageMachine(10);
        bm.insertMoney();
        bm.choiseBeverage();
        System.out.println("---------------------------");

        bm.insertMoney();
        bm.ejectMoney();
        bm.choiseBeverage();

        System.out.println("---------------------------");

        bm.insertMoney();
        bm.choiseBeverage();
        bm.insertMoney();
        bm.choiseBeverage();
        bm.ejectMoney();

        System.out.println("---------------------------");

        bm.insertMoney();
        bm.insertMoney();
        bm.choiseBeverage();
        bm.insertMoney();
        bm.choiseBeverage();
        bm.insertMoney();
        bm.choiseBeverage();
    }


控制台打印输出

[code]您投入了5块
您选择了一瓶饮料
发放一瓶饮料
---------------------------
您投入了5块
返还5块
机器需要您先投入5块钱
error:机器需要您先投入5块钱
---------------------------
您投入了5块
您选择了一瓶饮料
恭喜您,获得了再来一瓶,你可以得到两瓶饮料
发放一瓶饮料
发放一瓶饮料<
您投入了5块
您选择了一瓶饮料
发放一瓶饮料
之前您并没有投入5块钱
---------------------------
您投入了5块
不能再次投入5块
您选择了一瓶饮料
恭喜您,获得了再来一瓶,你可以得到两瓶饮料
发放一瓶饮料
发放一瓶饮料
您投入了5块
您选择了一瓶饮料
发放一瓶饮料
您投入了5块
您选择了一瓶饮料
发放一瓶饮料


当前的版本也是非常好使。不过现在的饮料机跟前一个版本差异颇大,但是功能上却是一样的。通过从结构上改变实现,目前的设计已经做到了以下几点:

将每个状态的行为或者动作封装到它自己的类中。

现在的设计当中已经没有长篇大论的if-else结构。日后维护更加轻松

让每一个状态”对修改关闭“,让饮料机”对扩展开放“,因为我们可以非常轻松的加入新的状态(只需要实现State接口即可,比如这里的”再来一瓶“状态)

以上就是状态模式Demo的实现,这里我们回顾本文开是提出的状态模式定义:允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类,现在来大致解释下。

1、允许对象在内部状态改变时改变它的行为:因为这个模式将状态封装成为独立类(SoldState、HasMoneyState……),并将动作(insertMoney()、ejectMoney()、choiseBeverage()、dispense())委托到代表当前状态的对象。所以当饮料售卖机是在NoMoneyState或者HasMoneyState两种不同的状态时,投入5块钱,就会得到不同的行为。

2、对象看起来好像修改了它的类:从客户的角度来审视这句话。如果说你使用的对象能够完全改变它的行为,那么你会觉得,这个对象实际上是从别的类实例化而来的,然而,我们知道我们是在使用组合通过简单的引用不同的状态对象来造成类改变的假象(当在”有5块钱“状态的时候,进行退回5块钱的动作那么此时beverageMachine的状态属性引用会被set为”没有5块“的状态——————-beverageMachine.setState(beverageMachine.getNoMoneyState()))。

下面是整体的UML设计:




这里强调一点,Demo中没有使用Machine父类,是因为现在的业务暂时用不到,如果后期需要扩展新的业务:制造一个糖果售卖机,或者TT售卖机,嘿嘿……你懂的~~。那么我们就需要使用到Machine父类。

现在我们来回答开始提出的一个问题:为什么说状态模式是策略模式的双胞胎弟弟?

大家回一下 设计模式一 前言与策略模式(strategy)篇尾的整体的类设计图,是不是一样的?这两个模式的差别仅仅在于他们的”意图“。

以状态模式而言:我们将一群行为封装在状态对象中,Machine的行为可以随时委托到那些状态对象中的任何一个。随着时间流逝,当前运行状态在那些状态对象中游走改变,以反映出Machine的内部状态。因此Machine的行为也会跟着改变。但是Machine的客户对于这些改变是浑然不知的。

以策略模式而言:客户通常指定角色所要组合的策略对象即角色所拥有的行为是哪一个。现在固然策略模式让我们具有弹性,能够在运行时改变策略。但是对于某一个角色来说,通常都只有一个最适合(符合现实场景)的策略对象。比方说,国王只使用单手剑,狼人一般都是使用自己的利爪不会使用武器。

一般来说,我们把策略模式想成是除了继承之外的一种弹性替代方案,如果使用了继承定义了一个类的行为,我们将被这个行为困死,而策略模式可以通过组合不同对象来改变行为;

我们把状态模式想象成不用在Machine中放置许多if-else 结构代码的替代方案。通过将行为包装进状态对象中,可以通过在Machine类简单改变状态对象来改变Machine行为

关于状态模式的记录到此为止。最后提出几个问题:

我们在”售出饮料“ 和 ”再来一瓶“状态中,有许多重复的代码,如何清理这部分代码?

dispense()方法即使在”没有5块钱“的状态中进行选择一瓶饮料的操作,仍然会被调用,该如何改进?

我们需要创建多个饮料售卖机,我们可能要将状态的实例转换成静态类变量进行共享,这需要对Machine和State做怎样的变动?
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: