状态模式
定义:
要点:
状态模式允许一个对象基于内部状态而拥有不同的行为。
状态模式用类来代表状态。
通过将每个状态封装进一个类,我们把以后需要做的任何改变局部化了。
例子:
1.我们这次的话题是要实现一个糖果机,这个糖果机中如下的一些状态以指导糖果机的转移:
我们根据这个状态机写了一个有if..else为主体的状态机程序,对每一个状态的转入转出做了实现:
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) { System.out.println("You can't insert another quarter"); } else if (state == NO_QUARTER) { 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) { System.out.println("Please wait, we're already giving you a gumball"); } }
public void ejectQuarter() { if (state == HAS_QUARTER) { System.out.println("Quarter returned"); state = NO_QUARTER; } else if (state == NO_QUARTER) { System.out.println("You haven't inserted a quarter"); } else if (state == SOLD) { System.out.println("Sorry, you already turned the crank"); } else if (state == SOLD_OUT) { System.out.println("You can't eject, you haven't inserted a quarter yet"); } }
public void turnCrank() { if (state == SOLD) { System.out.println("Turning twice doesn't get you another gumball!"); } else if (state == NO_QUARTER) { 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) { 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 refill(int numGumBalls) { this.count = numGumBalls; state = NO_QUARTER; }
public String toString() { } }
但是现在这个公司有了新的要求:当曲柄被转动时,有10%的几率掉下来的是两颗糖——这有点gambling的意思,但这只是一个游戏。
这个时候我们发现一切都变得如此郁闷,我们不但要加入一个“赢家”的状态, 还有在每一个动作中都要加入一些判断是不是在这个状态的判定和后续动作定义。这并不是我们想要的。
2.对于此,我们的设计应该首先定义一个State接口,在这个接口中,糖果机的每一个动作都有一个对应的方法。在设计这个接口时,我们的原则是:我们倾向于使用抽象类,这样想加入新的方法就容易的多,但是没有共同的功能(的实现)可以放入其中的则用接口。
public interface State { public void insertQuarter(); public void ejectQuarter(); public void turnCrank(); public void dispense(); }
然后我们对每个状态实现状态类,我们将动作委托到状态类中。
例如:没有投币的状态NoQuarterState
public class NoQuarterState implements State { GumballMachine gumballMachine; public NoQuarterState(GumballMachine gumballMachine) { this.gumballMachine = gumballMachine; } public void insertQuarter() { System.out.println("You inserted a quarter"); gumballMachine.setState(gumballMachine.getHasQuarterState());//状态转移 } public void ejectQuarter() { System.out.println("You haven't inserted a quarter"); } public void turnCrank() { System.out.println("You turned, but there's no quarter"); } public void dispense() { System.out.println("You need to pay first"); } public String toString() { return "waiting for quarter"; } }
3.我们将这些状态整合到一个类中——糖果机类,在这个类的设计中分为两大部分——对于状态的setter和getter,对于状态方法的封装(委托给当前的类)。另外这样我们就得到一个糖果机了:
public class GumballMachine { State soldOutState; State noQuarterState; State hasQuarterState; State soldState; 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 = count - 1; } } int getCount() { return count; } void refill(int count) { this.count = count; state = noQuarterState; }
public State getState() { return state; }
public State getSoldOutState() { return soldOutState; }
public State getNoQuarterState() { return noQuarterState; }
public State getHasQuarterState() { return hasQuarterState; }
public State getSoldState() { return soldState; } public String toString() { } }
我们现在写一个测试类,用这个糖果机买点糖果出来,并测试一下这个机器的逻辑是不是正确:
public class GumballMachineTestDrive {
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); } }
4.这时,我们再考虑那个10%中奖率的小游戏就简单许多了。我们在GumballMachine 中加入一个新的状态winnerState,虽然我们可以把这个逻辑放入原来发放糖果的状态,但是就违背了“一个类,一个职责”的原则:
public class WinnerState implements State { GumballMachine gumballMachine; public WinnerState(GumballMachine gumballMachine) { this.gumballMachine = gumballMachine; } public void insertQuarter() { System.out.println("Please wait, we're already giving you a Gumball"); } public void ejectQuarter() { System.out.println("Please wait, we're already giving you a Gumball"); } public void turnCrank() { System.out.println("Turning again doesn't get you another gumball!"); } 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()); } } } public String toString() { return "despensing two gumballs for your quarter, because YOU'RE A WINNER!"; } }
我们再加入10%成为赢家的逻辑:
public class HasQuarterState implements State { Random randomWinner = new Random(System.currentTimeMillis()); GumballMachine gumballMachine; public HasQuarterState(GumballMachine gumballMachine) { this.gumballMachine = gumballMachine; } public void insertQuarter() { System.out.println("You can't insert another quarter"); } public void ejectQuarter() { System.out.println("Quarter returned"); gumballMachine.setState(gumballMachine.getNoQuarterState()); } 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()); } }
public void dispense() { System.out.println("No gumball dispensed"); } public String toString() { return "waiting for turn of crank"; } }
5.我们给这样的状态机设计一个名字就是“状态模式”——允许对象在内部状态改变时改变它的行为,对象在外边看起来就像是修改了它的类一样。也就是说,里面的逻辑对外是不可见的,但同时实际的状态,或者说参与的逻辑又是高度可扩展的。记得,不要让外界直接接触状态,这样就破坏了面向对象设计的原则。
我们可以拿状态模式与策略模式做个对比,前者在内部定了各个逻辑步骤和状态转移,随着程序的进行对外呈现不同的样子,这是对多个条件判断的一种替代方案,而后者则是在一开始由外部主动指定要组合的策略对象是哪些,这是对继承之外的一种替代方案。
6.我们可以把这个设计再提升一下:
1) 我们可以把State设计为抽象类,将一些方法默认的行为,尤其是出错的行为放入其中,毕竟有些出错提示是颇具有通用性的,可以供子状态去继承。
2)当我们没有投币时也可以调用转动曲柄的方法,这实际上是不合理的,我们可以通过加入一个布尔类型来屏蔽这一bug。