【行为型模式】观察者模式

  • Post author:
  • Post category:其他




1、概述

观察者模式(Observer)是一种行为设计模式,它定义了对象之间的

一对多依赖关系

,当一个对象的状态发生改变时,其他所有依赖于它的对象都会自动被通知并更新。这个模式也被称为

发布/订阅模式

观察者模式通常用于

需要实现事件驱动系统的场景

中。例如,当用户订阅了一个新闻网站的推送服务后,当这个网站发布了新的文章时,它会将这些更新通知给所有的订阅用户。在这个例子中,订阅用户是观察者,新闻网站是被观察者。

观察者模式的好处是减少了对象之间的耦合性,使得它们可以独立地进行修改和扩展。同时,当我们需要为一个对象添加或删除观察者时,也变得更加容易。因此,观察者模式是一种非常有用的设计模式。



2、结构

观察者模式的结构通常由以下几个部分组成:


  1. Subject

    (目标):被观察者,它包含了所有的观察者,并提供了添加、删除和通知观察者的方法;

  2. Observer

    (观察者):观察者,它定义了接收目标发送的通知并更新自己状态的方法;

  3. ConcreteSubject

    (具体目标):具体的被观察者,它维护着一组观察者,并在状态发生变化时通知它们;

  4. ConcreteObserver

    (具体观察者):具体的观察者,实现了Observer定义的接口,以便在接收到通知时更新自己的状态。

uml图



3、实现方式



3.1、案例引入

当微信公众号发布一条新的文章时,所有关注该公众号的微信用户都会收到推送通知。在这个例子中,微信公众号就是被观察者,而所有关注该公众号的微信用户则是观察者。 当微信公众号发布新的文章时,它会将这个状态变化(即发布了新的文章)通知给所有观察者,也就是微信用户,从而让他们第一时间了解到最新的内容。

image-20230416133718647



3.2、结构分析

根据案例中的场景分析,可以将场景中的各个部分对应到观察者模式中的相应角色:


  1. Subject

    接口对应抽象被观察者(Subject)角色。

  2. WeChatOfficialAccount

    类对应具体被观察者(ConcreteSubject)角色。

  3. Observer

    接口对应抽象观察者(Observer)角色。

  4. WeChatUser

    类对应具体观察者(ConcreteObserver)角色。

案例uml



3.3、具体实现

以下是使用Java语言实现观察者模式的具体代码,实现微信公众号发布新文章通知所有关注该公众号的微信用户:

  1. 定义被观察者接口

    Subject

    和观察者接口

    Observer

    ,其中

    Subject

    接口定义了三个方法:注册观察者、移除观察者和通知所有观察者;

    Observer

    接口定义了一个方法:更新状态。
// 定义被观察者接口
interface Subject {
    // 注册观察者
    void registerObserver(Observer observer); 
    // 移除观察者
    void removeObserver(Observer observer); 
    // 通知所有观察者
    void notifyObservers(String articleTitle); 
}

// 定义观察者接口
interface Observer {
    void update(String articleTitle); // 更新状态
}
  1. 微信公众号类

    WeChatOfficialAccount

    实现了被观察者接口

    Subject

    。它维护了一个观察者列表,实现了注册观察者、移除观察者和通知所有观察者的方法,并在发布新文章时通知所有观察者。
// 微信公众号类作为被观察者,实现Subject接口
class WeChatOfficialAccount implements Subject {
    private List<Observer> observers = new ArrayList<>();
    private String articleTitle;

    // 注册观察者
    @Override
    public void registerObserver(Observer observer) {
        observers.add(observer);
    }

    // 移除观察者
    @Override
    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }

    // 通知所有观察者
    @Override
    public void notifyObservers(String articleTitle) {
        for (Observer observer : observers) {
            observer.update(articleTitle);
        }
    }

    // 发布新文章,通知所有观察者
    public void publishArticle(String articleTitle) {
        this.articleTitle = articleTitle;
        System.out.println("微信公众号发布了新文章:" + articleTitle);
        notifyObservers(articleTitle);
    }
}
  1. 微信用户类

    WeChatUser

    实现了观察者接口

    Observer

    。它在接收到微信公众号发布新文章的通知时,更新状态,即打印接收到的文章标题。
// 微信用户类作为观察者,实现Observer接口
class WeChatUser implements Observer {
    private String userName;

    public WeChatUser(String userName) {
        this.userName = userName;
    }

    // 更新状态,即打印接收到的文章标题
    @Override
    public void update(String articleTitle) {
        System.out.println(userName + "收到了新文章通知:" + articleTitle);
    }
}
  1. 测试代码

    TestObserverPattern

    中,首先创建了一个微信公众号对象和三个微信用户对象,并让这三个用户都关注了该公众号。然后,微信公众号发布新文章,通知所有关注该公众号的微信用户。接着,其中一个微信用户取消了对该公众号的关注。最后,微信公众号再次发布新文章,通知所有关注该公众号的微信用户。
public class TestObserverPattern {
    public static void main(String[] args) {
        // 创建微信公众号对象
        WeChatOfficialAccount officialAccount = new WeChatOfficialAccount(); 

        // 创建微信用户对象
        WeChatUser zhangsan = new WeChatUser("张三"); 
        WeChatUser lisi = new WeChatUser("李四");
        WeChatUser wangwu = new WeChatUser("王五");

        // 微信用户关注微信公众号
        officialAccount.registerObserver(zhangsan); 
        officialAccount.registerObserver(lisi);
        officialAccount.registerObserver(wangwu);

        // 微信公众号发布新文章
        officialAccount.publishArticle("观察者模式在Java中的应用"); 

        // 微信用户李四取消关注微信公众号
        officialAccount.removeObserver(lisi); 

        // 微信公众号再次发布新文章
        officialAccount.publishArticle("学习Java多线程编程"); 
    }
}

整个程序运行后,输出结果为:

微信公众号发布了新文章:观察者模式在Java中的应用
张三收到了新文章通知:观察者模式在Java中的应用
李四收到了新文章通知:观察者模式在Java中的应用
王五收到了新文章通知:观察者模式在Java中的应用

微信公众号发布了新文章:学习Java多线程编程
张三收到了新文章通知:学习Java多线程编程
王五收到了新文章通知:学习Java多线程编程



4、观察者模式优缺点

观察者模式的优点:


  1. 降低了系统耦合度

    :被观察者对象和观察者对象之间是松耦合关系,它们之间仅仅通过接口进行交互,可以降低系统中各个对象之间的耦合度,提高系统的可扩展性和可维护性;


  2. 支持广播通信

    :一旦被观察者对象发生变化,它会自动通知所有观察者对象,从而实现了广播通信;


  3. 符合开闭原则

    :当需要在系统中添加新的观察者时,只需要定义一个新的观察者类并实现抽象观察者接口即可,不需要修改原有代码,符合开闭原则;


  4. 可以建立多层次触发链

    :观察者对象除了可以观察被观察者对象外,还可以是其他观察者对象的被观察者对象,从而实现多层次触发链。

观察者模式的缺点:


  1. 观察者太多,通知开销大

    :当观察者对象数量较多时,被观察者对象每次状态改变时都需要通知所有观察者对象,这将导致通知开销很大;


  2. 循环依赖问题

    :当观察者对象之间存在循环依赖时,系统的运行可能会出现问题;


  3. 无法保证通知顺序

    :由于被观察者和观察者之间是松耦合关系,观察者对象的调用顺序不能保证。

优点 缺点
降低系统耦合度 观察者太多,通知开销大
支持广播通信 循环依赖问题
符合开闭原则 无法保证通知顺序
可以建立多层次触发链

与此同时,针对观察者模式的缺点,可以采取以下优化的手段:


  1. 减少通知开销

    :如果被观察者状态变化太频繁,可以考虑采用“节流”或“防抖”的方式减少通知次数。例如,当被观察者状态连续变化时,只在最后一次变化后通知所有观察者;


  2. 避免循环依赖

    :可以通过定义一个中间对象来解决循环依赖问题,即让观察者对象与中间对象建立观察者-被观察者关系,中间对象与被观察者对象建立观察者-被观察者关系,从而避免存在直接的循环依赖;


  3. 确保通知顺序

    :可以在具体被观察者类中增加一个排序列表,每个观察者对象在注册时指定一个排序号,被观察者对象通知观察者时按照排序号的大小进行通知;


  4. 采用异步通知

    :将观察者的更新操作放到异步任务队列中执行,避免阻塞主线程,提高系统的响应速度和稳定性;


  5. 使用事件总线框架

    :可以使用事件总线框架来处理观察者模式中的通知问题,事件总线框架可以将观察者模式中的通知转化为事件,并在事件总线上进行管理和分发,从而实现了更高效的消息传递机制。



5、应用场景

观察者模式适用于以下场景:

  1. 当需要将一个对象的状态变化

    通知给其他多个对象

    时,可以使用观察者模式;

  2. 当一个对象的改变需要

    同时改变其他对象的状态或者操作

    时,可以使用观察者模式;



  3. 一个抽象模型

    有两个方面,其中

    一个方面依赖于另一个方面

    时,可以使用观察者模式。例如,模型和视图之间的关系,模型驱动视图的更新,而视图反过来也要反映到模型中;

  4. 当一个对象必须通知其他对象,但是你

    不知道

    这些对象是谁时,可以使用观察者模式。例如,你不知道哪些用户订阅了你的微信公众号,但你需要在每次发布新文章时通知所有订阅用户;

  5. 当一个对象需要有别的对象来协助完成某项工作,并且

    不希望知道

    具体协助它的对象时,可以使用观察者模式。例如,一个员工完成某项任务需要得到经理的批准,但他并不知道具体是哪个经理会批准。

观察者模式在Java和常见的框架中十分常见

在Java和Spring中,观察者模式被广泛应用。以下是几个常见的例子:


  1. Java内置的事件处理机制

    :Java提供了一套事件处理机制,其中就包括观察者模式的实现。例如,当用户点击一个按钮时,该按钮会生成一个事件(如ActionEvent),并通知所有注册的事件监听器(如ActionListener)执行相应的回调方法;


  2. Swing GUI编程框架

    :Swing是Java提供的GUI编程框架,它使用观察者模式来处理用户界面和程序之间的交互。例如,当用户输入数据时,文本框会生成一个事件(如TextEvent),并通知所有注册的事件监听器(如TextListener)执行相应的回调方法;


  3. Spring框架中的事件机制

    :Spring框架也提供了一套事件机制,其中就包括观察者模式的实现。例如,当某个业务逻辑发生变化时,可以通过发布相关的事件(如ApplicationEvent),并通知所有注册的事件监听器(如ApplicationListener)执行相应的回调方法。



版权声明:本文为Aqting原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。