• 欢迎访问 winrains 的个人网站!
  • 本网站主要从互联网整理和收集了与Java、网络安全、Linux等技术相关的文章,供学习和研究使用。如有侵权,请留言告知,谢谢!

图解设计模式(19):State模式(用来表示状态)

设计模式 winrains 1年前 (2019-09-23) 48次浏览

在State模式中,用类来表示状态。以类来表示状态后,我们就能通过切换类来方便地改变对象的状态。

1 State模式中的角色

  • State(状态)

表示状态,定义了根据不同状态进行不同处理的接口。该接口是那些处理内容依赖于状态的方法的集合。在示例中,对应State接口。

  • ConcreteState(具体状态)

表示各个具体的状态,它实现了State接口。在示例中,对应DayState类和NightState类。

  • Context(状态、前后关系、上下文)

持有表示当前状态的ConcreteState角色。此外,它还定义了提供外部调用者使用State模式的接口。在示例中,对应Context接口和SafeFrame类。
在示例中,Context角色的作用被Context接口和SafeFrame类分担了。具体而言,Context接口定义了供外部调用者使用State模式的接口,而SafeFrame类则持有表示当前状态的ConcreteState角色。

2 State模式的类图

3 示例程序

该示例程序是一个警戒状态每小时会改变一次的警报系统。

3.1 两种实现方式比较

3.1.1 不使用State模式的伪代码

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

3.1.2 使用State模式的伪代码

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

3.1.3 两种方式比较

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

3.2 类和接口一览表

名字 说明
State 表示金库状态的接口
DayState 表示“白天”状态的类。实现了State接口
NightState 表示“晚上”状态的类。实现了State接口
Context 表示管理金库状态,并与警报中心联系的接口
SafeFrame 实现了Context接口,在它内部持有按钮和画面显示等UI信息
Main 测试程序行为的类

3.3 类图

3.4 示例代码

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类

表示白天的状态。
doClock是用于设置时间的方法。如果接收到的参数表示晚上的时间,就会切换到夜间状态,即发生状态迁移。调用Context接口的changeState方法改变状态。

public class DayState implements State {
    private static DayState singleton = new DayState();
    private DayState() {                                // 构造函数的可见性是private
    }
    public static State getInstance() {                 // 获取唯一实例
        return singleton;
    }
    public void doClock(Context context, int hour) {    // 设置时间
        if (hour < 9 || 17 <= hour) {
            context.changeState(NightState.getInstance());
        }
    }
    public void doUse(Context context) {                // 使用金库
        context.recordLog("使用金库(白天)");
    }
    public void doAlarm(Context context) {              // 按下警铃
        context.callSecurityCenter("按下警铃(白天)");
    }
    public void doPhone(Context context) {              // 正常通话
        context.callSecurityCenter("正常通话(白天)");
    }
    public String toString() {                          // 显示表示类的文字
        return "[白天]";
    }
}

NightState类

表示晚上的状态,与DayState类似。

public class NightState implements State {
    private static NightState singleton = new NightState();
    private NightState() {                              // 构造函数的可见性是private
    }
    public static State getInstance() {                 // 获取唯一实例
        return singleton;
    }
    public void doClock(Context context, int hour) {    // 设置时间
        if (9 <= hour && hour < 17) {
            context.changeState(DayState.getInstance());
        }
    }
    public void doUse(Context context) {                // 使用金库
        context.callSecurityCenter("紧急:晚上使用金库!");
    }
    public void doAlarm(Context context) {              // 按下警铃
        context.callSecurityCenter("按下警铃(晚上)");
    }
    public void doPhone(Context context) {              // 正常通话
        context.recordLog("晚上的通话录音");
    }
    public String toString() {                          // 显示表示类的文字
        return "[晚上]";
    }
}

Context接口

负责管理状态和联系警报中心的接口。

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实现警报系统界面的类,实现了Context接口。
处理的内容对State模式非常重要。例如,当金库使用按钮被按下时,以下语句会被执行:
state.doUse(this);
我们并没有先去判断当前时间是白天还是晚上,也没有判断金库的状态,而是直接调用了doUse方法,这就是State模式的特点。
setClock方法中设置了当前时间,调用了如下语句:
state.doClock(this, hour);
该语句会进行当前状态下相应的处理,可能会发生状态迁移。
changeState方法会调用DayState类和NightState类。当发生状态迁移时,该方法会被调用。实际改变状态的是如下语句:
this.state = state;
给代表状态的字段赋予表示当前状态的类的实例,就相当于进行状态迁移。

import java.awt.Frame;
import java.awt.Label;
import java.awt.Color;
import java.awt.Button;
import java.awt.TextField;
import java.awt.TextArea;
import java.awt.Panel;
import java.awt.BorderLayout;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
public class SafeFrame extends Frame implements ActionListener, Context {
    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);
        buttonExit.addActionListener(this);
    }
    // 按钮被按下后该方法会被调用
    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("?");
        }
    }
    // 设置时间
    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);
    }
    // 改变状态
    public void changeState(State state) {
        System.out.println("从" + this.state + "状態变为了" + state + "状态。");
        this.state = state;
    }
    // 联系警报中心
    public void callSecurityCenter(String msg) {
        textScreen.append("call! " + msg + "\n");
    }
    // 在警报中心留下记录
    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) {
                }
            }
        }
    }
}

运行结果

现在时间是00:00
从[白天]状態变为了[晚上]状态。
现在时间是01:00
现在时间是02:00
现在时间是03:00
现在时间是04:00
现在时间是05:00
现在时间是06:00
现在时间是07:00
现在时间是08:00
现在时间是09:00
从[晚上]状態变为了[白天]状态。
省略...

4 总结

4.1 依赖于状态的处理

Main类会调用SafeFrame类的setClock方法,告诉setClock方法设置时间。在setClock方法中,会像下面这样将处理委托给state类。
state.doClock(this, hour);
也就是说,我们将设置时间的处理看作是“依赖于状态的处理“。
要State模式中,要实现“依赖于状态的处理“,需要做到如下两点:

  • 定义接口,声明抽象方法
  • 定义多个类,实现具体方法
  • 应当由谁来管理状态迁移

在示例中,扮演Context角色的SafeFrame类实现了实际状态迁移的changeState方法,但实际调用该方法却是扮演ConcreteState角色的DayState类和NightState类。这种处理方式有优点也有缺点。
优点是这种处理方式将“什么时候从一个状态迁移到其它状态“的信息集中在了一个类中。当我们想知道“什么时候会从DayState类变化为其它状态”时,只需要阅读DayState类的代码即可。
缺点是“每个ConcreteState角色都需要知道其它ConcreteState角色”。例如,DayState类的doClock方法就使用了NightState类。如果以后需求发生变更,需要删除NightState类时,就必须要相应地修改DayState类的代码。
也可以不使用示例程序中的做法,而是将所有的状态迁移交给扮演Context角色的SafeFrame类来负责。有时,使用这种解决方法可以提高ConcreteState角色的独立性,程序的整体结构也会更加清晰。不过这样做的话,Context角色就必须要知道所有的ConcreteState角色。在这种情况下,我们可以使用Midiator模式。

4.2 易于增加新的状态

如需增加新的状态XXXState类,只需让它实现State接口,然后实现一些所需的方法即可。但是,在State模式中增加其他“依赖于状态的处理”是很困难的,需要在State接口中增加新的方法,并在所有的ConcreteState角色中实现这个方法。

摘自《图解设计模式》


版权声明:文末如注明作者和来源,则表示本文系转载,版权为原作者所有 | 本文如有侵权,请及时联系,承诺在收到消息后第一时间删除 | 如转载本文,请注明原文链接。
喜欢 (1)