《图解设计模式》笔记(八)管理状态

十七、Observer模式:发送状态变化通知

Observer :"进行观察的人",也就是"观察者"。

在 Observer模式中,当观察对象的状态发生变化时,会通知给观察者。

适用场景:根据对象状态进行相应处理.

示例程序:观察者将观察一个会生成数值的对象,并将它生成的数值结果显示出来。不同的观察者的显示方式不一样:DigitObserver以数字形式显示数值,GraphObserver以简单的图示形式显示数值。

示例程序类图

Observer

java 复制代码
public interface Observer {
    public abstract void update(NumberGenerator generator);
}

NumberGenerator

java 复制代码
import java.util.ArrayList;
import java.util.Iterator;

public abstract class NumberGenerator {
    private ArrayList observers = new ArrayList();        // 保存Observer们
    public void addObserver(Observer observer) {    // 注册Observer
        observers.add(observer);
    }
    public void deleteObserver(Observer observer) { // 删除Observer
        observers.remove(observer);
    }
    public void notifyObservers() {               // 向Observer发送通知
        Iterator it = observers.iterator();
        while (it.hasNext()) {
            Observer o = (Observer)it.next();
            o.update(this);
        }
    }
    public abstract int getNumber();                // 获取数值
    public abstract void execute();                 // 生成数值
}

RandomNumberGenerator

java 复制代码
import java.util.Random;

public class RandomNumberGenerator extends NumberGenerator {
    private Random random = new Random();   // 随机数生成器
    private int number;                     // 当前数值
    public int getNumber() {                // 获取当前数值
        return number;
    }
    public void execute() {
        for (int i = 0; i < 20; i++) {
            number = random.nextInt(50);
            notifyObservers();
        }
    }
}

DigitObserver

java 复制代码
public class DigitObserver implements Observer {

    // 接收参数为NumberGenerator的实例,然后通过调用NumberGenerator类的实例的getNumber方法可以获取到当前的数值,并将这个数值显示出来。
    public void update(NumberGenerator generator) {
        System.out.println("DigitObserver:" + generator.getNumber());
        try {
            // 为能够看清它是如何显示数值的,使用Thread.sleep来降低程序的运行速度。
            Thread.sleep(100);
        } catch (InterruptedException e) {
        }
    }
}

GraphObserver

java 复制代码
public class GraphObserver implements Observer {
    public void update(NumberGenerator generator) {
        System.out.print("GraphObserver:");
        int count = generator.getNumber();
        for (int i = 0; i < count; i++) {
            System.out.print("*");
        }
        System.out.println("");
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
        }
    }
}

Main

java 复制代码
public class Main {
    public static void main(String[] args) {
        NumberGenerator generator = new RandomNumberGenerator();
        Observer observer1 = new DigitObserver();
        Observer observer2 = new GraphObserver();
        generator.addObserver(observer1);
        generator.addObserver(observer2);
        generator.execute();
    }
}

角色

  • Subject(观察对象)
    定义了注册观察者和删除观察者的方法。
    声明了"获取现在的状态"的方法。
    示例中是NumberGenerator类。
  • ConcreteSubject(具体的观察对象)
    当自身状态发生变化后,它会通知所有已经注册的Observer角色。
    示例中是RandomNumberGenerator类。
  • Observer(观察者)
    接收来自Subject角色的状态变化的通知。为此,它声明了update方法。
    示例中是Observer接口。
  • ConcreteObserver (具体的观察者)
    当它的update方法被调用后,会去获取要观察的对象的最新状态。
    示例中是DigitObserver类和GraphObserver类。

拓展思路的要点

这里也出现了可替换性

使用设计模式的目的之一就是使类成为可复用的组件。

在本模式中,有带状态的ConcreteSubject角色和接收状态变化通知的ConcreteObserver角色。

连接这两个角色的就是它们的接口(API) Subject角色和Observer角色。

一方面RandomNumberGenerator类并不知道,也无需在意正在观察自己的(自己需要通知的对象)到底是DigitObserver类的实例还是GraphObserver类的实例。不过它知道在它的observers字段中所保存的观察者们都实现了Observer接口。因为这些实例都是通过addObserver方法注册的,这就确保了它们一定都实现了Observer接口,一定可以调用它们的update方法。

另一方面,DigitObserver类也无需在意自己正在观察的究竟是RandomNumberGenerator类的实例还是其他XXXXNumberGenerator类的实例。不过,DigitObserver类知道它们是NumberGenerator类的子类的实例,并持有getNumber方法。

利用抽象类和接口从具体类中抽出抽象方法

在将实例作为参数传递至类中,或者在类的字段中保存实例时,不使用具体类型,而是使用抽象类型和接口

这样的实现方式可以轻松替换具体类。

Observer的顺序

在示例的notifyObservers方法中,先注册的Observer的update方法会先被调用。

通常在设计时就要注意调用顺序。

在示例中,绝不能因为先调用 DigitObserver的update方法后调用 GraphObserver的update方法而导致应用程序不能正常工作。

通常,只要保持各个类的独立性,就不会有上面这种类的依赖关系混乱问题。

不过,还需要注意下面将要提到的情况。

当Observer的行为会对Subject产生影响时

在示例中,RandomNumberGenerator类会在自身内部生成数值,调用 update方法。

但在通常的 Observer模式中,也可能是其他类触发Subject角色调用update方法。

例如,在GUI应用程序中,多数情况下是用户按下按钮后会触发update方法被调用。

Observer角色也有可能会触发 Subject 角色调用 update方法。这时注意不要导致方法被循环调用。

Subject 状态发生变化

↓

通知Observer

↓

Observer调用 Subject 的方法

↓

导致Subject状态发生变化

↓

通知Observer

↓

......

传递更新信息的方式

传递给update方法的参数只有一个:调用update方法的NumberGenerator的实例自身。

Observer会在update方法中调用该实例的getNumber来获取足够的数据。

不过在示例中,update方法接收到的参数中并没有被更新的数值。

即,update方法的定义可能不是void update(NumberGenerator generator);,而是void update(NumberGenerator generator, int number);或者更简单的void update(int number);这样的。

但void update(int number);这种方式在Observer需要观察多个Subject时不适用,因为Observer角色不知道传递给update方法的参数究竟是哪个Subject角色的数值。

从"观察"变为"通知"

Observer角色并非主动地去观察,而是被动地接受来自Subject角色的通知。

因此,Observer模式也被称为 Publish-Subscribe(发布-订阅)模式。

Publish (发布)和 Subscribe (订阅)这个名字可能更加合适。

ModelNiew/Controller(MVC)

MVC中的Model和View的关系与Subject角色和Observer角色的关系相对应。

Model是指操作"不依赖于显示形式的内部模型"的部分,View则是管理Model"怎样显示"的部分。

通常情况下,一个Model对应多个View。

相关的设计模式

  • Mediator模式(第16章)

    在 Mediator模式中,有时会使用 Observer模式来实现 Mediator角色与 Colleague角色之间的通信。

    就"发送状态变化通知"这一点而言,Mediator模式与Observer 模式是类似的。不过,两种模式中,通知的目的和视角不同。

    在 Mediator模式中,虽然也会发送通知,不过那不过是为了对Colleague角色进行仲裁而已。

    而在Observer模式中,将 Subject角色的状态变化通知给Observer角色的目的则主要是为了使Subject角色和Observer角色同步。

十八、Memento 模式:保存对象状态

Memento:纪念品、遗物、备忘录

要想像撤销一样恢复实例,需要一个可以自由访问实例内部结构的权限。但稍不注意,又可能会将依赖于实例内部结构的代码分散地编写在程序中的各个地方,导致程序变得难以维护。这种情况就叫作"破坏了封装性"。

Memento模式:通过引入表示实例状态的角色,可在保存和恢复实例时有效地防止对象的封装性遭到破坏。

使用本模式可实现应用程序的以下功能:Undo(撤销)、Redo(重做)、History (历史记录)、Snapshot (快照)

示例程序是一个收集水果和获取金钱数的掷骰子游戏,游戏规则如下:

游戏是自动进行的

游戏的主人公通过掷骰子来决定下一个状态

当骰子点数为1的时候,主人公的金钱会增加

当骰子点数为2的时候,主人公的金钱会减少

当骰子点数为6的时候,主人公会得到水果

主人公没有钱时游戏就会结束

如果金钱增加,为方便将来恢复状态,会生成Memento类的实例,将现在的状态保存起来。所保存的数据为当前持有的金钱和水果。

若金钱一直减少,为防止金钱变为0而结束游戏,将使用Memento的实例将游戏恢复至之前的状态。

示例程序类图

Memento

java 复制代码
package game;
import java.util.*;

public class Memento {
    int money;                              // 所持金钱
    ArrayList fruits;                       // 当前获得的水果
    public int getMoney() {                 // 获取当前所持金钱(narrow interface)
        return money;
    }
    Memento(int money) {                    // 构造函数(wide interface)
        this.money = money;
        this.fruits = new ArrayList();
    }
    void addFruit(String fruit) {           // 添加水果(wide interface)
        fruits.add(fruit);
    }
    List getFruits() {                      // 获取当前所持所有水果(wide interface)
         return (List)fruits.clone();
    }
}

Gamer

java 复制代码
package game;
import java.util.*;

public class Gamer {
    private int money;                          // 所持金钱
    private List fruits = new ArrayList();      // 获得的水果
    private Random random = new Random();       // 随机数生成器
    private static String[] fruitsname = {      // 表示水果种类的数组
        "苹果", "葡萄", "香蕉", "橘子",
    };
    public Gamer(int money) {                   // 构造函数
        this.money = money;
    }
    public int getMoney() {                     // 获取当前所持金钱
        return money;
    }

    // 只要主人公没有破产,就会一直掷骰子,并根据骰子结果改变所持有的金钱数目和水果个数。
    public void bet() {                         // 投掷骰子进行游戏
        int dice = random.nextInt(6) + 1;           // 掷骰子
        if (dice == 1) {                            // 骰子结果为1...增加所持金钱
            money += 100;
            System.out.println("所持金钱增加了。");
        } else if (dice == 2) {                     // 骰子结果为2...所持金钱减半
            money /= 2;
            System.out.println("所持金钱减半了。");
        } else if (dice == 6) {                     // 骰子结果为6...获得水果
            String f = getFruit();
            System.out.println("获得了水果(" + f + ")。");
            fruits.add(f);
        } else {                                    // 骰子结果为3、4、5则什么都不会发生
            System.out.println("什么都没有发生。");
        }
    }

    // 会根据当前在时间点所持有的金钱和水果生成一个Memento类的实例,该实例代表了"当前Gamer的状态",它会被返回给调用者。
    // 就如同给对象照了张照片一样,我们将对象现在的状态封存在 Memento类的实例中。请注意我们只保存了"好吃"的水果。
    public Memento createMemento() {                // 拍摄快照
        Memento m = new Memento(money);
        Iterator it = fruits.iterator();
        while (it.hasNext()) {
            String f = (String)it.next();
            if (f.startsWith("好吃的")) {         // 只保存好吃的水果
                m.addFruit(f);
            }
        }
        return m;
    }
    public void restoreMemento(Memento memento) {   // 撤销
        this.money = memento.money;
        this.fruits = memento.getFruits();
    }
    public String toString() {                      // 用字符串表示主人公状态
        return "[money = " + money + ", fruits = " + fruits + "]";
    }
    private String getFruit() {                     // 获得一个水果
        String prefix = "";
        if (random.nextBoolean()) {
            prefix = "好吃的";
        }
        return prefix + fruitsname[random.nextInt(fruitsname.length)];
    }
}

Main

java 复制代码
import game.Memento;
import game.Gamer;

public class Main {
    public static void main(String[] args) {
        Gamer gamer = new Gamer(100);               // 最初的所持金钱数为100
        Memento memento = gamer.createMemento();    // 保存最初的状态
        for (int i = 0; i < 100; i++) {
            System.out.println("==== " + i);        // 显示掷骰子的次数
            System.out.println("当前状态:" + gamer);    // 显示主人公现在的状态

            gamer.bet();    // 进行游戏 

            System.out.println("所持金钱为" + gamer.getMoney() + "元。");

            // 决定如何处理Memento
            if (gamer.getMoney() > memento.getMoney()) {
                System.out.println("    (所持金钱增加了许多,因此保存游戏当前的状态)");
                memento = gamer.createMemento();
            } else if (gamer.getMoney() < memento.getMoney() / 2) {
                System.out.println("    (所持金钱减少了许多,因此将游戏恢复至以前的状态)");
                gamer.restoreMemento(memento);
            }

            // 等待一段时间
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
            System.out.println("");
        }
    }
}

角色

  • Originator(生成者)

    Originator角色会在保存自己的最新状态时生成Memento角色。

    当把以前保存的Memento角色传递给Originator角色时,它会将自己恢复至生成该Memento角色时的状态。

    示例中是Gamer类。

  • Memento(纪念品)

    将 Originator角色的内部信息整合在一起。

    内部虽然保存了Originator角色的信息,但不会向外部公开信息。

    Memento角色有以下两种接口(API)。

    wide interface-宽接口(API):所有用于获取恢复对象状态信息的方法的集合。由于宽接口(API)会暴露所有Memento角色的内部信息,因此能够使用宽接口(API)的只有Originator角色。

    narrowinterface窄接口(API):可以通过窄接口(API)获取的Memento角色的内部信息非常有限,因此可以有效地防止信息泄露。

    对外提供以上两种接口(API),可有效防止破坏对象的封装性。

    示例中是Memento类。

    Originator角色和 Memento角色之间有着非常紧密的联系。

  • Caretaker(负责人)

    当 Caretaker角色想要保存当前的 Originator 角色的状态时,会通知Originator角色。

    Originator角色在接收到通知后会生成Memento角色的实例并将其返回给 Caretaker角色。

    以后可能会用Memento实例来将 Originator恢复至原来的状态,因此 Caretaker角色会一直保存Memento实例。

    示例中是Main类。

    不过,Caretaker角色只能使用 Memento角色的窄接口(API),即它无法访问Memento角色内部的所有信息。

    它只是将 Originator角色生成的Memento角色当作一个黑盒子保存起来。

    虽然Originator角色和Memento角色之间是强关联关系,但Caretaker角色和Memento角色之间是弱关联关系。Memento角色对 Caretaker角色隐藏了自身的内部信息。

拓展思路的要点

需要多少个Memento

在示例中,Main类只保存了一个Memento。如果在Main类中使用数组等集合,让它可以保存多个Memento类的实例,就可以实现保存各个时间点的对象的状态。

Memento的有效期限是多久

示例是在内存中保存Memento的,没问题。但若要将Memento永远保存在文件中,就会出现有效期限的问题了。

因为,假设在某个时间点将Memento保存在文件中,之后又升级了应用程序版本,则可能会原来保存的Memento与当前的应用程序不匹配。

划分 Caretaker角色和 Originator角色的意义

如果是要实现撤销功能,直接在Originator角色中实现不就好了吗?为什么要麻烦地引入Memento 模式?

Caretaker角色的职责是决定何时拍摄快照,何时撤销以及保存Memento角色。

另一方面,Originator角色的职责则是生成Memento角色和使用接收到的 Memento角色来恢复自己的状态。

有了这样的职责分担,当我们需要对应以下需求变更时,就可以完全不用修改Originator角色。

变更为可以多次撤销

变更为不仅可以撤销,还可以将现在的状态保存在文件中

相关的设计模式

  • Command 模式(第22章)

    在使用 Command模式处理命令时,可以使用 Memento模式实现撤销功能。

  • Protype模式(第6章)

    在Memento 模式中,为了能够实现快照和撤销功能,保存了对象当前的状态。保存的信息只是在恢复状态时所需要的那部分信息。

    而在Protype模式中,会生成一个与当前实例完全相同的另外一个实例。这两个实例的内容完全一样。

  • State 模式(第19章)

    在 Memento 模式中,是用"实例"表示状态。

    而在 State模式中,则是用"类"表示状态。

十九、State模式:用类表示状态

State:状态

在 State模式中,用类来表示状态。

以类来表示状态后,能通过切换类来方便地改变对象的状态。当增加新状态时,也能明确如何修改代码。

伪代码

不使用state模式:用方法来判断状态

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

使用了State 模式:用类来表示状态

java 复制代码
表示白天的状态的类{
    使用金库时被调用的方法(){
        向警报中心报告使用记录
    }
    铃响起时被调用的方法()(
        向警报中心报告紧急事态
    }
    正常通话时披调用的方法()(
        呼叫警报中心
    }

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

示例程序类图

State

java 复制代码
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

java 复制代码
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

java 复制代码
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

java 复制代码
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

java 复制代码
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

java 复制代码
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(状态)

    表示状态,定义了根据不同状态进行不同处理的接口(API)。

    该接口(API)是处理内容依赖于状态的方法的集合。

    示例中是State接口。

  • ConcreteState (具体状态)

    表示各个具体的状态,它实现了State接口。

    示例中是DayState类和NightState类。

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

    持有表示当前状态的ConcreteState角色。

    还定义了供外部调用者使用State模式的接口(API)。

    示例中是Context接口和SafeFrame类。

补充说明:示例中,Context角色的作用被Context接口和SafeFrame类分担了。

具体而言,Context接口定义了供外部调用者使用 State模式的接口(API),而SafeFrame类则持有表示当前状态的ConcreteState角色。

拓展思路的要点

分而治之

分而治之,就是将一个复杂的大问题分解为多个小问题然后逐个解决。

在 State模式中,用类来表示状态,并为每一种具体的状态都定义一个相应的类。这样,问题就被分解了。

开发人员可以在编写一个ConcreteState角色的代码的同时,在头脑中(一定程度上)考虑其他的类。

依赖于状态的处理

思考一下SafeFrame类的setClock方法和State接口的doClock方法之间的关系。

Main类会调用SafeFrame类的setClock方法"设置时间"。在setClock方法中,会state.doClock(this, hour);将处理委托给state类。

即,我们将设置时间的处理看作是"依赖于状态的处理"。

不只是doclock方法。在State接口中声明的所有方法都是"依赖于状态的处理",都是"状态不同处理也不同"。

在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模式(第16章)。
当然,还可以不用 State模式,而是用状态迁移表来设计程序。所谓状态迁移表是可以根据"输入和内部状态"得到"输出和下一个状态"的一览表。当状态迁移遵循一定的规则时,使用状态迁移表非常有效。

不会自相矛盾

若不使用 State模式,我们需要使用多个变量的值的集合来表示系统的状态。这时,必须注意不要让变量的值之间互相矛盾。

State模式中,是用类来表示状态的,只需要一个表示系统状态的变量即可。

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

易于增加新的状态

以示例程序来说,编写一个XXXState类,让它实现State接口,然后实现一些所需的方法就可以了。

但状态迁移的部分正是与其他 ConcreteState角色相关联的部分,修改时要小心。

在State模式中增加其他"依赖于状态的处理"是很困难的,因为需要在State接口中增加新的方法,并在所有的ConcreteState角色中都实现这个方法。虽说很困难,但是好在我们绝对不会忘记实现这个方法(否则编译报错)。

实例的多面性

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

SafeFrame类的构造函数中的buttonUse.addActionListener(this);

actionPerformed方法中的state.doUse(this);

这两条语句中都有this,它们都是SafeFrame类的实例。在示例中只生成了一个SafeFrame的实例,因此这两个this其实是同一个对象。

但在addActionListener、doUse方法中,对this的使用方式不一样。

向addActionListener方法传递this时,该实例会被当作"实现了ActionListener接口的类的实例"来使用,因为方法的参数类型是ActionListener类型,至于这个参数是否是SafeFrame类的实例并不重要。

向doUse方法传递this时,该实例会被当作"实现了Context接口的类的实例"来使用,因为doUse方法的参数类型是Context类型。在doUse方法中会用到的方法也都是在Context接口中定义了的方法(再回顾一下DayState类和NightState类的doUse方法)。

相关的设计模式

  • Singleton模式(第5章)

    Singleton模式常常会出现在ConcreteState角色中。

    在示例中就用了Singleton模式。这是因为在表示状态的类中并没有定义任何实例字段(即表示实例的状态的字段)。

  • Flyweight 模式(第20章)

    在表示状态的类中没有定义任何实例字段。

    因此,有时可以使用Flyweight模式在多个Context角色之间共享ConcreteState角色。

相关推荐
志尊宝8 小时前
Vue3 零基础每日笔记(082):文件上传实战——el-upload 校验、自定义上传、头像预览
前端·javascript·vue.js·笔记·#javascript·#vue.js
鬼手点金9 小时前
opencode-rules 插件
java·开发语言·前端·人工智能·chrome·git·github
后季暖9 小时前
电商问数笔记(2)
笔记
xier_ran9 小时前
【infra之路】AI 编译器学习笔记(二):重拾 C++——移动语义与 LLVM 风格的类型转换
人工智能·笔记·学习
paopaokaka_luck9 小时前
滑雪场管理系统(AI智能助手流式对话,ECharts 运营数据分析、教练、雪具与场地一体化预约,钱包支付退款、订单核销与服务评价)
java·人工智能·spring boot·网络协议·数据分析·echarts
Wang's Blog10 小时前
Java 项目实战: 交友项目 - Dubbo 的内部结构与一次调用的完整链路
java·dubbo
Wang's Blog10 小时前
Java 项目实战: 交友项目 - Dubbo 入门引导
java·dubbo·交友
蜗牛互联网10 小时前
Python Agent工具调用的白名单、参数校验与审批门禁
java·服务器·前端·人工智能·后端·python
SL_staff11 小时前
当规则引擎开始‘懂业务’:JVS-Rules金融风控四大原生能力拆解
java·python·全栈
Shaoshing11 小时前
前缀和与差分
java·数据结构·算法·前缀和·差分