设计模式学习(五):State状态模式
创始人
2024-05-11 11:21:02
0

一、什么是State模式

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

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

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

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

 

二、State模式示例程序

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

        功能表:

        结构图:

  

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

 

2.1 伪代码

2.1.1 不使用State模式的伪代码

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

使用金库时被调用的方法() {if(白天) {向警报中心报告使用记录} else if(晚上) {向警报中心报告紧急事态}
}警铃响起时被调用的方法() {像警报中心报告紧急事态
}正常通话时被调用的方法() {if(白天) {呼叫警报中心} else if(晚上) {呼叫警报中心的留言电话}
}

2.1.2 使用State模式的伪代码 

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

表示白天的状态的类{使用金库时被调用的方法() {向警报中心报告使用记录} 警铃响起时被调用的方法() {向警报中心报告紧急事态}正常通话时被调用的方法(){呼叫警报中心}
}表示晚上的状态的类{使用金库时被调用的方法() {向警报中心报告紧急事态}警铃响起时被调用的方法() {向警报中心报告紧急事态}正常通话时被调用的方法(){呼叫警报中心的留言电话}
)

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

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

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

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

2.2 各个类之间的关系

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

 

        再看看类图:

 

2.3 State接口 

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

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

public interface State {//设置时间public abstract void doClock(Context context, int hour);//使用金库public abstract void doUse(Context context);//按下警铃public abstract void doAlarm(Context context);//正常通话public abstract void doPhone(Context context);
}

2.4 DayState类 

        Daystate类表示白天的状态。

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

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

public class DayState implements State{//单例模式private static DayState singleton = new DayState();private DayState() {}public static State getInstance() {return singleton;}//切换白天或黑夜@Overridepublic void doClock(Context context, int hour) {if (hour<9 || 17<=hour) {context.changeState(NightState.getInstance());}}//使用金库@Overridepublic void doUse(Context context) {context.recordLog("使用金库(白天)");}//按下警铃@Overridepublic void doAlarm(Context context) {context.callSecurityCenter("按下警铃(白天)");}//正常通话@Overridepublic void doPhone(Context context) {context.callSecurityCenter("正常通话(白天)");}public String toString() {return "[ 白天 ]";}
}

 2.5 NightState类

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

public class NightState implements State{private static NightState singleton = new NightState();private NightState() {}public static State getInstance() {return singleton;}@Overridepublic void doClock(Context context, int hour) {if (9<=hour && hour<17) {context.changeState(DayState.getInstance());}}@Overridepublic void doUse(Context context) {context.callSecurityCenter("紧急:晚上使用金库!");}@Overridepublic void doAlarm(Context context) {context.callSecurityCenter("按下警铃(晚上)");}@Overridepublic void doPhone(Context context) {context.recordLog("晚上的通话录音");}public String toString() {return "[ 晚上 ]";}
}

2.6 Context接口

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

public interface Context {//设置时间public abstract void setClock(int hour);//改变状态public abstract void changeState(State state);//联系警报中心public abstract void callSecurityCenter(String msg);//在警报中心留下记录public abstract void recordLog(String msg);
}

2.7 SafeFrame类 

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

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

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

public class SafeFrame extends Frame implements ActionListener, Context {//GUI控件private TextField textClock = new TextField(60);private TextArea textScreen = new TextArea(10, 60);private Button buttonUse = new Button("使用金库");private Button buttonAlarm = new Button("按下警铃");private Button buttonPhone = new Button("正常通话");private Button buttonExit = new Button("结束");//当前状态(白天或夜晚)private State state = DayState.getInstance();public SafeFrame(String title) {super(title);setBackground(Color.lightGray);setLayout(new BorderLayout());//配置textClockadd(textClock, BorderLayout.NORTH);textClock.setEditable(false);//配置textScreenadd(textScreen, BorderLayout.CENTER);textScreen.setEditable(false);//为界面添加按钮Panel panel = new Panel();panel.add(buttonUse);panel.add(buttonAlarm);panel.add(buttonPhone);panel.add(buttonExit);//配置界面add(panel, BorderLayout.SOUTH);//显示pack();show();//设置监听器buttonUse.addActionListener(this);buttonAlarm.addActionListener(this);buttonPhone.addActionListener(this);buttonPhone.addActionListener(this);}//按下按钮后该方法会被调用,在该方法中,我们会先判断当前哪个按钮被按下了,然后进行相应的处理@Overridepublic void actionPerformed(ActionEvent e) {System.out.println(e.toString());if (e.getSource() == buttonUse) {state.doUse(this);} else if (e.getSource() == buttonAlarm) {state.doAlarm(this);} else if (e.getSource() == buttonPhone) {state.doPhone(this);} else if (e.getSource() == buttonExit) {System.exit(0);} else {System.out.println("?");}}//设置时间@Overridepublic void setClock(int hour) {String clockstring = "现在时间是";if (hour < 10) {clockstring += "0" + hour + ":00";} else {clockstring += hour + ":00";}System.out.println(clockstring);textClock.setText(clockstring);state.doClock(this, hour);}//改变状态@Overridepublic void changeState(State state) {System.out.println("从" + this.state + "状态变为了" + state + "状态。");this.state = state;}//联系报警中心@Overridepublic void callSecurityCenter(String msg) {textScreen.append("call!" + msg + "\n");}//在报警中心留下记录@Overridepublic void recordLog(String msg) {textScreen.append("record ..." + msg + "\n");}
}

 2.8 用于测试的Main类

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

public class Main {public static void main(String[] args) {SafeFrame frame = new SafeFrame("State Sample");while (true) {for (int hour = 0; hour < 24; hour++) {//设置时间frame.setClock(hour);try {Thread.sleep(1000);} catch (InterruptedException e) {}}}}
}

         程序的时序图:

三、拓展思路的要点 

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模式(第20章)

        在表示状态的类中并没有定义任何实例字段。因此,有时我们可以使用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)”状态。在午餐时间使用金库的话,会向警报中心通知紧急情况;在午餐时间按下警铃的话,会向警报中心通知紧急情况;在午餐时间使用电话的话,会呼叫警报中心的留言电话。
 

相关内容

热门资讯

【NI Multisim 14...   目录 序言 一、工具栏 🍊1.“标准”工具栏 🍊 2.视图工具...
银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
Android|无法访问或保存... 这个问题可能是由于权限设置不正确导致的。您需要在应用程序清单文件中添加以下代码来请求适当的权限:此外...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
AsusVivobook无法开... 首先,我们可以尝试重置BIOS(Basic Input/Output System)来解决这个问题。...
ASM贪吃蛇游戏-解决错误的问... 要解决ASM贪吃蛇游戏中的错误问题,你可以按照以下步骤进行:首先,确定错误的具体表现和问题所在。在贪...
月入8000+的steam搬砖... 大家好,我是阿阳 今天要给大家介绍的是 steam 游戏搬砖项目,目前...