Head First设计模式——状态模式
糖果机
如下糖果机工作状态图,我们对这个状态图进行编码实现糖果机的工作过程
这个状态图的每个圆圈代表一个状态,可以看到有4个状态同时又4个动作,分别是:“投入1元钱”、“退回1元钱”、“转动曲柄”、“发放糖果”。当要发放糖果的时候需要判断糖果数量是否为0来进入“糖果售磐”或者“没有1元钱”状态。所以有5个状态转换。
接下来我们对状态图进行分析实现编码
①找出状态:没有1元钱、有1元钱、糖果售出、糖果售磐。
②创建实例变量持有当前状态,定义每个状态的值。
static int SOLD_OUT=0; static int NO_ONERMB=1; static int HAS_ONERMB=2; static int SOLD=3; int State=SOLD_OUT;
③将系统中的动作整合起来:投入1元、退回1元、转动曲柄、发放糖果。
以投入1元为例
public void InsertOneRMB() { if (State == HAS_ONERMB) { Console.WriteLine("已经投入了,不能再投入"); } else if (State == SOLD_OUT) { Console.WriteLine("糖果已经售磐,不能再投入"); } else if (State == SOLD) { Console.WriteLine("请稍后投入,正在发放糖果"); } else if (State == NO_ONERMB) { State = HAS_ONERMB; Console.WriteLine("你投入了1元钱"); } }
根据分析我们就可以写出糖果机的代码,其他几个动作具体实现就不再写了。
1 class GumballMachine 2 { 3 readonly static int SOLD_OUT = 0; 4 readonly static int NO_ONERMB = 1; 5 readonly static int HAS_ONERMB = 2; 6 readonly static int SOLD = 3; 7 8 int State = SOLD_OUT; 9 int Count = 0; 10 11 public GumballMachine(int count) { 12 this.Count = count; 13 if (count > 0) 14 { 15 State = NO_ONERMB; 16 } 17 } 18 /// <summary> 19 /// 投入1元 20 /// </summary> 21 public void InsertOneRMB() { 22 23 if (State == HAS_ONERMB) 24 { 25 Console.WriteLine("已经投入了,不能再投入"); 26 } 27 else if (State == SOLD_OUT) { 28 Console.WriteLine("糖果已经售磐,不能再投入"); 29 } 30 else if (State == SOLD) 31 { 32 Console.WriteLine("请稍后投入,正在发放糖果"); 33 } 34 else if (State == NO_ONERMB) 35 { 36 State = HAS_ONERMB; 37 Console.WriteLine("你投入了1元钱"); 38 } 39 } 40 /// <summary> 41 /// 退回1元 42 /// </summary> 43 public void EjectOneRMB() { } 44 45 /// <summary> 46 /// 转动手柄 47 /// </summary> 48 public void TurnCrank() { } 49 50 /// <summary> 51 /// 发放糖果 52 /// </summary> 53 public void Dispense() { } 54 }
通过这样的实现已经是考虑的比较周详而且代码清晰。但是该来的还是回来,需求变更仍然让我们的代码面临问题。接下来我们看如何满足需求以及状态模式的使用。
需求变更
需求:当个赢家!10人有1人可以得到一颗免费糖果(当曲柄转动时,有10%的机率掉下来两颗糖果)。
针对于这个需求我们将状态添加到状态图
针对于原来的代码怎么修改呢?首先我们需要加上一个新的状态“赢家”,然后必须在每个方法中加入一个新的条件判断处理“赢家”状态,更麻烦的是TurnCrank方法需要大改造,因为必须加上检查是否赢家来决定切换到赢家状态还是售出糖果状态。如果再加入其他状态,那么代码要继续修改,而现在的代码面对变法时有几个问题。
①没有遵循开闭原则。
②状态转换被隐藏在条件语句中,不明显。
③没有把会改变的部分封装起来。
④该设计不符合面向对象。
新的设计
我们不用现在的代码,重新它以便将状态对象封装在各自的类中,然后再动作发生时委托给当前状态。
①首先,我们定义一个Sate接口。在这个接口内,糖果机的每个动作都有一个对应的方法。
②为机器中的每个状态实现状态类。这些类负责在对应的状态下进行机器的行为。
③将动作委托到状态类。
用类图来梳理设计
按照类图进行实现,首先定义接口。然后实现NoOneRMBState
1 public class NoOneRMBState : State 2 { 3 GumballMachine gumballMachine; 4 public NoOneRMBState(GumballMachine gumballMachine) 5 { 6 this.gumballMachine = gumballMachine; 7 } 8 9 public void InsertOneRMB() 10 { 11 Console.WriteLine("你投入了1元钱"); 12 gumballMachine.SetState(gumballMachine.hasOneRMBState); //将糖果状态改到hasOneRMBState 13 } 14 public void EjectOneRMB() 15 { 16 Console.WriteLine("没有钱可退"); 17 } 18 19 public void TurnCrank() 20 { 21 Console.WriteLine("没有钱,不能转动"); 22 } 23 public void Dispense() 24 { 25 Console.WriteLine("没有钱,不能发放糖果"); 26 } 27 }
其他状态类是具体的业务代码就不再一一实现了,我们最后改造糖果机
1 public class GumballMachine 2 { 3 public State soldOutState { get; } 4 public State noOneRMBState { get; } 5 public State hasOneRMBState { get; } 6 public State soldState { get; } 7 8 9 State State; 10 int Count = 0; 11 12 public GumballMachine(int count) 13 { 14 this.Count = count; 15 soldOutState = new SoldOutState(this); 16 noOneRMBState = new NoOneRMBState(this); 17 hasOneRMBState = new HasOneRMBState(this); 18 soldState = new SoldState(this); 19 if (count > 0) 20 { 21 State = noOneRMBState; 22 } 23 else { 24 State = soldOutState; 25 } 26 } 27 /// <summary> 28 /// 投入1元 29 /// </summary> 30 public void InsertOneRMB() 31 { 32 State.InsertOneRMB(); 33 } 34 /// <summary> 35 /// 退回1元 36 /// </summary> 37 public void EjectOneRMB() { 38 State.EjectOneRMB(); 39 } 40 41 /// <summary> 42 /// 转动手柄 43 /// </summary> 44 public void TurnCrank() { 45 State.TurnCrank(); 46 //状态内部动作,所以我们不在需要单独一个发放糖果的方法。 47 State.Dispense(); 48 } 49 50 /// <summary> 51 /// 设置状态 52 /// </summary> 53 /// <param name="state"></param> 54 public void SetState(State state) 55 { 56 this.State = state; 57 } 58 }
如上就是利用状态模式改造后的代码。
状态模式定义
状态模式:允许对象在内部状态改变时改变它的行为,对象看起来好像是修改了它的类。
定义的第一部分描述这个模式将状态封装为独立的类,并将动作委托到代表当前状态的对象,行为会随着内部状态而改变。例如在noOneRMBState和hasOneRMBState两个状态时,投入1元,就会得到不同的行为。
第二部分“对象看起来好像是修改了它的类”,从客户来看如果说使用的对象能够完全改变自己的行为,那么会觉得这个对象实际上是从别的类再实例化而来的。事实上我们实在使用组合简单引用不同状态对象来造成类改变的假象。
策略模式与状态模式
我们发现策略模式与状态模式类图一样,但是他们所要干事情的意图完全不一样,所以我做个简要的区分
状态模式:对象创建后,可以告诉客户从什么状态开始,然后随着时间推移改变自己的状态,而任何状态的改变都是定义好的。
策略模式:允许对象通过组合和委托来拥有不同的算法或行为。能实例化一个类,给它一个实现某些行为的策略对象,也可以在运行时改变行为。