赞
踩
目录
在面向对象编程中,是用类表示对象的。也就是说,程序的设计者需要考虑用类来表示什么东西。类对应的东西可能存在于真实世界中,也可能不存在于真实世界中。
在State模式中,我们用类来表示状态。在现实世界中,我们会考虑各种东西的“状态”,但是几乎不会将状态当作“东西”看待。因此,可能大家很难理解“用类来表示状态”的意思。
在本文中,我们将要学习用类来表示状态的方法。以类来表示状态后,我们就能通过切换类来方便地改变对象的状态。当需要增加新的状态时,如何修改代码这个问题也会很明确。
用一句话来概括:State模式就是用类来表示状态。
这里我们来看一个警戒状态每小时会改变一次的警报系统。
功能表:
结构图:
下面我们来用程序实现这个金库警报系统。
刚接触到这样的需求,你会怎样设计代码呢?如果是我,我可能会这样设计:
- 使用金库时被调用的方法() {
- if(白天) {
- 向警报中心报告使用记录
- } else if(晚上) {
- 向警报中心报告紧急事态
- }
- }
-
- 警铃响起时被调用的方法() {
- 像警报中心报告紧急事态
- }
-
- 正常通话时被调用的方法() {
- if(白天) {
- 呼叫警报中心
- } else if(晚上) {
- 呼叫警报中心的留言电话
- }
- }
并不能说上面的代码有什么不对,只是我们今天要讲的State模式是完全不同的角度,咱们一起来看看他们的区别在哪,state模式的好处在哪。
表示白天的状态的类{ 使用金库时被调用的方法() { 向警报中心报告使用记录 } 警铃响起时被调用的方法() { 向警报中心报告紧急事态 } 正常通话时被调用的方法(){ 呼叫警报中心 } } 表示晚上的状态的类{ 使用金库时被调用的方法() { 向警报中心报告紧急事态 } 警铃响起时被调用的方法() { 向警报中心报告紧急事态 } 正常通话时被调用的方法(){ 呼叫警报中心的留言电话 } )
大家看明白以上两种伪代码之间的区别了吗?也许此时你会说,这**也能用类来表示?
在没有使用State模式的2.1.1中,我们会先在各个方法里面使用if语句判断现在是白天还是晚上,然后再进行相应的处理。而在使用了State模式的2.1.2中,我们用类来表示白天和晚上。这样,在类的各个方法中就不需要用if语句判断现在是白天还是晚上了。
总结起来就是,2.1.1是用方法来判断状态,2.1.2是用类来表示状态。那么,大家能够想象出我们是如何从方法的深处挖出被埋的“状态”,将它传递给调用者的吗?
接下来我们就来看看示例程序:
先来看一下所有的类和接口。
再看看类图:
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);
- }
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;
- }
-
- //切换白天或黑夜
- @Override
- public void doClock(Context context, int hour) {
- if (hour<9 || 17<=hour) {
- context.changeState(NightState.getInstance());
- }
- }
-
- //使用金库
- @Override
- public void doUse(Context context) {
- context.recordLog("使用金库(白天)");
- }
-
- //按下警铃
- @Override
- public void doAlarm(Context context) {
- context.callSecurityCenter("按下警铃(白天)");
- }
-
- //正常通话
- @Override
- public void doPhone(Context context) {
- context.callSecurityCenter("正常通话(白天)");
- }
-
- public String toString() {
- return "[ 白天 ]";
- }
- }
NightState类表示晚上的状态。它与DayState类一样,也使用了Singleton模式。Nightstate类的结构与 Daystate完全相同。
- public class NightState implements State{
-
- private static NightState singleton = new NightState();
-
- private NightState() {}
-
- public static State getInstance() {
- return singleton;
- }
-
- @Override
- public void doClock(Context context, int hour) {
- if (9<=hour && hour<17) {
- context.changeState(DayState.getInstance());
- }
- }
-
- @Override
- public void doUse(Context context) {
- context.callSecurityCenter("紧急:晚上使用金库!");
- }
-
- @Override
- public void doAlarm(Context context) {
- context.callSecurityCenter("按下警铃(晚上)");
- }
-
- @Override
- public void doPhone(Context context) {
- context.recordLog("晚上的通话录音");
- }
-
- public String toString() {
- return "[ 晚上 ]";
- }
- }
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);
- }
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());
- //配置textClock
- add(textClock, BorderLayout.NORTH);
- textClock.setEditable(false);
- //配置textScreen
- add(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);
- }
-
- //按下按钮后该方法会被调用,在该方法中,我们会先判断当前哪个按钮被按下了,然后进行相应的处理
- @Override
- public 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("?");
- }
- }
-
- //设置时间
- @Override
- public 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);
- }
-
- //改变状态
- @Override
- public void changeState(State state) {
- System.out.println("从" + this.state + "状态变为了" + state + "状态。");
- this.state = state;
- }
-
- //联系报警中心
- @Override
- public void callSecurityCenter(String msg) {
- textScreen.append("call!" + msg + "\n");
- }
-
- //在报警中心留下记录
- @Override
- public void recordLog(String msg) {
- textScreen.append("record ..." + msg + "\n");
- }
- }
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) {
- }
- }
- }
- }
- }
程序的时序图:
在编程时,我们经常会使用分而治之的方针。它非常适用于大规模的复杂处理。当遇到庞大且复杂的问题,不能用一般的方法解决时,我们会先将该问题分解为多个小问题。如果还是不能解决这些小问题,我们会将它们继续划分为更小的问题,直至可以解决它们为止。分而治之,简单而言就是将一个复杂的大问题分解为多个小问题然后逐个解决。
在State模式中,我们用类来表示状态,并为每一种具体的状态都定义一个相应的类。这样,问题就被分解了。在本章的金库警报系统的示例程序中,只有“白天”和“晚上”两个状态,可能大家对此感受不深,但是当状态非常多的时候,State模式的优势就会非常明显了。
请大家再回忆一下前面的伪代码。在不使用State模式时,我们需要使用条件分支语句判断当前的状态,然后进行相应的处理。状态越多,条件分支就会越多。而且,我们必须在所有的事件处理方法中都编写这些条件分支语句。
State模式用类表示系统的“状态”,并以此将复杂的程序分解开来。
Main类会调用SafeFrame类的setClock方法,告诉setClock方法“请设置时间”。在setClock方法中,会像下面这样将处理委托给State类:state.doClock (this, hour) 。
也就是说,我们将设置时间的处理看作是“依赖于状态的处理”。
当然,不只是 doClock方法。在State接口中声明的所有方法都是“依赖于状态的处理”,都是“状态不同处理也不同”。这虽然看似理所当然,不过却需要我们特别注意。
在State模式中,我们应该如何编程,以实现“依赖于状态的处理”呢?总结起来有如下两点。
这就是State模式中的“依赖于状态的处理”的实现方法。
这里故意将上面两点说得很笼统,但是,如果大家在读完这两点之后会点头表示赞同,那就意味着大家完全理解了State模式以及接口与类之间的关系。
用类来表示状态,将依赖于状态的处理分散在每个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模式,而是用状态迁移表来设计程序。所谓状态迁移表是可以根据“输入和内部状态”得到“输出和下一个状态”的一览表。当状态迁移遵循一定的规则时,使用状态迁移表非常有效。
此外,当状态数过多时,可以用程序来生成代码而不是手写代码。
如果不使用State模式,我们需要使用多个变量的值的集合来表示系统的状态。这时,必须十
分小心,注意不要让变量的值之间互相矛盾。而在State模式中,是用类来表示状态的。这样,我们就只需要一个表示系统状态的变量即可。
在示例程序中,SafeFrame 类的state字段就是这个变量,它决定了系统的状态。因此,不会存在自相矛盾的状态。
在State模式中增加新的状态是非常简单的。以示例程序来说,编写一个XXXState类,让它实现State接口,然后实现一些所需的方法就可以了。当然,在修改状态迁移部分的代码时,还是需要仔细一点的。因为状态迁移的部分正是与其他ConcreteState角色相关联的部分。
但是,在State模式中增加其他“依赖于状态的处理”是很困难的。这是因为我们需要在State接口中增加新的方法,并在所有的ConcreteState 角色中都实现这个方法。
虽说很困难,但是好在我们绝对不会忘记实现这个方法。假设我们现在在State接口中增加了一个doYYY方法,而忘记了在Daystate类和Nightstate类中实现这个方法,那么编译器在编译代码时就会报错,告诉我们存在还没有实现的方法。
如果不使用State模式,就必须用if语句判断状态。这样就很难在编译代码时检测出“忘记实现方法”这种错误了(在运行时检测出问题并不难。我们只要事先在每个方法内部都加上一段“当检测到没有考虑到的状态时就报错”的代码即可)。
请注意SafeFrame类中的以下两条语句。
这两条语句中都有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方法就会明白了)。
大家一定要透彻理解此处的实例的多面性。
Singleton模式常常会出现在ConcreteState角色中。在示例程序中,我们就使用了Singleton模式。这是因为在表示状态的类中并没有定义任何实例字段(即表示实例的状态的字段)。
在表示状态的类中并没有定义任何实例字段。因此,有时我们可以使用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)”状态。在午餐时间使用金库的话,会向警报中心通知紧急情况;在午餐时间按下警铃的话,会向警报中心通知紧急情况;在午餐时间使用电话的话,会呼叫警报中心的留言电话。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。