设计模式之 -- 状态模式(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 }
View Code

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 }
View Code

   现在我们来测试它是否能正常工作:

 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 }
View Code

   经过一番简单的测试,糖果机能正常工作,测试明细如下:

机器已经准备完毕,请您投入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颗,尽情享受吧...
本糖果机所有糖果已经售罄,尽情下次光临哦~~~
View Code

   看到代码中大量的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 }
View Code

   然后,在糖果机类中使用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 }
View Code

   在重构后的代码中,不存在任何的条件分支语句,代码有了很好的可读性,也漂亮了许多,是么....

原文地址:https://www.cnblogs.com/hanganglin/p/4326061.html