什么是观察者模式?应用场景有哪些?


面试考察点

  1. 设计思想理解:面试官不仅仅是想知道 "被观察者变了,观察者自动收到通知" 这句话,更是想看你是否理解 "一对多的依赖关系" 以及 "松耦合" 这个核心设计目标。

  2. 推模式 vs 拉模式:能否区分这两种数据传递方式,以及各自的优缺点。这属于进阶考点,答好了能加分。

  3. 框架实战关联:能否把观察者模式和 Spring Event、Guava EventBus、JDK Observable 等实际技术联系起来,证明你不只是纸上谈兵。

核心答案

一句话定义:观察者模式定义了对象之间 "一对多" 的依赖关系——当一个对象(被观察者)的状态发生变化时,所有依赖于它的对象(观察者)都会自动收到通知并更新。

打个比方:微信群就是一个典型的观察者模式。群里有人发消息(被观察者状态变化),所有群成员(观察者)都能立刻收到通知。你作为群成员不需要主动去问 "有人发新消息了吗",消息会自动推送到你面前。

上面的图展示了观察者模式的核心结构:

  • Subject(被观察者/主题):维护一个观察者列表,提供注册(attach)、注销(detach)和通知(notify)三个核心方法。状态变化时负责通知所有注册的观察者。
  • Observer(观察者):定义一个统一的更新接口(update),收到通知后执行自己的业务逻辑。
  • 核心关系:一个被观察者对应多个观察者,这就是 "一对多"。被观察者不知道观察者的具体实现,只管调用 update(),实现了完全解耦。

深度解析

一、手写观察者模式

先用代码把核心结构写出来,一看就明白了。

// 观察者接口
public interface Observer {
    void update(String message);
}

// 被观察者(主题)
public class Subject {

    private List<Observer> observers = new ArrayList<>();
    private String state;

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

    // 注销观察者
    public void detach(Observer observer) {
        observers.remove(observer);
    }

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

    // 状态变更时自动通知
    public void setState(String state) {
        this.state = state;
        System.out.println("被观察者状态变更:" + state);
        notifyObservers();
    }
}

定义几个具体的观察者:

// 观察者 A:邮件通知
public class EmailObserver implements Observer {
    @Override
    public void update(String message) {
        System.out.println("[邮件] 收到通知:" + message);
    }
}

// 观察者 B:短信通知
public class SmsObserver implements Observer {
    @Override
    public void update(String message) {
        System.out.println("[短信] 收到通知:" + message);
    }
}

// 观察者 C:App 推送
public class AppObserver implements Observer {
    @Override
    public void update(String message) {
        System.out.println("[App 推送] 收到通知:" + message);
    }
}

使用:

Subject subject = new Subject();

// 注册观察者
subject.attach(new EmailObserver());
subject.attach(new SmsObserver());
subject.attach(new AppObserver());

// 状态变更,自动通知所有观察者
subject.setState("订单已发货");

输出:

被观察者状态变更:订单已发货
[邮件] 收到通知:订单已发货
[短信] 收到通知:订单已发货
[App 推送] 收到通知:订单已发货

看到没?Subject 完全不知道有哪些观察者、它们具体做什么。它只管调用 update(),至于发邮件、发短信还是 App 推送,那是观察者自己的事。新增通知渠道只需要写一个实现 Observer 的类,然后 attach 上去就行,被观察者的代码一行不用改。这就是松耦合的魅力。

二、推模式 vs 拉模式

这是面试中的一个细节考点。观察者模式有两种数据传递方式:

对比项推模式(Push)拉模式(Pull)
数据流向被观察者主动把数据推给观察者被观察者只通知 "变了",观察者自己来拉数据
update() 参数update(String data)update(Subject subject)
优点观察者不用关心细节,拿来就用观察者按需获取,更灵活
缺点可能推送观察者不需要的数据观察者需要了解被观察者的内部结构

上面手写的代码就是推模式——直接把 state 传给观察者。如果改成拉模式,update() 的参数就是 Subject 本身,观察者自己去调用 subject.getState() 获取需要的数据。

实际开发中推模式用得更多,因为耦合更低。

三、Spring Event 中的观察者模式

Spring 内置了一套事件机制,本质就是观察者模式。这比你自己手写要优雅得多。

// 1. 定义事件
public class OrderCreatedEvent extends ApplicationEvent {
    private String orderId;

    public OrderCreatedEvent(Object source, String orderId) {
        super(source);
        this.orderId = orderId;
    }

    public String getOrderId() {
        return orderId;
    }
}

// 2. 发布事件(被观察者)
@Service
public class OrderService {

    @Autowired
    private ApplicationEventPublisher publisher;

    public void createOrder(String orderId) {
        System.out.println("创建订单:" + orderId);
        // 发布事件,通知所有监听器
        publisher.publishEvent(new OrderCreatedEvent(this, orderId));
    }
}

// 3. 监听事件(观察者 A:发邮件)
@Component
public class EmailNotifier {

    @EventListener
    public void onOrderCreated(OrderCreatedEvent event) {
        System.out.println("[邮件] 订单 " + event.getOrderId() + " 已创建,发送邮件通知");
    }
}

// 4. 监听事件(观察者 B:扣库存)
@Component
public class StockService {

    @EventListener
    public void onOrderCreated(OrderCreatedEvent event) {
        System.out.println("[库存] 订单 " + event.getOrderId() + " 已创建,扣减库存");
    }
}

调用 orderService.createOrder("ORD001") 后输出:

创建订单:ORD001
[邮件] 订单 ORD001 已创建,发送邮件通知
[库存] 订单 ORD001 已创建,扣减库存

Spring Event 的好处是:发布者和监听者完全解耦,通过 @EventListener 注解就能声明观察者,不用手动维护观察者列表。而且支持异步事件,加上 @Async 注解就行。

需要注意,Spring Event 默认是同步的——publishEvent() 会阻塞等待所有监听器执行完毕。如果你不希望主流程被拖慢,记得加 @Async

四、Guava EventBus

如果项目没用到 Spring,或者想在非 Spring 环境用观察者模式,Google 的 Guava 提供了一个轻量级的事件总线——EventBus,用起来非常舒服。

// 创建事件总线
EventBus eventBus = new EventBus();

// 注册观察者
eventBus.register(new EmailListener());
eventBus.register(new SmsListener());

// 发布事件
eventBus.post("订单已发货");

观察者只需要在方法上加 @Subscribe 注解:

public class EmailListener {
    @Subscribe
    public void onEvent(String message) {
        System.out.println("[邮件] " + message);
    }
}

比手写观察者模式简洁多了,而且支持异步(AsyncEventBus)。

五、实际应用场景

场景说明
事件驱动系统下单后触发发邮件、扣库存、发短信等一系列操作
消息推送用户关注某个话题后,有新内容自动推送
GUI 事件监听按钮点击、输入框内容变化等事件的响应
日志/监控系统状态变更后自动记录日志、触发告警
Spring 事件机制@EventListener 监听各种 Spring 事件
RxJava/Reactor响应式编程的核心就是观察者模式
ZooKeeper Watcher节点数据变更时自动通知客户端

六、观察者模式的注意事项

生产环境用观察者模式有几个坑要踩:

  • 内存泄漏:观察者注册后忘记注销,导致被观察者一直持有观察者的引用,GC 回收不了。Spring Event 用 @Component 管理生命周期还好,手写的话尤其要注意。
  • 通知顺序不确定:如果观察者之间有先后依赖,手写模式不保证顺序。需要顺序的话用有序集合或者直接用 Spring 的 @Order 注解。
  • 级联触发:观察者 A 的 update() 又触发了事件,导致观察者 B 被通知,B 又触发事件……形成循环调用。这在复杂系统里是个大坑。

面试高频追问

  1. 观察者模式和发布订阅模式是一回事吗?

    • 严格来说不是。经典观察者模式中,被观察者直接通知观察者(彼此知道对方)。发布订阅模式引入了一个 "事件总线" 作为中间层,发布者和订阅者完全不知道对方的存在,解耦更彻底。Spring Event 和 Guava EventBus 更偏向发布订阅模式。
  2. Spring Event 是同步还是异步的?

    • 默认同步。publishEvent() 会等所有 @EventListener 执行完才返回。想要异步,在监听方法上加 @Async,并确保项目中开启了 @EnableAsync
  3. 观察者模式怎么处理异常?

    • 一个观察者抛异常不应该影响其他观察者。notifyObservers() 里需要给每个观察者的 update()try-catch,单独处理。Spring Event 也是这样,某个监听器报错不会阻断其他监听器。

常见面试变体

  • "手写一个观察者模式"
  • "Spring 事件机制的原理是什么?"
  • "观察者模式和发布订阅模式有什么区别?"
  • "你在项目中哪里用到了观察者模式?"

记忆口诀

核心思想:一变多变,自动通知;注册注销,松耦合。

推 vs 拉:推模式把数据塞给你,拉模式你自己来拿。

与发布订阅的区别:观察者直连,发布订阅隔了个中间人。

总结

观察者模式的精髓就是 "一对多依赖,状态变化自动通知"。面试时先说概念,手写一个简单实现,然后重点展开 Spring Event 的用法,最后提一嘴推/拉模式和发布订阅的区别。能把 Spring Event 讲明白的候选人,面试官基本就认定你是有实战经验的。