设计模式 -- 状态模式
1、什么是状态模式?
状态模式:当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了它的类。
对象的状态可能有很多种情况,在不同的状态下,对象会有不同的行为,或者对同一行为有不同的反应。在这种环境下,使用状态模式能替代我们编写的大量判断对象当前状态的if-else语句。这是符合“开闭原则”的。举个例子来说,我们用的电灯开关,通常电灯开着的时候,我们按下开关,电灯会关闭,而电灯关着的时候,按下开关,电灯会打开。对于同样是按下开关这个请求,电灯会做出不同的响应,原因是电灯的状态不同。
2、如何实现状态模式?
状态模式状态模式主要解决的是当控制一个对象状态的条件表达式过于复杂时的情况。这种解决方案就是把状态的判断逻辑转移到表示不同状态的一系列类中,可以把复杂的判断逻辑简化。基本设计类图:
这里面说的很明白,Context就是拥有不同状态的那个类,State是状态抽象,这个抽象里规定了所有状态下的接口,ConcreteState是具体的状态,这里面要实现在这个状态下的动作。这样,Context把自己的行为封装在了状态里,他的请求(Request)都要借给他的状态来实现,这样就将状态逻辑和实现动作分离开来,当有新的状态时,我们只需要添加状态子类即可。
举例分析:
用“自动糖果机”来说明状态模式:自动糖果机的控制器需要按照下图来工作:
此为状态图,图中每个圆圈都表示一个状态,每个箭头都表示状态的转换。
糖果机的状态:没有25分钱(大概应该是糖果机最开始的状态),有25分钱,售出糖果,糖果售完4种,这4种状态下的可以的操作不相同,4种状态间的转换条件也不相同。比如在没有25分钱的状态下有一个操作:投入25分钱,之后状态就改为有25分钱,这时的操作有两种:转动曲柄和退回25分钱……现在需求有变,为了增加客源,我们的客户希望糖果机能够有随机产生一位赢家,兵多给他一颗糖果。那么很简单,我们需要在状态图中加一个状态。
好了,状态图理解了,其实在做状态模式设计的时候这个图还是挺有作用的。
下面我们用代码来体现这个设计:
//糖果机类 public class GumballMachine { //一个糖果机在不同时刻会出现不同状态 static State soldOutState; public State getSoldOutState() { return soldOutState; } static State noQuarterState; public State getNoQuarterState() { return noQuarterState; } static State hasQuarterState; public State getHasQuarterState() { return hasQuarterState; } static State soldState; public State getSoldState() { return soldState; } static State winnerState; public State getWinnerState() { return winnerState; } //维护一个状态 State state = soldOutState; //糖果数量 int count = 0; public int Count { get { return count; } set { count = value; } } //用糖果数量初始化一个糖果机 public GumballMachine(int numberGumballs) { soldOutState = new SoldOutState(this); noQuarterState = new NoQuaterState(this); hasQuarterState = new HasQuaterState(this); soldState = new SoldState(this); this.count = numberGumballs; if (numberGumballs > 0) { state = noQuarterState; } } //插入25分钱 public void insertQuarter() { state.insertQuater();//由状态来完成 } //退回25分钱 public void ejectQuarter() { state.ejectQuater();//由状态来完成 } //转动曲柄 public void turnCrank() { state.turnCrank(); state.dispense();//由状态来完成 } //释放糖果 public void releaseBall() { Console.WriteLine("A gumball comes rolling out the slot ……"); if (count != 0) { count = count - 1; } } //设置状态 public void setState(State aState) { this.state = aState; } //打印糖果机状态及糖果数量 public void printGumballMachine() { Console.WriteLine("gumballs number: "+this.count); Console.WriteLine("gumballMachine state: "+this.state); } }
//状态接口类 -- 定义了一定状态下的行为方法 public interface State { void insertQuater();//插入25分钱 void ejectQuater(); //退回25分钱 void turnCrank(); //转动曲柄 void dispense(); //弹出糖果 } //弹出糖果状态 public class SoldState : State { GumballMachine gumballMachine;//维护自己的主体 public SoldState(GumballMachine gumballMachine) { this.gumballMachine = gumballMachine; } //在弹出糖果状态下再次插入25分钱会给出提示 public void insertQuater() { Console.WriteLine("Please wait ,we are already giving you a gumball"); } //在弹出糖果状态下退回25分钱会给出提示 public void ejectQuater() { Console.WriteLine("Sorry ,but you have turned the crank"); } //在弹出糖果状态下再次转动曲柄会给出提示 public void turnCrank() { Console.WriteLine("Turning twice doesn't get you another gumball!"); } //这里才是真正弹出糖果的方法 public void dispense() { gumballMachine.releaseBall();//将糖果数量减1 //根据情况改变状态 if (gumballMachine.Count > 0) { gumballMachine.setState(gumballMachine.getNoQuarterState()); } else { Console.WriteLine("Oops! out of gumballs"); gumballMachine.setState(gumballMachine.getSoldOutState()); } } } //糖果卖光的状态 public class SoldOutState : State { GumballMachine gumballMachine; public SoldOutState(GumballMachine gumballMachine) { this.gumballMachine = gumballMachine; } public void insertQuater() { //卖光状态下不应该再接受用户的钱 Console.WriteLine("You cann't insert a quarter, the machine is sold out"); } public void ejectQuater() { Console.WriteLine("You cann't eject,you haven't inserted a quarter yet"); } public void turnCrank() { Console.WriteLine("You turnd,but there are no gumballs"); } public void dispense() { Console.WriteLine("No gumball dispensed"); } } //机器里没钱状态 public class NoQuaterState : State { GumballMachine gumballMachine; public NoQuaterState(GumballMachine gumballMachine) { this.gumballMachine = gumballMachine; } public void insertQuater() { Console.WriteLine("You inserted a quarter"); gumballMachine.setState(gumballMachine.getHasQuarterState()); } public void ejectQuater() { Console.WriteLine("You haven't inserted a quarter"); } public void turnCrank() { Console.WriteLine("You turned ,but there is no quarter"); } public void dispense() { Console.WriteLine("You need to pay first"); } } } //赢家状态 public class WinnerState : State { GumballMachine gumballMachine; public WinnerState(GumballMachine gumballMachine) { this.gumballMachine = gumballMachine; } public void insertQuater() { Console.WriteLine("ERROR"); } public void ejectQuater() { Console.WriteLine("ERROR"); } public void turnCrank() { Console.WriteLine("ERROR"); } public void dispense() { Console.WriteLine("YOU ARE A WINNER! You get two gumballs for your quarter"); gumballMachine.releaseBall(); if (gumballMachine.Count == 0) { gumballMachine.setState(gumballMachine.getSoldOutState()); } else { gumballMachine.releaseBall(); if (gumballMachine.Count >= 0) { gumballMachine.setState(gumballMachine.getNoQuarterState()); } else { Console.WriteLine("Oops! out of gumballs!"); gumballMachine.setState(gumballMachine.getSoldOutState()); } } } }
//测试代码 static void Main(string[] args) { GumballMachine gumballMachine = new GumballMachine(5); gumballMachine.printGumballMachine(); Console.WriteLine("\n"); gumballMachine.insertQuarter(); gumballMachine.turnCrank(); gumballMachine.printGumballMachine(); Console.WriteLine("\n"); gumballMachine.insertQuarter(); gumballMachine.turnCrank(); gumballMachine.insertQuarter(); gumballMachine.turnCrank(); gumballMachine.printGumballMachine(); Console.ReadKey(); }
状态模式封装基于状态的行为,并将行为委托到当前状态实现。我们使用组合通过简单引用不同状态对象来造成类改变的假象。
关于状态模式的几个问题:
1、具体状态总是要决定下一个状态是什么吗?
不,并非总是如此,Context也可以决定状态的转换流向。一般说,状态转换比价固定时,就是和放在Context中,然而如果状态转换是更加动态的时候,通常就会放在状态类中(比如说,在GumballMachine中,由运行时糖果数目来决定状态药转换到NoQuarter还是SoldOut)。当然,我们将状态转换放在状态中是有缺点的:状态之间产生了依赖。在糖果机的实现中,试图通过使用Context的getter方法来把依赖减到最小,而不是显示硬编码具体状态类。请注意:在做这个决策同时,也等于在为另一件事做决策,那就是究竟那个类(是Context还是状态)是对修改封闭的。
2、客户会直接和状态交互吗?
不会。状态是用在Context中代表它的内部状态及行为的,所以只有Context才会对状态提出请求。客户不会直接改变Context的状态。全盘了解状态是Context的工作,客户根本不了解,所以不会直接和状态联系。
3、如果我的程序中Context中有许多实例,这些实例之间可以共享状态对象吗?
是的,绝对可以!事实上,这是很常见的做法,但是唯一的前提是:你的状态对象不能持有它们自己的内部状态,否则就不能共享。想要共享状态,你需要把每个状态都指定到静态的实例变量中,如果你的状态需要利用Context中的方法或者实例变量,还必须在每个Handler()方法内传入一个Context引用。