观察者模式:一个对象变化,一群对象自动响应

引言

你一定遇到过这样的需求:用户注册成功后,既要"发欢迎邮件",又要"送新人优惠券",还要"记录注册日志",甚至以后可能还要"发短信通知"。如果把这些逻辑全部硬塞在注册方法里:

java

复制代码
public void register(User user) {
    // 保存用户
    saveUser(user);
    // 发邮件
    sendEmail(user);
    // 送券
    sendCoupon(user);
    // 记日志
    writeLog(user);
    // 以后加需求还得回来改这里......
}

每加一个后续动作,都要回来改这个方法,注册逻辑和一堆"副作用"死死耦合在一起。观察者模式(Observer Pattern) ​就是来解决"一个对象状态变化,需要自动通知一群其他对象"这类问题的。它属于行为型设计模式,也是"发布-订阅"机制的鼻祖。

一、什么是观察者模式

观察者模式的核心思想:**定义对象间的一种一对多依赖关系,当一个对象(主题/被观察者)状态改变时,所有依赖它的对象(观察者)都会得到通知并自动更新。**​

两个核心角色:

  • 主题(Subject / Observable):被观察的对象,维护一个观察者列表,提供注册、移除、通知的方法。
  • 观察者(Observer):依赖主题的对象,当主题状态变化时收到通知并作出响应。

打个比方:你关注了一个公众号(订阅),公众号一发文,你和所有其他关注者都会收到推送。公众号是"主题",你们是"观察者"。这就是观察者模式,也就是常说的"发布-订阅"。

二、自定义实现观察者模式

用一个"气象站"的经典例子:气象站(主题)实时更新气温,多个显示面板(观察者)收到通知后各自展示。

2.1 定义观察者接口

java

复制代码
public interface Observer {
    void update(float temperature);
}

2.2 定义主题接口

java

复制代码
public interface Subject {
    void registerObserver(Observer observer);   // 注册观察者
    void removeObserver(Observer observer);     // 移除观察者
    void notifyObservers();                     // 通知所有观察者
}

2.3 实现具体主题(气象站)

java

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

public class WeatherStation implements Subject {
    private final List<Observer> observers = new ArrayList<>();
    private float temperature;

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

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

    @Override
    public void notifyObservers() {
        for (Observer observer : observers) {
            observer.update(temperature); // 把最新数据推给每个观察者
        }
    }

    // 气温变化时,更新数据并通知所有观察者
    public void setTemperature(float temperature) {
        this.temperature = temperature;
        notifyObservers();
    }
}

2.4 实现具体观察者

java

复制代码
public class PhoneDisplay implements Observer {
    private final String name;

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

    @Override
    public void update(float temperature) {
        System.out.println(name + " 显示当前气温:" + temperature + "℃");
    }
}

2.5 使用

java

复制代码
public class Main {
    public static void main(String[] args) {
        WeatherStation station = new WeatherStation();

        // 两个面板订阅了气象站
        station.registerObserver(new PhoneDisplay("手机面板"));
        station.registerObserver(new PhoneDisplay("电脑面板"));

        // 气温变化,两个面板自动收到通知
        station.setTemperature(25.5f);
        // 手机面板 显示当前气温:25.5℃
        // 电脑面板 显示当前气温:25.5℃
    }
}

好处 :以后要新增一个"电视面板"观察者,只需 registerObserver 一个新观察者,完全不改气象站和其他观察者的代码,符合开闭原则。

三、观察者模式的两种通知方式

  1. **推模型(Push)**​:主题主动把所有数据"推"给观察者(上面例子就是)。优点是观察者简单,缺点是主题把所有数据都推过去,观察者可能只用到其中一部分。

  2. **拉模型(Pull)**​:主题只通知"我变了",观察者自己按需"拉"取关心的数据。更灵活,但观察者要持有主题的引用。

java

复制代码
// 拉模型:update 不带数据,观察者自己来取
public interface Observer {
    void update();
}
// 观察者里:
@Override
public void update() {
    float temp = weatherStation.getTemperature(); // 自己拉数据
}

实际开发中两种都有,按需选择。

四、实际应用:Spring 事件机制

Spring 提供了一套完整的观察者模式实现------事件监听机制 ,ApplicationEvent + ApplicationListener + @EventListener。回到引言里的"注册后发邮件、送券、记日志",用 Spring 事件可以非常优雅地解耦:

4.1 定义事件

java

复制代码
public class UserRegisterEvent extends ApplicationEvent {
    private final User user;

    public UserRegisterEvent(Object source, User user) {
        super(source);
        this.user = user;
    }

    public User getUser() {
        return user;
    }
}

4.2 发布事件

java

复制代码
@Service
public class UserService {

    @Autowired
    private ApplicationEventPublisher publisher;

    public void register(User user) {
        saveUser(user); // 核心逻辑
        // 只发布事件,不关心谁在监听
        publisher.publishEvent(new UserRegisterEvent(this, user));
    }
}

4.3 多个监听器(观察者)各自处理

java

复制代码
@Component
public class EmailListener {
    @EventListener
    public void onRegister(UserRegisterEvent event) {
        System.out.println("给 " + event.getUser().getName() + " 发送欢迎邮件");
    }
}

@Component
public class CouponListener {
    @EventListener
    public void onRegister(UserRegisterEvent event) {
        System.out.println("给 " + event.getUser().getName() + " 发放新人优惠券");
    }
}

@Component
public class LogListener {
    @EventListener
    public void onRegister(UserRegisterEvent event) {
        System.out.println("记录注册日志:" + event.getUser().getName());
    }
}

现在 register 方法干干净净只做核心业务,发邮件、送券、记日志这些"副作用"分散到了各自的监听器里,新增动作只需加一个监听器,彻底解耦。**这就是观察者模式在 Spring 里的最佳实践。**​

另外,结合 @Async 还能让这些监听器异步执行,进一步提升性能(这点在你前面写过的 AOP、统一处理那几篇里也提到过)。

五、观察者模式 vs 发布-订阅模式

很多人把这两个当成一回事,严格来说有细微差别:

对比项 观察者模式 发布-订阅模式
耦合方式 主题直接持有观察者,二者互相知道对方 通过"事件中心/消息中间件"中转,双方互不知道
通知机制 同步直接调用 通常异步、经消息队列
典型实现 Java Observable、Spring 事件 MQ(RocketMQ、Kafka)、Redis 发布订阅

简单理解:**观察者模式是"点对点"的松耦合,发布-订阅是经过"中间人"的更彻底解耦。**​ 观察者模式可以看作发布-订阅在"单应用内"的简化版。

总结

观察者模式是实现"一对多自动通知、模块解耦"的利器:

  1. 核心:主题维护观察者列表,状态变化时逐一通知;观察者自动响应。
  2. 两种方式:推模型(主题推数据)和拉模型(观察者自己取)。
  3. Spring 事件机制 是观察者模式的最佳实践------ApplicationEvent + @EventListener,把核心业务和"邮件、优惠券、日志"等副作用彻底解耦,新增功能只加监听器不动主流程。
  4. 与发布-订阅的区别:观察者是单应用内的直接通知,发布-订阅经过消息中间件中转、更彻底解耦。

一句话总结:当一个对象变化需要带动一群对象响应时,别把逻辑都堆在变化的方法里,用观察者模式把"谁响应、怎么响应"分离出去。

相关推荐
文丑颜不良啊24 分钟前
LeetCode2441~2460题目解析
java·leetcode
阿屿1 小时前
从零手搓 AI 伴侣(二):好感度通胀危机,与它的「货币政策」
java·人工智能
灿飞1 小时前
Kotlin与Java虚拟机十一:类与属性:构造顺序、backing field 与显式 backing field
java·后端·kotlin
aiks_yuan1 小时前
40亿个QQ号 1G内存怎么去重:Java BitSet 装不下,long 数组位图 512MB 实测 46 秒(JDK 17)
java
cjz38991 小时前
规则树:把一百行if-else拆成一棵树
设计模式
文丑颜不良啊2 小时前
LeetCode2521~2540题目解析
java·leetcode
杨凯凡2 小时前
Spring Framework 7 HTTP Interface 实战:声明式 RestClient、超时与测试
java·spring·http
吴名氏.2 小时前
Java转AI Agent教程(第十二周)检索与切分
java·开发语言·人工智能·ai·ai agent
瑞雪兆丰年兮2 小时前
JavaWeb-day10-SpringBootWeb案例
java·开发语言·javaweb·web
Rain的Java大神之路3 小时前
你背的Spring答案,全是5.x时代的:7.0.8源码一翻,80%的博客都过时了
java·后端·面试