zoukankan      html  css  js  c++  java
  • 设计模式之 -- 状态模式(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

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

  • 相关阅读:
    Prim算法的3个版本
    [转]"undefined reference to" 问题解决方法
    C/C++ 读写 Excel
    Poj 3468
    关于PS中矩形工具的学习
    PS初学习
    java第二天学习。
    Java学习第一天
    二叉树的线索化
    struct files_struct
  • 原文地址:https://www.cnblogs.com/hanganglin/p/4326061.html
Copyright © 2011-2022 走看看