当前位置:   article > 正文

设计模式学习(五):State状态模式_state模式

state模式

目录

一、什么是State模式

二、State模式示例程序

2.1 伪代码

2.1.1 不使用State模式的伪代码

2.1.2 使用State模式的伪代码 

2.2 各个类之间的关系

2.3 State接口 

2.4 DayState类 

 2.5 NightState类

2.6 Context接口

2.7 SafeFrame类 

 2.8 用于测试的Main类

三、拓展思路的要点 

3.1 分而治之

3.2 依赖于状态的处理

3.3 应当是谁来管理状态迁移

3.4 不会自相矛盾

3.5 易于增加新的状态

3.6 实例的多面性

四、相关的设计模式

4.1 Singleton模式

4.2 Flyweight模式

五、思考题


一、什么是State模式

        在面向对象编程中,是用类表示对象的。也就是说,程序的设计者需要考虑用类来表示什么东西。类对应的东西可能存在于真实世界中,也可能不存在于真实世界中。

        在State模式中,我们用类来表示状态。在现实世界中,我们会考虑各种东西的“状态”,但是几乎不会将状态当作“东西”看待。因此,可能大家很难理解“用类来表示状态”的意思。

        在本文中,我们将要学习用类来表示状态的方法。以类来表示状态后,我们就能通过切换类来方便地改变对象的状态。当需要增加新的状态时,如何修改代码这个问题也会很明确。

        用一句话来概括:State模式就是用类来表示状态。

二、State模式示例程序

        这里我们来看一个警戒状态每小时会改变一次的警报系统。

        功能表:

        结构图:

  

        下面我们来用程序实现这个金库警报系统。

2.1 伪代码

2.1.1 不使用State模式的伪代码

        刚接触到这样的需求,你会怎样设计代码呢?如果是我,我可能会这样设计:

  1. 使用金库时被调用的方法() {
  2. if(白天) {
  3. 向警报中心报告使用记录
  4. } else if(晚上) {
  5. 向警报中心报告紧急事态
  6. }
  7. }
  8. 警铃响起时被调用的方法() {
  9. 像警报中心报告紧急事态
  10. }
  11. 正常通话时被调用的方法() {
  12. if(白天) {
  13. 呼叫警报中心
  14. } else if(晚上) {
  15. 呼叫警报中心的留言电话
  16. }
  17. }

2.1.2 使用State模式的伪代码 

        并不能说上面的代码有什么不对,只是我们今天要讲的State模式是完全不同的角度,咱们一起来看看他们的区别在哪,state模式的好处在哪。

  1. 表示白天的状态的类{
  2. 使用金库时被调用的方法() {
  3. 向警报中心报告使用记录
  4. }
  5. 警铃响起时被调用的方法() {
  6. 向警报中心报告紧急事态
  7. }
  8. 正常通话时被调用的方法(){
  9. 呼叫警报中心
  10. }
  11. }
  12. 表示晚上的状态的类{
  13. 使用金库时被调用的方法() {
  14. 向警报中心报告紧急事态
  15. }
  16. 警铃响起时被调用的方法() {
  17. 向警报中心报告紧急事态
  18. }
  19. 正常通话时被调用的方法(){
  20. 呼叫警报中心的留言电话
  21. }
  22. )

         大家看明白以上两种伪代码之间的区别了吗?也许此时你会说,这**也能用类来表示?

        在没有使用State模式的2.1.1中,我们会先在各个方法里面使用if语句判断现在是白天还是晚上,然后再进行相应的处理。而在使用了State模式的2.1.2中,我们用类来表示白天和晚上。这样,在类的各个方法中就不需要用if语句判断现在是白天还是晚上了。

        总结起来就是,2.1.1是用方法来判断状态,2.1.2是用类来表示状态。那么,大家能够想象出我们是如何从方法的深处挖出被埋的“状态”,将它传递给调用者的吗?

         接下来我们就来看看示例程序:

2.2 各个类之间的关系

        先来看一下所有的类和接口。

        再看看类图:

2.3 State接口 

        state接口是表示金库状态的接口。在state接口中定义了以下事件对应的接口:设置时间、使用金库、按下警铃、正常通话。

        以上这些接口分别对应我们之前在伪代码中编写的“使用金库时被调用的方法”等方法。这些方法的处理都会根据状态不同而不同。可以说,state接口是一个依赖于状态的方法的集合。

  1. public interface State {
  2. //设置时间
  3. public abstract void doClock(Context context, int hour);
  4. //使用金库
  5. public abstract void doUse(Context context);
  6. //按下警铃
  7. public abstract void doAlarm(Context context);
  8. //正常通话
  9. public abstract void doPhone(Context context);
  10. }

2.4 DayState类 

        Daystate类表示白天的状态。

        对于每个表示状态的类,我们都只会生成一个实例。因为如果每次发生状态改变时都生成一个实例的话,太浪费内存和时间了。为此,此处我们使用了Singleton模式

        doUse、doAlarm、doPhone分别是使用金库、按下警铃、正常通话等事件对应的方法。它们的内部实现都是调用Context中的对应方法。请注意,在这些方法中,并没有任何“判断当前状态”的if语句。在编写这些方法时,开发人员都知道“现在是白天的状态”。在State模式中,每个状态都用相应的类来表示,因此无需使用if语句或是switch语句来判断状态。

  1. public class DayState implements State{
  2. //单例模式
  3. private static DayState singleton = new DayState();
  4. private DayState() {}
  5. public static State getInstance() {
  6. return singleton;
  7. }
  8. //切换白天或黑夜
  9. @Override
  10. public void doClock(Context context, int hour) {
  11. if (hour<9 || 17<=hour) {
  12. context.changeState(NightState.getInstance());
  13. }
  14. }
  15. //使用金库
  16. @Override
  17. public void doUse(Context context) {
  18. context.recordLog("使用金库(白天)");
  19. }
  20. //按下警铃
  21. @Override
  22. public void doAlarm(Context context) {
  23. context.callSecurityCenter("按下警铃(白天)");
  24. }
  25. //正常通话
  26. @Override
  27. public void doPhone(Context context) {
  28. context.callSecurityCenter("正常通话(白天)");
  29. }
  30. public String toString() {
  31. return "[ 白天 ]";
  32. }
  33. }

 2.5 NightState类

        NightState类表示晚上的状态。它与DayState类一样,也使用了Singleton模式。Nightstate类的结构与 Daystate完全相同。

  1. public class NightState implements State{
  2. private static NightState singleton = new NightState();
  3. private NightState() {}
  4. public static State getInstance() {
  5. return singleton;
  6. }
  7. @Override
  8. public void doClock(Context context, int hour) {
  9. if (9<=hour && hour<17) {
  10. context.changeState(DayState.getInstance());
  11. }
  12. }
  13. @Override
  14. public void doUse(Context context) {
  15. context.callSecurityCenter("紧急:晚上使用金库!");
  16. }
  17. @Override
  18. public void doAlarm(Context context) {
  19. context.callSecurityCenter("按下警铃(晚上)");
  20. }
  21. @Override
  22. public void doPhone(Context context) {
  23. context.recordLog("晚上的通话录音");
  24. }
  25. public String toString() {
  26. return "[ 晚上 ]";
  27. }
  28. }

2.6 Context接口

        Context接口是负责管理状态和联系警报中心的接口。在介绍SafeFrame类时结合代码再说它实际进行了哪些处理。

  1. public interface Context {
  2. //设置时间
  3. public abstract void setClock(int hour);
  4. //改变状态
  5. public abstract void changeState(State state);
  6. //联系警报中心
  7. public abstract void callSecurityCenter(String msg);
  8. //在警报中心留下记录
  9. public abstract void recordLog(String msg);
  10. }

2.7 SafeFrame类 

        SafeFrame类是使用GUI实现警报系统界面的类( safe有“金库”的意思)。它实现了context接口。

        这里有必要说一下我们对按钮监听器的设置。我们通过调用各个按钮的addActionListener方法来设置监听器。addActionListener方法接收的参数是“当按钮被按下时会被调用的实例”,该实例必须是实现了ActionListener接口的实例。本例中,我们传递的参数是this,即SafeFrame类的实例自身(从代码中可以看到,SafeFrame类的确实现了ActionListener接口)。“当按钮被按下后,监听器会被调用”这种程序结构类似于我们在第17章中学习过的Observer模式

        还有必要说的是,在actionPerformed方法中虽然出现了if语句,但是它是用来判断“按钮的种类”的,而并非用于判断“当前状态”。请不要将我们之前说过“使用State模式可以消除if语句”误认为是“程序中不会出现任何if语句”。

  1. public class SafeFrame extends Frame implements ActionListener, Context {
  2. //GUI控件
  3. private TextField textClock = new TextField(60);
  4. private TextArea textScreen = new TextArea(10, 60);
  5. private Button buttonUse = new Button("使用金库");
  6. private Button buttonAlarm = new Button("按下警铃");
  7. private Button buttonPhone = new Button("正常通话");
  8. private Button buttonExit = new Button("结束");
  9. //当前状态(白天或夜晚)
  10. private State state = DayState.getInstance();
  11. public SafeFrame(String title) {
  12. super(title);
  13. setBackground(Color.lightGray);
  14. setLayout(new BorderLayout());
  15. //配置textClock
  16. add(textClock, BorderLayout.NORTH);
  17. textClock.setEditable(false);
  18. //配置textScreen
  19. add(textScreen, BorderLayout.CENTER);
  20. textScreen.setEditable(false);
  21. //为界面添加按钮
  22. Panel panel = new Panel();
  23. panel.add(buttonUse);
  24. panel.add(buttonAlarm);
  25. panel.add(buttonPhone);
  26. panel.add(buttonExit);
  27. //配置界面
  28. add(panel, BorderLayout.SOUTH);
  29. //显示
  30. pack();
  31. show();
  32. //设置监听器
  33. buttonUse.addActionListener(this);
  34. buttonAlarm.addActionListener(this);
  35. buttonPhone.addActionListener(this);
  36. buttonPhone.addActionListener(this);
  37. }
  38. //按下按钮后该方法会被调用,在该方法中,我们会先判断当前哪个按钮被按下了,然后进行相应的处理
  39. @Override
  40. public void actionPerformed(ActionEvent e) {
  41. System.out.println(e.toString());
  42. if (e.getSource() == buttonUse) {
  43. state.doUse(this);
  44. } else if (e.getSource() == buttonAlarm) {
  45. state.doAlarm(this);
  46. } else if (e.getSource() == buttonPhone) {
  47. state.doPhone(this);
  48. } else if (e.getSource() == buttonExit) {
  49. System.exit(0);
  50. } else {
  51. System.out.println("?");
  52. }
  53. }
  54. //设置时间
  55. @Override
  56. public void setClock(int hour) {
  57. String clockstring = "现在时间是";
  58. if (hour < 10) {
  59. clockstring += "0" + hour + ":00";
  60. } else {
  61. clockstring += hour + ":00";
  62. }
  63. System.out.println(clockstring);
  64. textClock.setText(clockstring);
  65. state.doClock(this, hour);
  66. }
  67. //改变状态
  68. @Override
  69. public void changeState(State state) {
  70. System.out.println("从" + this.state + "状态变为了" + state + "状态。");
  71. this.state = state;
  72. }
  73. //联系报警中心
  74. @Override
  75. public void callSecurityCenter(String msg) {
  76. textScreen.append("call!" + msg + "\n");
  77. }
  78. //在报警中心留下记录
  79. @Override
  80. public void recordLog(String msg) {
  81. textScreen.append("record ..." + msg + "\n");
  82. }
  83. }

 2.8 用于测试的Main类

        Main类生成了一个safeFrame类的实例并每秒调用一次setClock方法,对该实例设置一次时间。这相当于在真实世界中经过了一小时。 

  1. public class Main {
  2. public static void main(String[] args) {
  3. SafeFrame frame = new SafeFrame("State Sample");
  4. while (true) {
  5. for (int hour = 0; hour < 24; hour++) {
  6. //设置时间
  7. frame.setClock(hour);
  8. try {
  9. Thread.sleep(1000);
  10. } catch (InterruptedException e) {
  11. }
  12. }
  13. }
  14. }
  15. }

         程序的时序图:

三、拓展思路的要点 

3.1 分而治之

        在编程时,我们经常会使用分而治之的方针。它非常适用于大规模的复杂处理。当遇到庞大且复杂的问题,不能用一般的方法解决时,我们会先将该问题分解为多个小问题。如果还是不能解决这些小问题,我们会将它们继续划分为更小的问题,直至可以解决它们为止。分而治之,简单而言就是将一个复杂的大问题分解为多个小问题然后逐个解决。

        在State模式中,我们用类来表示状态,并为每一种具体的状态都定义一个相应的类。这样,问题就被分解了。在本章的金库警报系统的示例程序中,只有“白天”和“晚上”两个状态,可能大家对此感受不深,但是当状态非常多的时候,State模式的优势就会非常明显了。

        请大家再回忆一下前面的伪代码。在不使用State模式时,我们需要使用条件分支语句判断当前的状态,然后进行相应的处理。状态越多,条件分支就会越多。而且,我们必须在所有的事件处理方法中都编写这些条件分支语句。

        State模式用类表示系统的“状态”,并以此将复杂的程序分解开来。

3.2 依赖于状态的处理

        Main类会调用SafeFrame类的setClock方法,告诉setClock方法“请设置时间”。在setClock方法中,会像下面这样将处理委托给State类:state.doClock (this, hour) 。

        也就是说,我们将设置时间的处理看作是“依赖于状态的处理”。

        当然,不只是 doClock方法。在State接口中声明的所有方法都是“依赖于状态的处理”,都是“状态不同处理也不同”。这虽然看似理所当然,不过却需要我们特别注意。

        在State模式中,我们应该如何编程,以实现“依赖于状态的处理”呢?总结起来有如下两点。

  • 定义接口,声明抽象方法
  • 定义多个类,实现具体方法

        这就是State模式中的“依赖于状态的处理”的实现方法。

        这里故意将上面两点说得很笼统,但是,如果大家在读完这两点之后会点头表示赞同,那就意味着大家完全理解了State模式以及接口与类之间的关系。

3.3 应当是谁来管理状态迁移

        用类来表示状态,将依赖于状态的处理分散在每个ConcreteState角色中,这是一种非常好的解决办法。

        不过,在使用State模式时需要注意应当是谁来管理状态迁移。

        在示例程序中,扮演Context 角色的SafeFrame类实现了实际进行状态迁移的changeState方法。但是,实际调用该方法的却是扮演ConcreteState角色的 DayState类和NightState类。也就是说,在示例程序中,我们将“状态迁移”看作是“依赖于状态的处理”。这种处理方式既有优点也有缺点。

        优点是这种处理方式将“什么时候从一个状态迁移到其他状态”的信息集中在了一个类中。也就是说,当我们想知道“什么时候会从 DayState类变化为其他状态”时,只需要阅读DayState类的代码就可以了。

        缺点是“每个ConcreteState角色都需要知道其他ConcreteState角色”。例如,DayState类的doClock方法就使用了Nightstate类。这样,如果以后发生需求变更,需要删除NightState类时,就必须要相应地修改Daystate类的代码。将状态迁移交给ConcreteState角色后,每个ConcreteState角色都需要或多或少地知道其他ConcreteState角色。也就是说,将状态迁移交给ConcreteState角色后,各个类之间的依赖关系就会加强。

        我们也可以不使用示例程序中的做法,而是将所有的状态迁移交给扮演Context角色的SafeFrame类来负责。有时,使用这种解决方法可以提高ConcreteState角色的独立性,程序的整体结构也会更加清晰。不过这样做的话,Context角色就必须要知道“所有的ConcreteState 角色”。在这种情况下,我们可以使用Mediator模式

        当然,还可以不用State模式,而是用状态迁移表来设计程序。所谓状态迁移表是可以根据“输入和内部状态”得到“输出和下一个状态”的一览表。当状态迁移遵循一定的规则时,使用状态迁移表非常有效。

        此外,当状态数过多时,可以用程序来生成代码而不是手写代码。

3.4 不会自相矛盾

        如果不使用State模式,我们需要使用多个变量的值的集合来表示系统的状态。这时,必须十
分小心,注意不要让变量的值之间互相矛盾。而在State模式中,是用类来表示状态的。这样,我们就只需要一个表示系统状态的变量即可。

        在示例程序中,SafeFrame 类的state字段就是这个变量,它决定了系统的状态。因此,不会存在自相矛盾的状态。

3.5 易于增加新的状态

        在State模式中增加新的状态是非常简单的。以示例程序来说,编写一个XXXState类,让它实现State接口,然后实现一些所需的方法就可以了。当然,在修改状态迁移部分的代码时,还是需要仔细一点的。因为状态迁移的部分正是与其他ConcreteState角色相关联的部分

        但是,在State模式中增加其他“依赖于状态的处理”是很困难的。这是因为我们需要在State接口中增加新的方法,并在所有的ConcreteState 角色中都实现这个方法。

        虽说很困难,但是好在我们绝对不会忘记实现这个方法。假设我们现在在State接口中增加了一个doYYY方法,而忘记了在Daystate类和Nightstate类中实现这个方法,那么编译器在编译代码时就会报错,告诉我们存在还没有实现的方法。

        如果不使用State模式,就必须用if语句判断状态。这样就很难在编译代码时检测出“忘记实现方法”这种错误了(在运行时检测出问题并不难。我们只要事先在每个方法内部都加上一段“当检测到没有考虑到的状态时就报错”的代码即可)。

3.6 实例的多面性

        请注意SafeFrame类中的以下两条语句。

  •         safeFrame类的构造函数中的         buttonUse .addActionListener (this) ;
  •         actionPerformed方法中的              state.doUse (this) ;

        这两条语句中都有this。那么这个this到底是什么呢?当然,它们都是safeFrame类的实例。由于在示例程序中只生成了一个safeFrame 的实例,因此这两个this其实是同一个对象。

        不过,在addActionListener方法中和doUse方法中,对this的使用方式是不一样的。

        向addActionListener方法传递this时,该实例会被当作“实现了ActionListener接口的类的实例”来使用。这是因为addActionListener方法的参数类型是ActionListener类型。在addActionListener方法中会用到的方法也都是在ActionListener接口中定义了的方法。至于这个参数是否是safeFrame类的实例并不重要。

        向doUse方法传递this时,该实例会被当作“实现了Context接口的类的实例”来使用。这是因为douse方法的参数类型是context类型。在doUse方法中会用到的方法也都是在Context接口中定义了的方法(大家只要再回顾一下 Daystate类和Nightstate类的doUse方法就会明白了)。

        大家一定要透彻理解此处的实例的多面性。

四、相关的设计模式

4.1 Singleton模式

        Singleton模式常常会出现在ConcreteState角色中。在示例程序中,我们就使用了Singleton模式。这是因为在表示状态的类中并没有定义任何实例字段(即表示实例的状态的字段)。

4.2 Flyweight模式

        在表示状态的类中并没有定义任何实例字段。因此,有时我们可以使用Flyweight模式在多个Context角色之间共享ConcreteState角色。

五、思考题

5.1、

题目

        本来应当将Context定义为抽象类而非接口,然后让Context类持有state字段,这样更符合State模式的设计思想。但是在示例程序中我们并没有这么做,而是将Context角色定义为context 接口,让safeFrame类持有state字段,请问这是为什么呢?

答案

        因为在Java中只能单一继承,所以如果将Context角色定义为类,那么由于safeFrame类已经是Frame类的子类了,它将无法再继承context类。

        不过,如果另外编写一个Context类的子类,并将它的实例保存在SafeFrame类的字段中,那么通过将处理委托给这个实例是可以实现习题中的需求的。
 

5.2、

题目:如果要对示例程序中的“白天”和“晚上”的时间区间做如下变更,请问应该怎样修改程序呢?

答案: 

        需要修改Daystate类(代码清单19-4 )以及Nightstate类(代码清单19-5)的doclock方法。

        如果事先在SafeFrame类中定义一个isDay方法和一个isNight方法,让外部可以判断当前究竞是白天还是晚上,那么就可以将白天和晚上的具体时间范围限制在safeFrame类内部。这样修改后,当时间范围发生变更时,只需要修改safeFrame类即可。

5.3、

题目:请在示例程序中增加一个新的“午餐时间(12:00~12:59)”状态。在午餐时间使用金库的话,会向警报中心通知紧急情况;在午餐时间按下警铃的话,会向警报中心通知紧急情况;在午餐时间使用电话的话,会呼叫警报中心的留言电话。
 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/你好赵伟/article/detail/63425
推荐阅读
相关标签
  

闽ICP备14008679号