19、观察者模式(Observer):事件驱动架构

观察者模式,说白了就是「我变了,你得知道」。

定义上写的是:对象间一对多的依赖关系,一个对象状态改变,所有依赖它的对象都得到通知并自动更新。嗯,这个定义很标准,但我觉得不够形象。我更喜欢这么理解——你关注了一个公众号,号主一发文章,你就收到推送。你就是观察者,号主就是被观察者。

模式结构:谁在观察谁?

观察者模式的核心角色就四个:

  • Subject(主题):被观察的对象,它维护一个观察者列表
  • Observer(观察者):接收通知的接口
  • ConcreteSubject(具体主题):真正干活的对象,状态变了就通知
  • ConcreteObserver(具体观察者):收到通知后执行自己的逻辑

我画了一张图,帮你理清它们之间的关系:

Subject(主题接口) + attach(Observer) + detach(Observer) + notify() ConcreteSubject - observers: List - state: Object + getState() Observer(观察者接口) + update() ConcreteObserver - subject: Subject + update() 通知所有观察者 持有主题引用

业务场景一:Spring 中的 ApplicationListener

Spring 的事件机制,就是观察者模式的经典实现。我个人特别喜欢这个设计,因为它让业务代码解耦得干干净净。

举个例子,用户注册成功后,需要发邮件、送积分、记录日志。如果把这些逻辑全写在注册方法里,那代码就成了一锅粥。用事件驱动的方式,注册方法只管发布一个「UserRegisteredEvent」,剩下的让监听器去处理。

// 1. 定义事件
public class UserRegisteredEvent extends ApplicationEvent {
    private String userId;
    private String email;

    public UserRegisteredEvent(Object source, String userId, String email) {
        super(source);
        this.userId = userId;
        this.email = email;
    }
    // getters...
}

// 2. 发布事件
@Service
public class UserService {
    @Autowired
    private ApplicationEventPublisher publisher;

    public void register(String username, String email) {
        // 注册逻辑...
        String userId = saveUser(username, email);
        // 发布事件
        publisher.publishEvent(new UserRegisteredEvent(this, userId, email));
    }
}

// 3. 监听事件
@Component
public class EmailNotificationListener 
        implements ApplicationListener<UserRegisteredEvent> {
    @Override
    public void onApplicationEvent(UserRegisteredEvent event) {
        sendWelcomeEmail(event.getEmail());
    }
}

// 4. 或者用 @EventListener 注解(更简洁)
@Component
public class BonusService {
    @EventListener
    public void handleUserRegistered(UserRegisteredEvent event) {
        grantSignUpBonus(event.getUserId());
    }
}

核心要点:Spring 的事件机制默认是同步的。也就是说,监听器执行完,发布者的代码才继续往下走。如果你需要异步处理,记得加 @Async 注解。

我的经验:我曾经在一个项目中,把所有业务逻辑都塞进监听器里,结果一个事件触发了十几个监听器,接口响应时间直接飙到 5 秒。后来我把耗时的操作(比如发邮件、推送通知)改成异步执行,响应时间降到了 200 毫秒。记住:监听器里不要做重活。

业务场景二:消息队列的发布-订阅模型

Kafka、RabbitMQ 这些消息中间件,本质上就是观察者模式在分布式环境下的延伸。生产者(被观察者)发布消息,消费者(观察者)订阅并处理消息。

为什么需要消息队列?说白了,就是解决「同步调用」的痛点。你想想看,如果 A 服务调用 B 服务,B 挂了,A 也跟着挂了。用消息队列,A 只管往队列里丢消息,B 自己来取,B 挂了也不影响 A。

// Kafka 生产者示例
public class OrderService {
    private KafkaTemplate<String, Order> kafkaTemplate;

    public void createOrder(Order order) {
        // 保存订单...
        // 发送消息到 "order.created" 主题
        kafkaTemplate.send("order.created", order.getOrderId(), order);
    }
}

// Kafka 消费者示例
@Component
public class InventoryService {
    @KafkaListener(topics = "order.created", groupId = "inventory-group")
    public void handleOrderCreated(Order order) {
        // 扣减库存
        deductInventory(order.getProductId(), order.getQuantity());
    }
}

@Component
public class NotificationService {
    @KafkaListener(topics = "order.created", groupId = "notification-group")
    public void handleOrderCreated(Order order) {
        // 发送通知
        sendOrderConfirmation(order.getUserId(), order.getOrderId());
    }
}

避坑指南:我曾经遇到过一个线上事故——消费者处理消息时抛了异常,结果消息一直重试,把数据库连接池打满了。后来我养成了一个习惯:在消费者入口处加 try-catch,把异常消息记录到死信队列,而不是无限重试。

业务场景三:前端框架的双向数据绑定

Vue.js 的双向绑定,底层就用到了观察者模式。Vue 通过 Object.defineProperty() 把数据变成「响应式」的,数据一变,视图自动更新。

我简单模拟一下这个机制:

// 一个极简的观察者模式实现
class Dep {
    constructor() {
        this.subscribers = new Set();
    }
    depend() {
        if (activeWatcher) {
            this.subscribers.add(activeWatcher);
        }
    }
    notify() {
        this.subscribers.forEach(watcher => watcher.update());
    }
}

let activeWatcher = null;

class Watcher {
    constructor(updateFn) {
        this.update = updateFn;
    }
    // 收集依赖时,把自己设为 activeWatcher
    collect() {
        activeWatcher = this;
        this.update(); // 触发 getter,完成依赖收集
        activeWatcher = null;
    }
}

// 使用示例
const data = { name: '张三' };
const dep = new Dep();

// 把 data.name 变成响应式
let value = data.name;
Object.defineProperty(data, 'name', {
    get() {
        dep.depend(); // 收集依赖
        return value;
    },
    set(newValue) {
        value = newValue;
        dep.notify(); // 通知更新
    }
});

// 创建一个 watcher,当 data.name 变化时更新视图
const watcher = new Watcher(() => {
    console.log('视图更新:', data.name);
});
watcher.collect(); // 收集依赖

data.name = '李四'; // 输出:视图更新:李四

设计精髓:观察者模式的核心就是「依赖收集」和「变更通知」。Vue 在编译模板时,会解析出哪些数据被模板使用了,然后建立 watcher 和数据的对应关系。数据一变,watcher 就触发视图更新。

观察者模式的优缺点

优点 缺点
解耦:主题和观察者之间是松耦合的 通知顺序不可控:多个观察者时,执行顺序不确定
支持广播通信:一对多通知 性能问题:观察者太多时,通知开销大
符合开闭原则:新增观察者无需修改主题 循环依赖:观察者更新又触发主题变化,可能死循环

我的建议:观察者模式虽然好用,但别滥用。如果一个事件只有一两个观察者,直接用方法调用更简单。观察者模式的价值在于「一对多」和「解耦」,用对了地方才是好模式。

好了,观察者模式就聊到这儿。记住它的核心思想:状态变化时,自动通知所有关注者。Spring 的事件、Kafka 的消息、Vue 的数据绑定,都是这个思路。下次你在项目中遇到「某个动作发生后,需要触发一系列后续操作」的场景,不妨想想观察者模式。


公众号:蓝海资料掘金营,微信deep3321