行为型设计模式之观察者(发布-订阅)模式

基本概念

行为型模式

设计模式分为以下三种:

  • 创建型模式:核心解决对象的创建问题,如工厂模式、单例模式等;
  • 结构型模式:专注对象的组合问题,如代理模式、适配器模式;
  • 行为型模式:负责规范对象间的交互方式,如模板方法、命令模式、订阅发布模式。

行为型设计模式,主要用于规范对象之间的交互方式、划分对象职责、封装行为与算法,聚焦运行时对象的通信、行为流转、状态变化、算法替换,解决不同对象如何协同完成业务逻辑的问题,既包含多对象协作,也包含单一对象的行为管控。

观察者模式

观察者模式,俗称发布-订阅模式,该模式定义了一对多的依赖关系,多个观察者对象监听同一个主题对象。当主题对象状态发生改变时,会主动通知所有已注册的观察者,观察者接收通知后自动执行更新逻辑。

角色与示例

角色

观察者模式中应包含以下角色:

  1. 抽象主题:提供增删观察者对象的接口方法;
  2. 具体主题:实现增删方法,声明观察者类型列表;
  3. 抽象观察者:定义更新接口,监听主题更改时更新自身;
  4. 具体观察者:实现抽象观察者接口。

代码结构为:

  1. 观察者定义更新方法,接受消息时更新自身进行处理;
  2. 主题类聚合观察者,提供增删及观察者的通知方法;
  3. 聚合的观察者通常为列表,增删方法用于维护观察者,通知则是遍历列表集合,分别调用其update方法。

示例------微信公众号推送

微信公众号是经典的发布订阅模式体现,公众号更新时,新内容推送给关注公众号的用户端,公众号为主题,用户端为观察者,整体类图如下:

观察者代码:

java 复制代码
// 观察者接口
public interface Observer {
    // 观察者更新自身
    void update(String message);
}

public class WeiXinUser implements Observer{

    private String name;

    public WeiXinUser(String name) {
        this.name = name;
    }

    @Override
    public void update(String message) {
        System.out.println("WeiXinUser: " + this.name + "收到推文 " +message);
    }
}

主题代码:

java 复制代码
// 主题接口
public interface Subject {
    // 增加观察者
    void attach(Observer observer);
    // 删除观察者
    void detach(Observer observer);
    // 更新通知观察者
    void notifyObservers(String message);
}

import java.util.ArrayList;

public class SubscriptionSubject implements Subject{
    ArrayList<Observer> observers = new ArrayList<Observer>();

    @Override
    public void attach(Observer observer) {
        observers.add(observer);
    }

    @Override
    public void detach(Observer observer) {
        observers.remove(observer);
    }

    @Override
    public void notifyObservers(String message) {
        for (Observer observer : observers) {
            observer.update(message);
        }
    }
}

使用如下代码进行测试:

java 复制代码
public static void main(String[] args) {
    SubscriptionSubject subject = new SubscriptionSubject();
    subject.attach(new WeiXinUser("张三"));
    subject.attach(new WeiXinUser("李四"));
    subject.attach(new WeiXinUser("王五"));

    subject.notifyObservers("《震惊!竟然有人2026年还手搓代码?》");
}

输出为:

WeiXinUser: 张三收到推文 《震惊!竟然有人2026年还手搓代码?》

WeiXinUser: 李四收到推文 《震惊!竟然有人2026年还手搓代码?》

WeiXinUser: 王五收到推文 《震惊!竟然有人2026年还手搓代码?》

模式分析

优缺点

优点:

  1. 降低耦合关系,主题虽聚合观察者,但为抽象耦合;
  2. 可实现广播机制。

缺点:

  1. 观察者较多时,逐个通知耗时较长;
  2. 有循环依赖时会产生循环调用,导致系统崩溃,需梳理依赖关系。

使用场景

适用于对象间存在一对多关系,一个改变影响其他对象,或抽象模型的两个方面,一个方面依赖于另一方面的场景。

工程实践

JDK实现

JDK提供了观察者模式的实现方法:

  • 主题为java.util.Observable,该类通过Vector作为观察者列表,提供add、delete方法用于增删观察者,notify方法用于通知,由以下源码,后加入的观察者会被先通知:

    java 复制代码
        if (!changed)
                return;
            // 列表转为数组
            arrLocal = obs.toArray();
            clearChanged();
        }
    	// 倒序通知
        for (int i = arrLocal.length-1; i>=0; i--)
            ((Observer)arrLocal[i]).update(this, arg);

    使用方法如下:

    java 复制代码
    import java.util.Observable;
    
    // 被观察者:商品
    class Product extends Observable {
    	private String name;
    	private double price;
    
        public Product(String name, double price) {
            this.name = name;
            this.price = price;
        }
    
        // 改价格 → 通知所有观察者
        public void setPrice(double newPrice) {
            this.price = newPrice;
    
            setChanged();   // 标记:状态变了(必须!)
            notifyObservers(price); // 把新价格当 arg 传过去
        }
    
        public double getPrice() {
            return price;
        }
    
        public String getName() {
            return name;
        	}
    }
  • 观察者为java.util.Observer,具体观察者实现该方法即可,示例代码如下:

    java 复制代码
    import java.util.Observer;
    import java.util.Observable;
    
    // 观察者:用户
    class User implements Observer {
        private String userName;
    
    public User(String userName) {
        this.userName = userName;
    }
    
    // 回调:主题变了 → 自动调用 update
    @Override
    public void update(Observable o, Object arg) {
        // o:是谁通知我的?(那个 Product)
        // arg:它捎来的数据(新价格)
    
        if (o instanceof Product) {
            Product p = (Product) o;
            double newPrice = (Double) arg;
    
            System.out.println(userName + " 收到:"
                    + p.getName() + " 降价了!新价格:" + newPrice);
        }
    }
    }

相比前面手工推送的方法,JDK实现主要有以下三点不同:

  1. 设置changed标识进行手动确认
    notify通常结合set方法使用,每次修改都自动同步,可通过该标识结合判断逻辑减小通知开销,示例代码如下:
java 复制代码
public void setPrice(double p) {
    if (Math.abs(p - this.price) > 0.01) { // 业务判断:才算真变化
        this.price = p;
        setChanged(); // ✅ 手动标记:我变了,需要通知
    }
    notifyObservers(price); // 内部会检查 changed,没 set 就不发
}
  1. obs.toArray() 转数组保证线程安全

    toArray把当前时刻的观察者列表vector复制一份到新数组(获取的是引用副本),避免其他线程增删观察者造成不一致影响,同时锁只在拷贝过程中生效,提高并发效率。

  2. 倒序通知

    遗留设计,早期正序通知后删除观察者会导致Vector大小发生变化,倒序则不会影响前面还没遍历到的下标,但目前使用array数组副本,不会对业务造成影响。

Spring实现

Spring中该模式称为事件驱动,在JDK的基础上作了进一步封装,用户只需指定事件(主题)、监听者、发布者即可,代码层面主题与监听者不直接相关,简单示例如下:

java 复制代码
// 继承ApplicationEvent类,形成主题事件
public class Event extends ApplicationEvent {
    private String message;

    public Event(Object source,String message) {
        super(source);
        this.message = message;
    }

    public String getMessage() {
        return this.message;
    }
}

@Component
public class Listener {
    // 监听器处理事件
    @EventListener
    public void dealEvent(Event event){
        System.out.println("监听器收到消息:"+event.getMessage());
    }
}

@Component
public class Publisher {
    @Autowired
    private ApplicationEventPublisher eventPublisher;

    // 发布事件
    public void publishEvent(String message){
        eventPublisher.publishEvent(new Event(this,message));
    }
}

// 实现CommandLineRunner接口,Spring启动自动执行run方法
@Component
public class PublisherTest implements CommandLineRunner {

    @Autowired // 直接从 Spring 拿
    private Publisher publisher;

    @Override
    public void run(String... args) throws Exception {
        publisher.publishEvent("Hello World");
    }
}

启动类启动后可在控制台看见如下输出:
监听器收到消息:Hello World

整体流程如下图:

该模式下Event仅作为数据载体,发布由Publisher完成,底层通过Spring广播器Multicaster实现。

相比JDK原生循环推送的方式,Spring引入了Publisher,有以下三点优势:

  • 解耦发布逻辑和业务逻辑 ,业务代码只需要调用 publisher.publishEvent(event),完全不感知监听者是谁、有多少、如何处理;
  • 发布逻辑Spring统一管理,事件交给Spring广播器,无需手写循环与线程调度;
  • 避免业务代码直接持有监听器引用,避免业务代码直接持有所有监听器的引用,新增/删除监听器无需修改发布方代码,符合开闭原则。

有关EventListener注解需要进一步说明:

  • @EventListener自动按参数类型匹配
  • @EventListener(classes = {UserEvent.class, OrderEvent.class})指定监听多个事件
  • @EventListener(condition = "#event.message.contains('test')")条件过滤,只监听 message 包含 "test" 的事件
  • 额外增加@Async,异步执行,@Async("线程池")可使用线程池异步执行,否则在主线程执行
  • @Order(1)指定多个监听器的执行顺序

总结

本文介绍了行为型设计模式的观察者模式,核心是主题变化时自动同步更新到观察者,但严格意义上观察者模式 ≠ 发布 - 订阅模式,二者思想同源但架构不同:

  • 观察者模式:运行于同一应用内,主题与观察者存在间接依赖,主题状态变更后直接通知所有观察者,属于松耦合。代表技术:JDK Observable、Spring 事件驱动。
  • 发布 - 订阅模式:通过中间件实现完全解耦,发布者与订阅者不直接通信、互不感知,支持跨服务、跨进程、跨机器。代表技术:RabbitMQ、Kafka、Redis 发布订阅。

有关发布订阅模式的实际项目可见:英语四六级证书审核

相关推荐
王_teacher2 小时前
23种设计模式全解析(GoF 设计模式)
设计模式·软考·软件设计师·软考中级
阿坤带你走近大数据2 小时前
分别介绍下java主流的开发框架、设计模式与对应编程语言的高级特性
java·开发语言·设计模式
geovindu3 小时前
go: Coroutines Pattern
开发语言·后端·设计模式·golang·协程模式
Anastasiozzzz3 小时前
构建健壮软件系统的基石:深入解析面向对象设计七大原则
开发语言·javascript·设计模式·ecmascript
qq_297574671 天前
设计模式系列文章(基础篇第19篇):中介者模式——封装交互关系,解耦网状依赖
设计模式·交互·中介者模式
AI大法师1 天前
老牌媒体怎么从“出版物更新”走到“品牌系统升级”
大数据·人工智能·设计模式·新媒体运营
野生技术架构师1 天前
Java 23 种设计模式:从踩坑到精通 —— 开篇及系列介绍
java·开发语言·设计模式
艾利克斯冰1 天前
Java设计模式-创建型模式(更新完成)
设计模式
王_teacher1 天前
23种设计模式之工厂模式
设计模式·软件工程·简单工厂模式·工厂方法模式·抽象工厂模式