设计模式之 -- 状态模式(State)
状态模式允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。当控制一个对象的状态转换条件分支语句(if...else或switch...case)过于复杂时,可以此模式将状态的判断逻辑转移到不同状态的一系列类中,将复杂的逻辑简单化,便于阅读与维护。
1、为什么要使用状态模式?
在软件开发过程中,应用程序可能会根据不同的条件作出不同的行为。常见的解决办法是先分析所有条件,通过大量的条件分支语句(if...else或switch...case)指定应用程序在不同条件下作出的不同行为。但是,每当增加一个条件时,就可能修改大量的条件分支语句,使得分支代码越来越复杂,代码的可读性、扩展性、可维护性也会越来越差,这时候就该状态模式粉墨登场了。
2、解决原理
状态模式将大量的判断逻辑转移到表示不同状态的一系列中,从而消除了原先复杂的条件分支语句,降低了判断逻辑的复杂度。
3、状态模式适用的两种情况
① 一个对象的行为取决于它的状态,并且他必须在运行时刻根据状态改变它的行为;
② 一个操作中含有庞大的多分支的条件语句,且这些分支依赖于该对象的状态。这个状态通常用一个或多个枚举常量表示;
4、结构
状态模式的UML类图如图1所示:
图1 状态模式的UML类图
由图可知:
① State类,抽象状态类,定义一个接口以封装与Context的一个特定状态相关的行为;
② ConcreteState类,具体状态,每一个子类实现一个与Context的一个特定状态相关的行为;
③ Context类,维护一个ConcreteState子类的实例,这个实例定义当前的状态;
实现代码如下:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 6 namespace State 7 { 8 /* 9 * State类,抽象状态类,定义一个接口以封装与Context的一个特定状态相关的行为 10 */ 11 abstract class State 12 { 13 public abstract void Handle(Context context); 14 } 15 16 class ConcreteStateA : State 17 { 18 public override void Handle(Context context) 19 { 20 //这里写状态A的处理代码 21 //... 22 23 //假设ConcreteStateA的下一个状态是ConcreteStateB 24 //此处状态定义可以在状态子类中指定,也可以在外部指定 25 context.setState(new ConcreteStateB()); 26 } 27 } 28 29 class ConcreteStateB : State 30 { 31 public override void Handle(Context context) 32 { 33 //这里写状态B的处理代码 34 //... 35 36 //假假设ConcreteStateB的下一个状态是ConcreteStateA 37 //此处状态定义可以在状态子类中指定,也可以在外部指定 38 context.setState(new ConcreteStateA()); 39 } 40 } 41 /* 42 * Context类,维护一个ConcreteState子类的实例,这个实例定义当前的状态 43 */ 44 class Context 45 { 46 private State state; 47 48 public Context(State state) 49 { 50 this.state = state; 51 } 52 53 public State getState() 54 { 55 return this.state; 56 } 57 58 public void setState(State state) 59 { 60 this.state = state; 61 Console.WriteLine("当前状态:"+this.state.GetType().Name); 62 } 63 64 //调用子类的对应方法 65 public void Request() 66 { 67 this.state.Handle(this); 68 } 69 } 70 }
5、状态模式带来的优点与效果
① 使得程序逻辑更加清晰、易维护。使用状态模式消除了大量的条件分支语句,将特定的状态相关的行为都放入一个State子类对象中,由于所有与状态相关的代码都存在于某个ConcreteState中,所以通过定义新的子类可以很容易地增加新的状态和转换;
② 它使得状态转换显示化。通过State对象表示不同的程序状态,比通过内部数据值来表示更加明确。而且数据转换状态有可能操作多个变量,而State对象转换只需更改状态实例,是一个原子操作,更加方便;
③ State对象可以被共享。不同Context对象可以共享一个State对象,这是使用内部数据变量表示状态很难实现的;
此外,状态模式实现较为复杂,同时也会大大增加系统类和对象个数,建议在合适条件下引用。
在著名的《Head First设计模式》有关状态模式的一节中提到一个经典的糖果机设计问题,其状态图如下图所示:
图2 糖果机设计状态图
在此糖果机状态图,我们可以看出存在有四种状态和四种动作,这四种动作分别为:“投入25分钱”、“退回25分钱”、“转动曲柄”和“发放糖果”。如果糖果工程师让你来设计这个程序,那么作为一个聪明的程序员,你会怎么设计呢?
首先,我们会用一个枚举来表示不同的状态,分别代表:售罄状态、售出状态、没有25分钱状态、有25分钱状态。
1 private enum State { 2 SOLD_OUT, SOLD, NO_QUARTER, HAS_QUARTER 3 }
然后在糖果机类体内定义一个内部状态变量state,用于记录糖果机当前所处的不同状态。然后在上述四种不同的动作方法内,根据此内部状态state的当前值来做出不同的处理。很快,糖果机很快就设计好了,代码如下:
1 package state.candymachine; 2 3 public class CandyMachine{ 4 5 //四种状态,分别代表:售罄状态、售出状态、没有25分钱状态、有25分钱状态 6 private enum State { 7 SOLD_OUT, SOLD, NO_QUARTER, HAS_QUARTER 8 } 9 10 private State state = State.NO_QUARTER; 11 private int candyNums = 0; 12 13 public CandyMachine(int candyNums) { 14 this.candyNums = candyNums; 15 if (candyNums > 0) { 16 this.state = State.NO_QUARTER; 17 } else { 18 this.state = State.SOLD_OUT; 19 } 20 } 21 22 public State getState() { 23 return state; 24 } 25 26 public void setState(State state) { 27 this.state = state; 28 switch(this.state){ 29 case SOLD: 30 System.out.println("糖果已经为您准备好,请点击售出糖果按钮.."); 31 break; 32 case SOLD_OUT: 33 System.out.println("本糖果机所有糖果已经售罄,尽情下次光临哦~~~"); 34 break; 35 case NO_QUARTER: 36 System.out.println("机器已经准备完毕,请您投入25分钱购买糖果~~~"); 37 break; 38 case HAS_QUARTER: 39 System.out.println("请您选择操作:退回25分钱 or 转动曲柄...."); 40 break; 41 } 42 } 43 44 public int getCandyNums() { 45 return candyNums; 46 } 47 48 public void setCandyNums(int candyNums) { 49 this.candyNums = candyNums; 50 } 51 52 public void trunCrank() { 53 if (state == State.HAS_QUARTER) { 54 System.out.println("曲柄已经开始转动,您的糖果即将出炉,尽请稍候~~~"); 55 setState(State.SOLD); 56 } else { 57 System.out.println("无法转动曲柄,您还未投入25分钱呢"); 58 } 59 } 60 61 public void dispenseCandy() { 62 if (state == State.SOLD) { 63 System.out.println("发放糖果1颗,尽情享受吧..."); 64 this.candyNums = this.candyNums - 1; 65 if (this.candyNums > 0) { 66 setState(State.NO_QUARTER); 67 } else { 68 setState(State.SOLD_OUT); 69 } 70 }else{ 71 System.out.println("无法发放糖果,请先转动曲柄"); 72 } 73 } 74 75 public void insertQuarter() { 76 if(state == State.NO_QUARTER){ 77 System.out.println("成功投入25分钱,您的糖果已经在等您了哦~~"); 78 setState(State.HAS_QUARTER); 79 }else{ 80 System.out.println("无法投入25分钱,机器中已经有25分钱了"); 81 } 82 } 83 84 public void ejectQuarter(){ 85 if(state == State.HAS_QUARTER){ 86 System.out.println("您的25分钱已经退回,欢迎下次光临~~~"); 87 setState(State.NO_QUARTER); 88 }else{ 89 System.out.println("无法退回25分钱,您还未投入钱呢"); 90 } 91 } 92 93 }
现在我们来测试它是否能正常工作:
1 package state.candymachine; 2 3 import java.util.Scanner; 4 5 public class MachineTest { 6 7 public static void main(String[] args) { 8 CandyMachine machine = new CandyMachine(3); 9 while (machine.getCandyNums() > 0) { 10 System.out.println("当前糖果机还剩" + machine.getCandyNums() + "颗糖果"); 11 System.out.println("请您选择您要执行的操作:1-投入 25分钱 2-退回25分钱 3-转动曲柄 4-发放糖果"); 12 Scanner sc = new Scanner(System.in); 13 int op = sc.nextInt(); 14 if (op == 1) 15 machine.insertQuarter(); 16 else if (op == 2) 17 machine.ejectQuarter(); 18 else if (op == 3) 19 machine.trunCrank(); 20 else if (op == 4) 21 machine.dispenseCandy(); 22 else 23 System.out.println("输入有误,请重新输入..."); 24 } 25 26 } 27 }
经过一番简单的测试,糖果机能正常工作,测试明细如下:
机器已经准备完毕,请您投入25分钱购买糖果~~~
当前糖果机还剩1颗糖果
请您选择您要执行的操作:1-投入 25分钱 2-退回25分钱 3-转动曲柄 4-发放糖果
1
【操作成功】成功投入25分钱,您的糖果已经在等您了哦~~
请您选择操作:退回25分钱 or 转动曲柄....
当前糖果机还剩1颗糖果
请您选择您要执行的操作:1-投入 25分钱 2-退回25分钱 3-转动曲柄 4-发放糖果
2
【操作成功】您的25分钱已经退回,欢迎下次光临~~~
机器已经准备完毕,请您投入25分钱购买糖果~~~
当前糖果机还剩1颗糖果
请您选择您要执行的操作:1-投入 25分钱 2-退回25分钱 3-转动曲柄 4-发放糖果
1
【操作成功】成功投入25分钱,您的糖果已经在等您了哦~~
请您选择操作:退回25分钱 or 转动曲柄....
当前糖果机还剩1颗糖果
请您选择您要执行的操作:1-投入 25分钱 2-退回25分钱 3-转动曲柄 4-发放糖果
1
无法投入25分钱,机器中已经有25分钱了
当前糖果机还剩1颗糖果
请您选择您要执行的操作:1-投入 25分钱 2-退回25分钱 3-转动曲柄 4-发放糖果
3
【操作成功】曲柄已经开始转动,您的糖果即将出炉,尽请稍候~~~
糖果已经为您准备好,请点击售出糖果按钮..
当前糖果机还剩1颗糖果
请您选择您要执行的操作:1-投入 25分钱 2-退回25分钱 3-转动曲柄 4-发放糖果
4
【操作成功】发放糖果1颗,尽情享受吧...
本糖果机所有糖果已经售罄,尽情下次光临哦~~~
看到代码中大量的if...else了吗,有没有觉得它们很不雅观?如果在此基础上,糖果机增加几个状态与动作,那么将会出现更大一大拨if...else,极大地降低了代码的可读性,提高了维护成本。
那么,如何使用State模式来重构此程序呢?
首先要定义一个State基类,包含上述四种动作。然后再分别定义四种不同状态的State子类,分别是:SoldState、SoldOutState、NoQuarterState和HasQuarterState,分别在对应的状态子类中实现不同的动作。
重构后的State以及其不同子类如下所示:
1 package state.candymachine; 2 3 public class State { 4 5 // 转动曲柄 6 public void trunCrank(CandyMachine machine) { 7 System.out.println("无法转动曲柄,请先投入25分钱"); 8 } 9 10 // 发放糖果 11 public void dispenseCandy(CandyMachine machine) { 12 System.out.println("无法发放糖果,请先转动曲柄"); 13 } 14 15 // 投入25分钱 16 public void insertQuarter(CandyMachine machine) { 17 System.out.println("无法投入25分钱,机器中已经有25分钱了"); 18 } 19 20 // 退回25分钱 21 public void ejectQuarter(CandyMachine machine) { 22 System.out.println("无法退回25分钱,您还未投入钱呢"); 23 } 24 25 } 26 27 /** 28 * 售出糖果状态 29 * 本次售出后 糖果=0则转入“糖果售罄”状态 糖果>0则转入“无25分钱”状态 30 */ 31 class SoldState extends State { 32 33 public SoldState() { 34 System.out.println("糖果已经为您准备好,请点击售出糖果按钮.."); 35 } 36 37 @Override 38 public void dispenseCandy(CandyMachine machine) { 39 System.out.println("【操作成功】发放糖果1颗,尽情享受吧..."); 40 int currCandyNums = machine.getCandyNums() - 1; 41 machine.setCandyNums(currCandyNums); 42 if (currCandyNums > 0) { 43 machine.setState(new NoQuarterState()); 44 } else { 45 machine.setState(new SoldOutState()); 46 } 47 } 48 } 49 50 // 售罄状态 51 class SoldOutState extends State { 52 public SoldOutState(){ 53 System.out.println("本糖果机所有糖果已经售罄,尽情下次光临哦~~~"); 54 } 55 } 56 57 // 无25分钱状态 58 class NoQuarterState extends State { 59 60 public NoQuarterState() { 61 System.out.println("机器已经准备完毕,请您投入25分钱购买糖果~~~"); 62 } 63 64 @Override 65 public void insertQuarter(CandyMachine machine) { 66 System.out.println("【操作成功】成功投入25分钱,您的糖果已经在等您了哦~~"); 67 machine.setState(new HasQuarterState()); 68 } 69 } 70 71 // 有25分钱状态 72 class HasQuarterState extends State { 73 74 public HasQuarterState() { 75 System.out.println("请您选择操作:退回25分钱 or 转动曲柄...."); 76 } 77 78 @Override 79 public void trunCrank(CandyMachine machine) { 80 System.out.println("【操作成功】曲柄已经开始转动,您的糖果即将出炉,尽请稍候~~~"); 81 machine.setState(new SoldState()); 82 } 83 @Override 84 public void ejectQuarter(CandyMachine machine) { 85 System.out.println("【操作成功】您的25分钱已经退回,欢迎下次光临~~~"); 86 machine.setState(new NoQuarterState()); 87 } 88 }
然后,在糖果机类中使用State的一个实例对象来记录当前的状态,对于四种动作则分别交给当前State实例对象来处理。
重构后的糖果机类CandyMachine如下所示:
1 package state.candymachine; 2 3 public class CandyMachine { 4 5 private State state; 6 private int candyNums = 0; 7 8 public CandyMachine(int candyNums) { 9 this.candyNums = candyNums; 10 if (candyNums > 0) { 11 setState(new NoQuarterState()); 12 } else { 13 setState(new SoldOutState()); 14 } 15 } 16 17 public State getState() { 18 return state; 19 } 20 21 public void setState(State state) { 22 this.state = state; 23 } 24 25 public int getCandyNums() { 26 return candyNums; 27 } 28 29 public void setCandyNums(int candyNums) { 30 this.candyNums = candyNums; 31 } 32 33 public void trunCrank(){ 34 this.state.trunCrank(this); 35 } 36 37 public void dispenseCandy(){ 38 this.state.dispenseCandy(this); 39 } 40 41 public void insertQuarter(){ 42 this.state.insertQuarter(this); 43 } 44 45 public void ejectQuarter(){ 46 this.state.ejectQuarter(this); 47 } 48 49 }
在重构后的代码中,不存在任何的条件分支语句,代码有了很好的可读性,也漂亮了许多,是么....