设计模式(十九)State模式
在面向对象编程中,是用类表示对象的。也就是说,程序的设计者需要考虑用类来表示什么东西。类对应的东西可能存在于真实世界中,也可能不存在于真实世界中。对于后者,可能有人看到代码后会感到吃惊:这些东西居然也可以是类啊。
在State模式中,用类来表示状态。用类来表示状态后,就能通过切换类方便地改变对象的状态,当需要增加新的状态时,如何修改代码这个问题也会很明确。
示例程序的类图如上图所示。
1 package bigjunoba.bjtu.state; 2 3 public interface Context { 4 public abstract void setClock(int hour); // 设置时间 5 public abstract void changeState(State state); // 改变状态 6 public abstract void callSecurityCenter(String msg); // 联系警报中心 7 public abstract void recordLog(String msg); // 在警报中心留下记录 8 }
Context接口是负责管理状态和联系警报中心的接口。
package bigjunoba.bjtu.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); // 正常通话 }
State接口是表示金库状态的接口。这些方法接收的参数Context是管理状态的接口。
1 package bigjunoba.bjtu.state; 2 3 public class DayState implements State { 4 private static DayState singleton = new DayState(); 5 6 private DayState() { // 构造函数的可见性是private 7 } 8 9 public static State getInstance() { // 获取唯一实例 10 return singleton; 11 } 12 13 public void doClock(Context context, int hour) { // 设置时间 14 if (hour < 9 || 17 <= hour) { 15 context.changeState(NightState.getInstance()); 16 } 17 } 18 19 public void doUse(Context context) { // 使用金库 20 context.recordLog("使用金库(白天)"); 21 } 22 23 public void doAlarm(Context context) { // 按下警铃 24 context.callSecurityCenter("按下警铃(白天)"); 25 } 26 27 public void doPhone(Context context) { // 正常通话 28 context.callSecurityCenter("正常通话(白天)"); 29 } 30 31 public String toString() { // 显示表示类的文字 32 return "[白天]"; 33 } 34 }
DayState类表示白天的状态。对于每个表示状态的类,都只生成一个实例,因为如果每次发生状态改变时都生成一个实例的话,太浪费内存和时间了。因此,使用了Singleton模式。doClock方法是用于设置时间的方法。如果接收到的参数表示晚上的时间,就会切换到夜间状态,即发生状态变化,用程序表现就是获取夜晚状态的类的实例,然后通过changeState方法实现。
1 package bigjunoba.bjtu.state; 2 3 import java.awt.Frame; 4 import java.awt.Label; 5 import java.awt.Color; 6 import java.awt.Button; 7 import java.awt.TextField; 8 import java.awt.TextArea; 9 import java.awt.Panel; 10 import java.awt.BorderLayout; 11 import java.awt.event.ActionListener; 12 13 import javax.swing.JButton; 14 15 import java.awt.event.ActionEvent; 16 17 public class SafeFrame extends Frame implements ActionListener, Context { 18 private TextField textClock = new TextField(60); // 显示当前时间 19 private TextArea textScreen = new TextArea(10, 60); // 显示警报中心的记录 20 private JButton buttonUse = new JButton("使用金库"); // 金库使用按钮 21 private JButton buttonAlarm = new JButton("按下警铃"); // 按下警铃按钮 22 private JButton buttonPhone = new JButton("正常通话"); // 正常通话按钮 23 private JButton buttonExit = new JButton("结束"); // 结束按钮 24 25 private State state = DayState.getInstance(); // 当前的状态 26 27 // 构造函数 28 public SafeFrame(String title) { 29 super(title); 30 setBackground(Color.lightGray); 31 setLayout(new BorderLayout()); 32 // 配置textClock 33 add(textClock, BorderLayout.NORTH); 34 textClock.setEditable(false); 35 // 配置textScreen 36 add(textScreen, BorderLayout.CENTER); 37 textScreen.setEditable(false); 38 // 为界面添加按钮 39 Panel panel = new Panel(); 40 panel.add(buttonUse); 41 panel.add(buttonAlarm); 42 panel.add(buttonPhone); 43 panel.add(buttonExit); 44 // 配置界面 45 add(panel, BorderLayout.SOUTH); 46 // 显示 47 pack(); 48 show(); 49 // 设置监听器 50 buttonUse.addActionListener(this); 51 buttonAlarm.addActionListener(this); 52 buttonPhone.addActionListener(this); 53 buttonExit.addActionListener(this); 54 } 55 56 // 按钮被按下后该方法会被调用 57 public void actionPerformed(ActionEvent e) { 58 System.out.println(e.toString()); 59 if (e.getSource() == buttonUse) { // 金库使用按钮 60 state.doUse(this); 61 } else if (e.getSource() == buttonAlarm) { // 按下警铃按钮 62 state.doAlarm(this); 63 } else if (e.getSource() == buttonPhone) { // 正常通话按钮 64 state.doPhone(this); 65 } else if (e.getSource() == buttonExit) { // 结束按钮 66 System.exit(0); 67 } else { 68 System.out.println("?"); 69 } 70 } 71 72 // 设置时间 73 public void setClock(int hour) { 74 String clockstring = "现在时间是"; 75 if (hour < 10) { 76 clockstring += "0" + hour + ":00"; 77 } else { 78 clockstring += hour + ":00"; 79 } 80 System.out.println(clockstring); 81 textClock.setText(clockstring); 82 state.doClock(this, hour); 83 } 84 85 // 改变状态 86 public void changeState(State state) { 87 System.out.println("从" + this.state + "状態变为了" + state + "状态。"); 88 this.state = state; 89 } 90 91 // 联系警报中心 92 public void callSecurityCenter(String msg) { 93 textScreen.append("call! " + msg + "\n"); 94 } 95 96 // 在警报中心留下记录 97 public void recordLog(String msg) { 98 textScreen.append("record ... " + msg + "\n"); 99 } 100 }
SafeFrame类是使用GUI实现警报系统界面的类。这里要注意的是,没有先去判断时间是白天还是晚上,也没有判断金库的状态,如果按下按钮就立即执行对应的方法。changeState方法会调用白天状态和晚上状态两个类,当状态发生迁移时,实际改变状态的是this.state = state;这句话就是给代表状态的字段赋予表示当前状态的类的实例,就相当于进行了状态迁移。
上图为状态改变前后doUse方法的调用流程。一开始调用DayState类的doUse方法,当changeState后,变为了调用NightState类的doUse方法。
1 package bigjunoba.bjtu.state; 2 3 public class Main { 4 public static void main(String[] args) { 5 SafeFrame frame = new SafeFrame("State Sample"); 6 while (true) { 7 for (int hour = 0; hour < 24; hour++) { 8 frame.setClock(hour); // 设置时间 9 try { 10 Thread.sleep(1000); 11 } catch (InterruptedException e) { 12 } 13 } 14 } 15 } 16 }
Main类作为测试类,很容易理解,就不做解释了。
实际效果图如上。
State模式的类图如上图。
State模式扩展知识:
1.分而治之。在大规模的复杂处理时,不能用一般的方法解决时,会先将多个问题分解为多个小问题来解决。
2.在State接口中声明的所有方法都是“依赖于状态的处理”,都是“状态不同处理也不同”。实现起来总结为:1.定义接口,声明抽象方法。 2.定义多个类,实现具体方法。
3.实例的多面性