bboyjing's blog

HeadFirst设计模式十二【状态模式】

书上对该模式的第一解释是:策略模式和状态模式是双胞胎,在出生时才分开。暂时不太明白,我们继续往下学。

场景描述

假设现在要实现一个糖果贩卖机,投一美分硬币,转动曲柄,机器判断是否有糖,有的话吐出来,售罄的话将硬币退还。这个过程会涉及到一系列的状态,比如说:有没有投币、是否已售罄等。下面用实例变量的普通方式来实现这个场景。

  1. 创建糖果机

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    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;
    }
    }
    /**
    * 投币
    */
    public void insertQuarter() {
    if (state == HAS_QUARTER) {
    // 已投25美分,不能再投了
    System.out.println("You can't insert another quarter");
    } else if (state == NO_QUARTER) {
    // 接受25美分,并更改成已经有25美分的状态
    state = HAS_QUARTER;
    System.out.println("You inserted a 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");
    }
    }
    /**
    * 退币
    */
    public void ejectQuarter() {
    if (state == HAS_QUARTER) {
    // 有25美分的话,退出25美分,回到没有25美分状态
    System.out.println("Quarter returned");
    state = NO_QUARTER;
    } else if (state == NO_QUARTER) {
    // 没有25美分的话,不能退出25美分
    System.out.println("You haven't inserted a quarter");
    } else if (state == SOLD_OUT) {
    // 糖果售罄,不可能接收25美分,当然也不可能退款
    System.out.println("You can't eject, you haven't inserted a quarter");
    } else if (state == SOLD) {
    // 已经转动曲柄,不能退款了
    System.out.println("Sorry, you already turned the crank");
    }
    }
    /**
    * 转动曲柄
    */
    public void turnCrank() {
    if (state == HAS_QUARTER) {
    // 转动曲柄,将状态改成售出,然后调用dispense()方法
    System.out.println("You turned...");
    state = SOLD;
    dispense();
    } 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, there are no gumballs");
    } else if (state == SOLD) {
    // 已经转动曲柄,不能再转了
    System.out.println("Turning twice doesn't get you another gumball");
    }
    }
    /**
    * 发糖果
    */
    public void dispense() {
    if (state == HAS_QUARTER) {
    // 错误状态,得不到糖果
    System.out.println("No gumball dispensed");
    } else if (state == NO_QUARTER) {
    // 还没投币
    System.out.println("You need pay first");
    } else if (state == SOLD_OUT) {
    // 错误状态,得不到糖果
    System.out.println("No gumball dispensed");
    } else if (state == SOLD) {
    // 成功,发出糖果
    System.out.println("A gumball comes rolling out the slot");
    count = count - 1;
    if (count == 0) {
    // count == 0 售罄
    System.out.println("0pps, out of gumballs");
    state = SOLD_OUT;
    } else {
    // 未售罄,状态变为等待投币
    state = NO_QUARTER;
    }
    }
    }
    public void refill(int numGumBalls) {
    this.count = numGumBalls;
    state = NO_QUARTER;
    }
    @Override
    public String toString() {
    StringBuffer result = new StringBuffer();
    result.append("\nMighty Gumball, Inc.");
    result.append("\nJava-enabled Standing Gumball Model #2004\n");
    result.append("Inventory: " + count + " gumball");
    if (count != 1) {
    result.append("s");
    }
    result.append("\nMachine is ");
    if (state == SOLD_OUT) {
    result.append("sold out");
    } else if (state == NO_QUARTER) {
    result.append("waiting for quarter");
    } else if (state == HAS_QUARTER) {
    result.append("waiting for turn of crank");
    } else if (state == SOLD) {
    result.append("delivering a gumball");
    }
    result.append("\n");
    return result.toString();
    }
    }
  2. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    public class Test {
    public static void main(String[] args) {
    GumballMachine gumballMachine = new GumballMachine(5);
    System.out.println(gumballMachine);
    gumballMachine.insertQuarter();
    gumballMachine.turnCrank();
    System.out.println(gumballMachine);
    gumballMachine.insertQuarter();
    gumballMachine.ejectQuarter();
    gumballMachine.turnCrank();
    System.out.println(gumballMachine);
    gumballMachine.insertQuarter();
    gumballMachine.turnCrank();
    gumballMachine.insertQuarter();
    gumballMachine.turnCrank();
    gumballMachine.ejectQuarter();
    System.out.println(gumballMachine);
    gumballMachine.insertQuarter();
    gumballMachine.insertQuarter();
    gumballMachine.turnCrank();
    gumballMachine.insertQuarter();
    gumballMachine.turnCrank();
    gumballMachine.insertQuarter();
    gumballMachine.turnCrank();
    System.out.println(gumballMachine);
    }
    }

可以看出用成员变量状态来控制状态的变更,很不友好,这么多if/else容易出错,当需求有变更时也不容易扩展。比如说下面要新加个需求,当曲柄被转动时,有10%的几率掉下两颗糖果,如果这个需要新增一个状态的话,每个方法都得修改一遍。下面我们来学习使用状态模式重写这个场景。

代码示例

  1. 创建状态接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public interface State {
    public void insertQuarter();
    public void ejectQuarter();
    public void turnCrank();
    public void dispense();
    public void refill();
    }
  2. 创建一个空的糖果机,目的是构造各个状态的时候需要用到该类。这个糖果机存储了一系列实现State接口的状态,同时包含它本身该有的行为。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    public class GumballMachine {
    State hasQuarterState;
    State noQuarterState;
    State soldState;
    State soldOutState;
    State state = soldOutState;
    int count = 0;
    public GumballMachine(int numberGumballs) {
    this.count = numberGumballs;
    if (numberGumballs > 0) {
    state = noQuarterState;
    }
    }
    public void insertQuarter() {
    }
    public void ejectQuarter() {
    }
    public void turnCrank() {
    }
    void setState(State state) {
    this.state = state;
    }
    void releaseBall() {
    System.out.println("A gumball comes rolling out the slot...");
    if (count != 0) {
    count--;
    }
    }
    public State getHasQuarterState() {
    return hasQuarterState;
    }
    public State getNoQuarterState() {
    return noQuarterState;
    }
    public State getSoldState() {
    return soldState;
    }
    public State getSoldOutState() {
    return soldOutState;
    }
    public int getCount() {
    return count;
    }
    }
  3. 实现NoQuarter状态,状态实现中保存了糖果机对象,其中实现的所有方法都是基于在当前状态下允许的行为

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    public class NoQuarterState implements State {
    GumballMachine gumballMachine;
    public NoQuarterState(GumballMachine gumballMachine) {
    this.gumballMachine = gumballMachine;
    }
    /**
    * 当前状态下, 可以投币
    */
    @Override
    public void insertQuarter() {
    System.out.println("You inserted quarter");
    gumballMachine.setState(gumballMachine.getHasQuarterState());
    }
    /**
    * 当前状态下,退币操作无效
    */
    @Override
    public void ejectQuarter() {
    System.out.println("You haven't 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");
    }
    }
  4. 实现HasQuarter状态

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    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");
    }
    /**
    * 当前状态下,可以退币,并且状态回到等待投币状态
    */
    @Override
    public void ejectQuarter() {
    System.out.println("Quarter returned");
    gumballMachine.setState(gumballMachine.getNoQuarterState());
    }
    /**
    * 当前状态下,可以转动曲柄,并且状态变成已出售
    */
    @Override
    public void turnCrank() {
    System.out.println("You turned...");
    gumballMachine.setState(gumballMachine.getSoldState());
    }
    /**
    * 当前状态下,不可以执行发糖果操作
    */
    @Override
    public void dispense() {
    System.out.println("No gumball dispensed");
    }
    }
  5. 实现Sold状态

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    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();
    if (gumballMachine.getCount() > 0) {
    gumballMachine.setState(gumballMachine.getNoQuarterState());
    } else {
    System.out.println("Oops, out of gumballs!");
    gumballMachine.setState(gumballMachine.getSoldOutState());
    }
    }
    }
  6. 实现SoldOut状态

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    public class SoldOutState implements State {
    GumballMachine gumballMachine;
    public SoldOutState(GumballMachine gumballMachine) {
    this.gumballMachine = gumballMachine;
    }
    /**
    * 当前状态下,不支持投币
    */
    @Override
    public void insertQuarter() {
    System.out.println("You can't insert a quarter, the machine is sold out");
    }
    /**
    * 当前状态下不支持退币
    */
    @Override
    public void ejectQuarter() {
    System.out.println("You can't eject, you haven't inserted a quarter yet");
    }
    /**
    * 当前状态下不支持转动曲柄
    */
    @Override
    public void turnCrank() {
    System.out.println("You turned, but there are no gumballs");
    }
    /**
    * 当前状态下不支持发糖果
    */
    @Override
    public void dispense() {
    System.out.println("No gumball dispensed");
    }
    }
  7. 完善糖果机,将几个状态保存到其中,并完善行为

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    public class GumballMachine {
    State hasQuarterState;
    State noQuarterState;
    State soldState;
    State soldOutState;
    State state = soldOutState;
    int count = 0;
    public GumballMachine(int numberGumballs) {
    soldOutState = new SoldOutState(this);
    noQuarterState = new NoQuarterState(this);
    hasQuarterState = new HasQuarterState(this);
    soldState = new SoldState(this);
    this.count = numberGumballs;
    if (numberGumballs > 0) {
    state = noQuarterState;
    }
    }
    public void insertQuarter() {
    state.insertQuarter();
    }
    public void ejectQuarter() {
    state.ejectQuarter();
    }
    public void turnCrank() {
    state.turnCrank();
    state.dispense();
    }
    void setState(State state) {
    this.state = state;
    }
    void releaseBall() {
    System.out.println("A gumball comes rolling out the slot...");
    if (count != 0) {
    count--;
    }
    }
    public State getHasQuarterState() {
    return hasQuarterState;
    }
    public State getNoQuarterState() {
    return noQuarterState;
    }
    public State getSoldState() {
    return soldState;
    }
    public State getSoldOutState() {
    return soldOutState;
    }
    public int getCount() {
    return count;
    }
    @Override
    public String toString() {
    StringBuffer result = new StringBuffer();
    result.append("\nMighty Gumball, Inc.");
    result.append("\nJava-enabled Standing Gumball Model #2004");
    result.append("\nInventory: " + count + " gumball");
    if (count != 1) {
    result.append("s");
    }
    result.append("\n");
    result.append("Machine is " + state + "\n");
    return result.toString();
    }
    }
  8. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class Test {
    public static void main(String[] args) {
    GumballMachine gumballMachine = new GumballMachine(2);
    System.out.println(gumballMachine);
    gumballMachine.insertQuarter();
    gumballMachine.turnCrank();
    System.out.println(gumballMachine);
    gumballMachine.insertQuarter();
    gumballMachine.turnCrank();
    gumballMachine.insertQuarter();
    gumballMachine.turnCrank();
    System.out.println(gumballMachine);
    }
    }

通过代码可以很清晰地看出,将每个状态从if/else中独立出来,管好自己当前状态下该做的事情,这样代码清晰,也便于扩展。这个模式跟策略模式确实很像,都是通过组合的方式来改变类的行为,之间的差别在于它们的意图。状态模式是将一群行为封装在状态对象中,可以随时委托到其中的一个。而策略模式的意图是在封装的行为中可以动态地选择最适当的策略去执行。之前说的十次抽中一次的游戏,尚未解决。。。

扩展抽奖功能

场景上面已经说过了,就是有10%地几率掉下两颗糖果,下面我们来扩展代码。

  1. 添加一个WinnerState状态,该状态和Sold状态是差不多的,不同的是,该状态下,会掉两颗糖果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    public class WinnerState implements State {
    GumballMachine gumballMachine;
    public WinnerState(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() {
    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());
    }
    }
    }
    }
  2. 修改糖果机,在其中添加WinnerState,下面只贴出扩展部分

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class GumballMachine {
    ......
    State state = soldOutState;
    public GumballMachine(int numberGumballs) {
    winnerState = new WinnerState(this);
    }
    public State getWinnerState() {
    return winnerState;
    }
    }
  3. 修改HashQuarter状态,增加随机数,产生10%赢的机会,下面只贴出扩展部分

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class HasQuarterState implements State {
    Random randomWinner = new Random(System.currentTimeMillis());
    /**
    * 当前状态下,可以转动曲柄,并且状态变成已出售
    */
    @Override
    public void turnCrank() {
    System.out.println("You turned...");
    int winner = randomWinner.nextInt(10);
    if ((winner == 0) && gumballMachine.getCount() > 1) {
    gumballMachine.setState(gumballMachine.getWinnerState());
    } else {
    gumballMachine.setState(gumballMachine.getSoldState());
    }
    }
    }
  4. 测试,多跑几次,将有几率掉落两颗糖果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class Test {
    public static void main(String[] args) {
    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);
    }
    }

通过修改需求可见扩展还是挺方便的,原有代码的改动也不大,逻辑清晰,不容易出错。我们再回顾下之前容易混淆的集中模式:

  • 状态模式:封装基于状态的行为,并将行为委托到当前状态
  • 策略模式:将可以互换的行为封装起来,然后使用委托的方法,决定使用哪一个行为
  • 模板方法:由子类决定如何实现算法中的某些步骤

状态模式的学习就到这里了。