目 录CONTENT

文章目录

观察者模式

半糖
2024-08-30 / 0 评论 / 0 点赞 / 8 阅读 / 17132 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2024-08-30,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

名称

观察者模式(OBSERVER),别名:依赖(Dependents),发布-订阅(Publish-Subscribe)

目的

定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

适用性

在以下任一情况下可以使用观察者模式:

  • 当一个抽象模型有两个方面,其中一个方面依赖于另一方面。将这二者封装在独立的对象中以使它们可以各自独立地改变和复用。

  • 当对一个对象的改变需要同时改变其它对象,而不知道具体有多少对象有待改变。

  • 当一个对象必须通知其它对象,而它又不能假定其它对象是谁,换言之,不希望这些对象是紧密耦合的。

结构

观察者(Observer):为那些在目标发生改变时需获得通知的对象定义一个更新接口。

具体观察者(ConcreteObserver)

  • 维护一个指向ConcreteSubject对象的引用。

  • 存储有关状态,这些状态应与目标的状态保持一致。

  • 实现Observer的更新接口以使自身状态与目标的状态保持一致。

目标(Subject)

  • 目标知道它的观察者。可以有任意多个观察者观察同一个目标。

  • 提供注册和删除观察者对象的接口。

具体目标(ConcreteSubject)

  • 将有关状态存入各ConcreteObserver对象。

  • 当它的状态发生改变时,向它的各个观察者发出通知。

Tips:以发布-订阅(Publish-Subscribe)模型来看,观察者是订阅者,目标是发布者,不要被sub搞反了,订阅的sub是Subscribe不是Subject。

协作

  1. 当ConcreteSubject发生任何可能导致其观察者与其本身状态不一致的改变时,它将通知它的各个观察者。

  2. 在得到一个具体目标的改变通知后,ConcreteObserver对象可向目标对象查询信息。ConcreteObserver使用这些信息以使它的状态与目标对象的状态一致。

交互图如下:

注意:发出改变请求的Observer对象并不立即更新,而是将其推迟到它从目标得到一个通知之后。Notify不总是由目标对象调用,它也可被一个观察者或其它对象调用。

效果

优点

  1. 目标和观察者间的抽象耦合。一个目标所知道的仅仅是它有一系列观察者,每个都符合抽象的Observer类的简单接口。目标不知道任何一个观察者属于哪一个具体的类。这样目标和观察者之间的耦合是抽象的和最小的。
    因为目标和观察者不是紧密耦合的,它们可以属于一个系统中的不同抽象层次。一个处于较低层次的目标对象可与一个处于较高层次的观察者通信并通知它,这样就保持了系统层次的完整。如果目标和观察者混在一块,那么得到的对象要么横贯两个层次(违反了层次性),要么必须放在这两层的某一层中(这可能会损害层次抽象)。

  2. 支持广播通信。不像通常的请求,目标发送的通知不需指定它的接收者。通知被自动广播给所有已向该目标对象登记的有关对象。目标对象并不关心到底有多少对象对自己感兴趣,它唯一的责任就是通知它的各观察者。

  3. 提供了在任何时刻增加和删除观察者的自由,处理还是忽略一个通知取决于观察者。

  4. 提高了可扩展性。当需要增加新的功能时,可以通过添加新的观察者类来实现,无需修改现有的代码。

  5. 简化了维护和管理。由于观察者模式将关注点分离,因此更容易维护和管理各个组件。每个观察者只关心自己的逻辑,减少了代码之间的依赖关系。

缺点

  1. 可能会产生意外的更新。因为一个观察者并不知道其它观察者的存在,它可能对改变目标的最终代价一无所知。在目标上一个看似无害的的操作可能会引起一系列对观察者以及依赖于这些观察者的那些对象的更新。此外,如果依赖准则的定义或维护不当,常常会引起错误的更新,这种错误通常很难捕捉。

  2. 性能开销。如果观察者数量很多,每次通知时都需要遍历所有观察者并调用它们的方法,这可能导致性能下降。

  3. 循环引用问题。在某些情况下,如果观察者和主题对象之间存在循环引用,可能会导致内存泄漏。需要注意管理好引用,避免不必要的内存占用。

  4. 复杂性增加。引入观察者模式会增加系统的复杂性,特别是当观察者数量较多时。维护多个观察者之间的关系可能变得复杂,特别是在需要协调不同观察者的行为时。

  5. 过度通知。如果没有良好的设计,可能会出现过度通知的情况,即某些观察者接收到不需要的通知,这种情况会导致不必要的计算和资源消耗。

实现

  1. 创建目标到其观察者之间的映射。一个目标对象跟踪它应通知的观察者的最简单的方法是显式地在目标中保存对它们的引用。然而,当目标很多而观察者较少时,这样存储可能代价太高。一个解决办法是用时间换空间,用一个关联查找机制(例如一个hash表)来维护目标到观察者的映射。这样一个没有观察者的目标就不产生存储开销。但另一方面,这一方法增加了访问观察者的开销。

  2. 观察多个目标。在某些情况下,一个观察者依赖于多个目标可能是有意义的。例如,一个表格对象可能依赖于多个数据源。在这种情况下,必须扩展Update接口以使观察者知道是哪一个目标送来的通知。目标对象可以简单地将自己作为Update操作的一个参数,让观察者知道应去检查哪一个目标。

  3. 谁触发更新。目标和它的观察者依赖于通知机制来保持一致,但到底哪一个对象调用Notify来触发更新?此时有两个选择:

    1. 由目标对象的状态设定操作在改变目标对象的状态后自动调用Notify。这种方法的优点是客户不需要记住要在目标对象上调用Notify,缺点是多个连续的操作会产生多次连续的更新,可能效率较低。

    2. 让客户负责在适当的时候调用Notify。这样做的优点是客户可以在一系列的状态改变完成后再一次性地触发更新,避免了不必要的中间更新。缺点是给客户增加了触发更新的责任。由于客户可能会忘记调用Notify,这种方式较易出错。

  4. 对已删除目标的悬挂引用。删除一个目标时应注意不要在其观察者中遗留对该目标的悬挂引用。一种避免悬挂引用的方法是,当一个目标被删除时,让它通知它的观察者将对该目标的引用复位。一般来说,也不能简单地删除观察者,因为其他的对象可能会引用它们,或者也可能它们还在观察其他的目标。

  5. 在发出通知前确保目标的状态自身是一致的。在发出通知前确保状态自身一致这一点很重要,因为观察者在更新其状态的过程中需要查询目标的当前状态。当Subject的子类调用继承的该项操作时,很容易无意中违反这条自身一致的准则。可以用抽象的Subject类中的模板方法(模板方法模式)发送通知来避免这种错误。定义那些子类可以重定义的原语操作,并将Notify作为模板方法中的最后一个操作,这样当子类重定义了Subject的操作时,还可以保证该对象的状态是自身一致的。
    这句话出自“原文”,说简单一点就是怕你在通知前没有改变目标的状态,这样当调用通知时,观察者要使用它内部的目标引用来获取目标状态,此时获取到的值时旧值,新值还没修改。他的意思是使用模板方法模式,在目标中定义一个模板方法(方法上定义个final让子类不能重写),模板方法内会依次执行目标中的方法(把notify放在最后),修改状态的操作统一走这个模板方法,这样当具体目标重写了目标的方法,对使用模板方法来修改状态没有任何影响。

  6. 确定观察者的更新协议(推/拉模型)。观察者模式的实现经常需要让目标广播关于其改变状态,推模型强调的是目标知道观察者想要的信息,所以在通知观察者的时候把这些信息带过去,拉模型强调的目标不知道它的观察者想要的信息,甚至不清楚它的观察者是谁(有很多具体观察者),所以在通知观察者的时候不带任何信息,观察者调用目标获取对应状态值的方法来获取信息。推模型使得观察者难以复用,因为每个观察者需要的信息不一致,拉模型可能效率较差,因为观察者对象需在没有目标对象帮助的情况下确定什么改变了,选择哪种模型需要取舍。

  7. 显式地指定感兴趣的改变。可以扩展目标的注册接口,让各观察者注册为仅对特定事件感兴趣,以提高更新的效率,当一个事件发生时,目标仅通知那些已注册为对该事件感兴趣的观察者。
    在注册的时候,将观察者与它感兴趣的事件进行绑定,在通知的时候,目标将这方面的改变作为Update操作的一个参数提供给它的观察者
    Subject的注册方法:void attach(Observer observer , Aspect interest);
    Observer的修改方法:void update(Observer observer , Aspect interest);

  8. 封装复杂的更新语义。当目标和观察者间的依赖关系特别复杂时,可能需要一个维护这些关系的对象。我们称这样的对象为更改管理器(ChangeManager)。它的目的是尽量减少观察者反映其目标的状态变化所需的工作量。例如,如果一个操作涉及到对几个相互依赖的目标进行改动,就必须保证仅在所有的目标都已更改完毕后,才一次性地通知它们的观察者,而不是每个目标都通知观察者。这里的ChangeManager可以用到中介者模式作为中介去处理目标和观察者,同时通常只有一个全局可见的ChangeManager,所以可以用到单例模式,ChangeManager有三个责任:

    1. 它将一个目标映射到它的观察者并提供一个接口来维护这个映射。这就不需要由目标来维护对其观察者的引用,反之亦然。

    2. 它定义一个特定的更新策略。

    3. 根据一个目标的请求,它更新所有依赖于这个目标的观察者。

  9. 整合目标类和观察者类。用不支持多重继承的语言(如Smalltalk)书写的类库通常不单独定义Subject和Observer类,而是将它们的接口整合到一个类中。这就允许定义一个既是一个目标又是一个观察者的对象,而不需要多重继承。例如在java中,Subject和Observer接口定义于根类Object中,使得它们对所有的类都可用。

应用

问题

当我们在使用智能家居的时候,可以创建自动化,比如温度高于某个值开启空调,湿度高于某个值开启除湿机,温度和湿度的变化引起某个事物执行某项操作,这就是最典型的观察者模型。在这里,我们把小爱作为目标(XiaoAi是目标,MyXiaoAi是具体目标),温度、湿度、压强作为目标的状态,同时定义注册观察者、删除观察者、通知观察者的方法,维护一个观察者列表,发生变化时通知列表内的观察者(比较简单粗暴,没有把观察者和感兴趣的事件进行绑定),然后各个被通知的观察者执行自己的操作。

示例

UML

代码示例

观察者:自动化

package com.ysj.part5.observer.observer;

/**
 * 观察者:自动化
 */
public interface Automation {

    /**
     * 操作
     */
    void operate();

}

具体观察者:温度自动化、湿度自动化

package com.ysj.part5.observer.concreteObserve;

import com.ysj.part5.observer.concreteSubject.MyXiaoAi;
import com.ysj.part5.observer.observer.Automation;

/**
 * 具体观察者:温度自动化
 */
public class TemperatureAutomation implements Automation {

    /**
     * 温度(需要观测的状态state)
     */
    private float temperature;

    /**
     * 阈值上限
     */
    private float thresholdMax;

    /**
     * 操作
     */
    private String operate;

    /**
     * 我的小爱
     */
    private MyXiaoAi myXiaoAi;

    public TemperatureAutomation(MyXiaoAi myXiaoAi, float thresholdMax, String operate) {
        this.myXiaoAi = myXiaoAi;
        this.thresholdMax = thresholdMax;
        this.operate = operate;
        // 创建自动化(注册观察者)
        myXiaoAi.registerAutomation(this);
    }

    @Override
    public void operate() {
        this.temperature = myXiaoAi.getTempterature();
        if (temperature > thresholdMax) {
            System.out.println("温度自动化:当前温度" + temperature + ",超过阈值上限" + thresholdMax + "," + operate);
        } else {
            System.out.println("温度自动化:当前温度" + temperature + ",未超过阈值上限" + thresholdMax);
        }
    }

}
package com.ysj.part5.observer.concreteObserve;

import com.ysj.part5.observer.concreteSubject.MyXiaoAi;
import com.ysj.part5.observer.observer.Automation;

/**
 * 具体观察者:湿度自动化
 */
public class HumidityAutomation implements Automation {

    /**
     * 湿度(需要观测的状态state)
     */
    private float humidity;


    /**
     * 阈值上限
     */
    private float thresholdMax;

    /**
     * 操作
     */
    private String operate;

    /**
     * 我的小爱
     */
    private MyXiaoAi myXiaoAi;


    public HumidityAutomation(MyXiaoAi myXiaoAi, float thresholdMax, String operate) {
        this.myXiaoAi = myXiaoAi;
        this.thresholdMax = thresholdMax;
        this.operate = operate;
        // 创建自动化(注册观察者)
        myXiaoAi.registerAutomation(this);
    }


    @Override
    public void operate() {
        this.humidity = myXiaoAi.getHumidity();
        if (humidity > thresholdMax) {
            System.out.println("湿度自动化:当前湿度" + humidity + ",超过阈值上限" + thresholdMax + "," + operate);
        } else {
            System.out.println("湿度自动化:当前湿度" + humidity + ",未超过阈值上限" + thresholdMax);
        }
    }
    
}

目标:小爱

package com.ysj.part5.observer.subject;

import com.ysj.part5.observer.observer.Automation;

/**
 * 目标:小爱
 */
public interface XiaoAi {

    /**
     * 创建自动化
     *
     * @param automation
     */
    void registerAutomation(Automation automation);

    /**
     * 删除自动化
     *
     * @param automation
     */
    void removeAutomation(Automation automation);

    /**
     * 当目标发生变化时,通知各自动化
     */
    void notifyAutomation();

}

具体目标:我的小爱

package com.ysj.part5.observer.concreteSubject;

import com.ysj.part5.observer.observer.Automation;
import com.ysj.part5.observer.subject.XiaoAi;
import lombok.Getter;
import lombok.Setter;

import java.util.ArrayList;
import java.util.List;

/**
 * 具体目标:我的小爱
 */
@Getter
@Setter
public class MyXiaoAi implements XiaoAi {

    /**
     * 自动化列表
     */
    private final List<Automation> automations = new ArrayList<Automation>();

    /**
     * 温度
     */
    private float tempterature;

    /**
     * 压强
     */
    private float pressure;

    /**
     * 湿度
     */
    private float humidity;

    @Override
    public void registerAutomation(Automation automation) {
        automations.add(automation);
    }

    @Override
    public void removeAutomation(Automation automation) {
        int i = automations.indexOf(automation);
        if (i >= 0) {
            automations.remove(i);
        }
    }

    /**
     * 通知所有自动化(观察者)
     */
    @Override
    public void notifyAutomation() {
        for (int i = 0; i < automations.size(); i++) {
            Automation automation = automations.get(i);
            // "提醒自动化"要执行的操作就是调用各个自动化的操作方法
            automation.operate();
        }
    }

    /**
     * 修改环境时通知所有观察者
     *
     * @param temperature
     * @param humidity
     * @param pressure
     */
    public void updateConditions(float temperature, float humidity, float pressure) {
        this.tempterature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        // 通知所有自动化(观察者)
        notifyAutomation();
    }
}

客户端:客户端

package com.ysj.part5.observer;

import cn.hutool.core.thread.ThreadUtil;
import com.ysj.part5.observer.concreteObserve.TemperatureAutomation;
import com.ysj.part5.observer.concreteObserve.HumidityAutomation;
import com.ysj.part5.observer.concreteSubject.MyXiaoAi;

/**
 * 观察者模式(发布订阅模式)
 * 定义对象间的一种一对多的依赖关系 ,当一个对象的状态发生改变时 , 所有依赖于它的对象都得到通知并被自动更新
 */
public class Client {
    public static void main(String[] args) {
        // 创建具体目标对象
        MyXiaoAi myXiaoAi = new MyXiaoAi();

        // 构建自动化对象时就在目标(小爱)处注册观察者(自动化),温度是超过30度打开空调制冷,湿度超过80打开除湿器除湿
        new TemperatureAutomation(myXiaoAi, 30f, "打开空调制冷");
        new HumidityAutomation(myXiaoAi, 80f, "打开除湿器除湿");

        // 小爱在各时间段根据采集到的信息修改目标各状态值,并通知观察者(执行操作)
        System.out.println("12:00");
        myXiaoAi.updateConditions(28, 65, 30.4f);
        ThreadUtil.sleep(5000);
        System.out.println("12:30");
        myXiaoAi.updateConditions(34, 81, 29.2f);
        ThreadUtil.sleep(5000);
        System.out.println("13:00");
        myXiaoAi.updateConditions(40, 90, 40.4f);
    }
}

已知应用

Java Swing/AWT

Swing/AWT 是 Java 中用于构建图形用户界面的库。它广泛使用了观察者模式来处理用户界面事件,如按钮点击、键盘输入等。例如,JButton 类通过实现 ActionListener 接口来注册观察者,当按钮被点击时,会通知所有注册的监听器。

Spring Framework

Spring 是一个流行的 Java 应用程序框架,其中包含了多种事件发布和监听机制。Spring 的 ApplicationEvent 和 ApplicationListener 机制就是基于观察者模式实现的。

0
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin

评论区