设计模式祛魅之【观察者模式】——你不知道的 Java(9)

代码上线就出事,黑锅总往身上背?那是 Java 里那些“你不知道”的细节在作祟!你确定完全掌握 Java 了吗?那些看似基础的操作背后,可能隐藏着你从未触及的深度。《你不知道的 Java》 专栏,专挖这些隐藏知识点,每日一个知识点,助你写出真正健壮、少出问题的代码,从此告别救火队员,安心下班!

本期 2.5W 字长文,强烈推荐收藏,随时查阅!力求讲清楚,从此对设计模式祛魅,结合自身经验告诉你设计模式真的很重要!另外最后附有设计模板公式,可直接复用。

说到设计模式,是不是感觉既熟悉又陌生?熟悉的是面试题里总有它的身影,陌生的是实际项目中好像用得不多,或者用了也不知道是它?尤其是观察者模式,听起来好像挺玄乎,什么“发布-订阅”、“依赖倒置”… 别怕,今天咱们就把它扒个精光,让它从“神坛”走下来,变成你我手中的利器!


1. 基础回忆

在咱们撸起袖子干观察者模式之前,先快速扫盲一下设计模式的三大家族,有个全局观总没错:

  1. 创建型模式 (Creational Patterns): 关注点是“怎么创建对象?”。
  2. 结构型模式 (Structural Patterns): 关注点是“怎么组合类和对象?”。它们的目标是让类和对象的组合更强大、更灵活。
  3. 行为型模式 (Behavioral Patterns): 关注点是“对象之间怎么交互和分配职责?”。它们的目标是让对象间的通信更清晰、更高效。咱们今天的主角——观察者模式就属于这个家族。

好了,热身完毕,现在正式进入主题:观察者模式 (Observer Pattern)


2. 基础理论

2.1 什么是观察者模式?

想象一下你关注了某个技术大佬的公众号(比如我这个,咳咳…)。大佬(Subject/Observable,被观察者/主题)一发布新文章,微信(平台机制)就会自动通知所有关注了他的粉丝(Observer,观察者)。你作为粉丝,只需要做一件事:关注(Attach/Register)。等文章来了,你就收到通知,然后去阅读(Update)。如果哪天你不想关注了,取关(Detach/Unregister)就行。

核心思想: 定义对象间的一种 一对多 的依赖关系,当一个对象(Subject)的状态发生改变时,所有依赖于它的对象(Observers)都将得到 自动通知 并更新。

主要角色:

  • Subject (主题/被观察者):
    • 知道它的所有观察者。可以有任意多个观察者。
    • 提供用于**添加(attach/register)删除(detach/unregister)**观察者的接口。
    • 状态改变时,**通知(notify)**所有注册的观察者。
  • Observer (观察者):
    • 定义一个**更新(update)**接口,当收到主题的通知时,这个接口会被调用。
  • ConcreteSubject (具体主题):
    • 存储具体状态。
    • 当其状态改变时,向其注册的所有观察者发出通知。
  • ConcreteObserver (具体观察者):
    • 维护一个指向 ConcreteSubject 对象的引用(或者知道如何获取需要的数据)。
    • 存储有关主题的状态,这些状态应与主题的状态保持一致。
    • 实现 Observer 的更新接口,以使自身状态与主题的状态保持一致。

图示(简化版):

notifies
notifies
notifies
knows
knows
knows
Change State
Observer1
Observer2
ObserverN
Client

2.2 底层原理

观察者模式的精髓在于 解耦 (Decoupling)

想象一下没有观察者模式的场景:大佬发文章,得自己维护一个粉丝列表(硬编码或者配置文件?),然后挨个去调用粉丝的“接收文章”方法。如果新增一个粉丝,或者某个粉丝的接收方式变了(比如从微信通知改成邮件),大佬就得改自己的代码!这耦合度,简直高得令人发指!大佬只想安安静静写文章,不想管你们这些粉丝的破事啊!

用了观察者模式后:

  1. 主题和观察者解耦: 主题只知道它有一堆实现了 Observer 接口的对象。它不关心这些对象具体是谁,是干嘛的。观察者也只知道它需要关注一个 Subject,并在收到通知时做自己的事。双方可以独立地变化和扩展。
  2. 易于扩展: 想增加新的观察者(比如,大佬发文后,除了通知粉丝,还想自动同步到微博)?只需要实现一个新的 Observer 类,并注册到主题上即可,完全不需要修改主题的代码。符合 开闭原则 (Open/Closed Principle)
  3. 支持广播通信: 主题状态一变,所有观察者都能收到通知,非常适合需要“广而告之”的场景。

简单说,观察者模式就是用一种约定的方式(接口),让“被动方”(观察者)主动去“订阅”信息源(主题),而信息源在变化时,只需要按照约定去“广播”,不需要关心谁在听,怎么听。


3. 最佳实践

3.1 公式模版

Talk is cheap, show me the code! 咱们来看一个最经典的观察者模式实现结构:

import java.util.ArrayList;
import java.util.List;

// 1. 定义观察者接口
interface Observer {
    void update(String message); // 当主题状态改变时,调用此方法
}

// 2. 定义主题接口 (或者抽象类)
interface Subject {
    void attach(Observer observer); // 添加观察者
    void detach(Observer observer); // 移除观察者
    void notifyObservers(String message); // 通知所有观察者
}

// 3. 实现具体主题
class ConcreteSubject implements Subject {
    private List<Observer> observers = new ArrayList<>();
    private String state; // 主题的状态

    public String getState() {
        return state;
    }

    // 状态改变时,通知所有观察者
    public void setState(String state) {
        this.state = state;
        System.out.println("主题状态改变为: " + state);
        notifyObservers("主题状态已更新为: " + state);
    }

    @Override
    public void attach(Observer observer) {
        if (!observers.contains(observer)) {
            observers.add(observer);
            System.out.println("观察者 " + observer.getClass().getSimpleName() + " 已添加.");
        }
    }

    @Override
    public void detach(Observer observer) {
        if (observers.remove(observer)) {
            System.out.println("观察者 " + observer.getClass().getSimpleName() + " 已移除.");
        }
    }

    @Override
    public void notifyObservers(String message) {
        System.out.println("开始通知所有观察者...");
        // 遍历通知 - 注意:迭代时直接删除可能引发 ConcurrentModificationException,实际应用中可能需要复制列表或使用迭代器安全删除
        for (Observer observer : new ArrayList<>(observers)) { // 使用副本以避免并发修改问题
             observer.update(message);
        }
        System.out.println("通知完毕.");
    }
}

// 4. 实现具体观察者 A
class ConcreteObserverA implements Observer {
    @Override
    public void update(String message) {
        System.out.println("ConcreteObserverA 收到通知: " + message + " -> 执行A逻辑");
        // 这里可以做具体的操作,比如更新UI、记录日志等
    }
}

// 5. 实现具体观察者 B
class ConcreteObserverB implements Observer {
    @Override
    public void update(String message) {
        System.out.println("ConcreteObserverB 收到通知: " + message + " -> 执行B逻辑");
        // 做其他操作
    }
}

// 客户端代码 (演示如何使用)
public class ObserverDemo {
    public static void main(String[] args) {
        // 创建主题
        ConcreteSubject subject = new ConcreteSubject();

        // 创建观察者
        Observer observerA = new ConcreteObserverA();
        Observer observerB = new ConcreteObserverB();

        // 注册观察者
        subject.attach(observerA);
        subject.attach(observerB);

        System.out.println("\n--- 第一次状态变更 ---");
        // 改变主题状态,观察者会自动收到通知
        subject.setState("数据更新V1.0");

        System.out.println("\n--- 移除观察者A ---");
        subject.detach(observerA);

        System.out.println("\n--- 第二次状态变更 ---");
        // 再次改变主题状态
        subject.setState("数据更新V2.0 - A已收不到");

    }
}

运行结果大概是这样:

观察者 ConcreteObserverA 已添加.
观察者 ConcreteObserverB 已添加.

--- 第一次状态变更 ---
主题状态改变为: 数据更新V1.0
开始通知所有观察者...
ConcreteObserverA 收到通知: 主题状态已更新为: 数据更新V1.0 -> 执行A逻辑
ConcreteObserverB 收到通知: 主题状态已更新为: 数据更新V1.0 -> 执行B逻辑
通知完毕.

--- 移除观察者A ---
观察者 ConcreteObserverA 已移除.

--- 第二次状态变更 ---
主题状态改变为: 数据更新V2.0 - A已收不到
开始通知所有观察者...
ConcreteObserverB 收到通知: 主题状态已更新为: 数据更新V2.0 - A已收不到 -> 执行B逻辑
通知完毕.

看到没?主题只管改状态和喊一嗓子(notifyObservers),观察者们自动响应。添加、移除观察者对主题来说也很简单。解耦!优雅!

理论成立,实践开始!

3.2 最佳实践

光看模板不过瘾,咱们上点实际工作中用得到的例子。

3.2.1 用户注册:短信通知与优惠券发放(同步/异步请求解耦)

场景: 用户注册成功后,核心流程是创建用户记录。但通常还需要:

  1. 发送注册成功短信。(即时,同步请求)
  2. 给用户发放新人优惠券。(可容忍延时,异步请求)

直接在注册逻辑里写发送短信和发券的代码?耦合太高!如果以后要加“发送欢迎邮件”、“初始化积分”呢?注册方法会越来越臃肿。

这里发送优惠劵不是那么要求即时性,那么大家可能首先会想到用 MQ 消息处理,将业务解耦,达到异步请求的目的。但其实这里也可以用观察者模式解决。

观察者模式解决方案:

  • 主题 (Subject): UserServiceUserRegistrationService。它在用户成功注册后,触发一个“用户已注册”事件。
  • 观察者 (Observers):
    • SmsNotifierObserver: 监听“用户已注册”事件,收到通知后调用短信服务发送短信。
    • CouponIssuerObserver: 监听“用户已注册”事件,收到通知后调用优惠券服务发放优惠券。

代码示意 (核心逻辑):

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

// 事件对象 (可选,可以传递更丰富的数据)
class UserRegisteredEvent {
    String userId;
    String phoneNumber;
    // ... 其他信息

    public UserRegisteredEvent(String userId, String phoneNumber) {
        this.userId = userId;
        this.phoneNumber = phoneNumber;
    }
    // Getters...
}

// 观察者接口
interface UserRegistrationObserver {
    void onUserRegistered(UserRegisteredEvent event);
}

// 主题:用户服务
class UserService {
    private List<UserRegistrationObserver> observers = new ArrayList<>();
    // 搞个线程池玩玩异步,更真实一点
    private ExecutorService asyncExecutor = Executors.newFixedThreadPool(2);

    public void addObserver(UserRegistrationObserver observer) {
        observers.add(observer);
    }

    public void removeObserver(UserRegistrationObserver observer) {
        observers.remove(observer);
    }

    public void registerUser(String username, String password, String phoneNumber) {
        System.out.println("核心逻辑:正在注册用户 " + username + " ...");
        // 假设这里是数据库操作等...
        String userId = "user_" + System.currentTimeMillis(); // 模拟生成用户ID
        System.out.println("用户 " + username + " 注册成功!ID: " + userId);

        // 注册成功,发布事件
        UserRegisteredEvent event = new UserRegisteredEvent(userId, phoneNumber);
        notifyObservers(event);

        System.out.println("注册流程主体完成。");
    }

    private void notifyObservers(UserRegisteredEvent event) {
        System.out.println("开始通知注册成功的观察者...");
        for (UserRegistrationObserver observer : observers) {
            // 这里可以根据需要决定是同步调用还是异步调用
            if (observer instanceof SmsNotifierObserver) {
                 // 短信可能比较重要,同步调用 (或者用更可靠的方式)
                 observer.onUserRegistered(event);
            } else if (observer instanceof CouponIssuerObserver) {
                 // 发券可以容忍延迟,异步执行,不阻塞主注册流程
                 asyncExecutor.submit(() -> observer.onUserRegistered(event));
                 System.out.println("已将发券任务提交到异步线程池。");
            } else {
                 // 其他观察者默认同步
                 observer.onUserRegistered(event);
            }
        }
        // 注意:实际项目中,异步任务的异常处理、线程池管理等需要更完善
    }

    // 在应用关闭时记得关闭线程池
    public void shutdownAsyncExecutor() {
        asyncExecutor.shutdown();
    }
}

// 观察者1:短信通知
class SmsNotifierObserver implements UserRegistrationObserver {
    @Override
    public void onUserRegistered(UserRegisteredEvent event) {
        System.out.println("[短信服务] 收到通知 (线程: " + Thread.currentThread().getName() + ")");
        System.out.println("[短信服务] 准备向手机 " + event.phoneNumber + " 发送注册成功短信...");
        // 模拟调用短信API
        try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        System.out.println("[短信服务] 短信发送成功!");
    }
}

// 观察者2:优惠券发放
class CouponIssuerObserver implements UserRegistrationObserver {
    @Override
    public void onUserRegistered(UserRegisteredEvent event) {
        System.out.println("[优惠券服务] 收到通知 (线程: " + Thread.currentThread().getName() + ")");
        System.out.println("[优惠券服务] 准备为用户 " + event.userId + " 发放新人优惠券...");
        // 模拟调用优惠券服务
        try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } // 模拟耗时操作
        System.out.println("[优惠券服务] 新人优惠券发放成功!");
    }
}

// 测试
public class UserRegistrationDemo {
    public static void main(String[] args) {
        UserService userService = new UserService();

        // 添加观察者
        userService.addObserver(new SmsNotifierObserver());
        userService.addObserver(new CouponIssuerObserver());

        // 执行注册
        userService.registerUser("Alice", "password123", "13800138000");

        // 等待异步任务执行一会 (简单演示)
        try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }

        // 关闭线程池
        userService.shutdownAsyncExecutor();
    }
}

好处:

  • UserService 只负责核心注册逻辑和发布事件,非常干净。
  • 添加新功能(如发送邮件)只需新增 EmailNotifierObserver 并注册,无需修改 UserService
  • 异步处理:对于非核心、可延迟的操作(如发券),观察者的 update 方法可以将其提交到线程池异步执行,不阻塞主流程,提升用户体验。注意:观察者模式本身不提供异步机制,但它为实现异步处理提供了完美的切入点。 这比直接引入 MQ 更轻量级,适用于单体应用或服务内部的解耦。
3.2.2 模拟北京小客车指标摇号

场景: 摇号系统(主题)公布结果后,需要通知所有申请人(观察者)。

  • 主题 (Subject): LotterySystem (摇号系统)。核心状态是本期摇号结果。
  • 观察者 (Observer): Applicant (申请人)。每个人都关心自己是否中签。

代码示意:

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

// 摇号结果
class LotteryResult {
    int issueNumber; // 期号
    List<String> winners; // 中签者名单

    public LotteryResult(int issueNumber, List<String> winners) { this.issueNumber = issueNumber; this.winners = winners; }
    // Getters...
}

// 观察者接口
interface LotteryObserver {
    void onResultPublished(LotteryResult result);
}

// 主题:摇号系统
class LotterySystem {
    private List<LotteryObserver> observers = new ArrayList<>();
    private LotteryResult currentResult;

    public void addObserver(LotteryObserver observer) { observers.add(observer); }
    public void removeObserver(LotteryObserver observer) { observers.remove(observer); }

    // 模拟进行摇号并公布结果
    public void conductLottery(int issueNumber, List<String> allApplicants) {
        System.out.println("\n--- 第 " + issueNumber + " 期摇号开始 ---");
        // 模拟摇号逻辑...
        Random random = new Random();
        List<String> winners = new ArrayList<>();
        int winnerCount = Math.min(3, allApplicants.size()); // 假设每期最多摇出3个
        List<String> pool = new ArrayList<>(allApplicants);
        for (int i = 0; i < winnerCount && !pool.isEmpty(); i++) {
            int index = random.nextInt(pool.size());
            winners.add(pool.remove(index));
        }
        System.out.println("摇号结束,中签名单:" + winners);

        // 创建结果并通知
        this.currentResult = new LotteryResult(issueNumber, winners);
        notifyObservers();
        System.out.println("--- 第 " + issueNumber + " 期摇号结果公布完毕 ---");
    }

    private void notifyObservers() {
        System.out.println("开始通知所有申请人摇号结果...");
        for (LotteryObserver observer : observers) {
            observer.onResultPublished(currentResult);
        }
    }
}

// 观察者:申请人
class Applicant implements LotteryObserver {
    private String name;

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

    @Override
    public void onResultPublished(LotteryResult result) {
        System.out.print("[" + name + "] 收到第 " + result.issueNumber + " 期结果: ");
        if (result.winners.contains(this.name)) {
            System.out.println("恭喜!我中签了!激动的心,颤抖的手!");
        } else {
            System.out.println("唉,又没中,下期继续...");
        }
    }

    public String getName() { return name; }
}

// 测试
public class LotteryDemo {
    public static void main(String[] args) {
        LotterySystem lotterySystem = new LotterySystem();

        // 创建申请人 (观察者)
        Applicant applicant1 = new Applicant("张三");
        Applicant applicant2 = new Applicant("李四");
        Applicant applicant3 = new Applicant("王五");
        Applicant applicant4 = new Applicant("赵六");
        List<String> allApplicantNames = List.of(applicant1.getName(), applicant2.getName(), applicant3.getName(), applicant4.getName());

        // 申请人关注摇号结果
        lotterySystem.addObserver(applicant1);
        lotterySystem.addObserver(applicant2);
        lotterySystem.addObserver(applicant3);
        lotterySystem.addObserver(applicant4);

        // 进行第一期摇号
        lotterySystem.conductLottery(202401, allApplicantNames);

        // 赵六不想摇了,取消关注
        System.out.println("\n--- 赵六取消关注 ---");
        lotterySystem.removeObserver(applicant4);

         // 进行第二期摇号
        lotterySystem.conductLottery(202402, List.of(applicant1.getName(), applicant2.getName(), applicant3.getName())); // 假设只有这三人继续
    }
}

这个例子清晰地展示了“一对多”的通知机制。摇号系统不需要知道每个申请人的具体联系方式或通知偏好,只需要调用 observer.onResultPublished() 就行了。

3.2.3 java.util.Observablejava.util.Observer

Java 标准库里其实提供了观察者模式的实现:java.util.Observable 类和 java.util.Observer 接口。

但是,请注意:在现代 Java 开发中,通常不推荐使用它们!

为什么不推荐?

  1. Observable 是一个类 (Class),而不是接口 (Interface): 这意味着你的主题类必须继承 Observable。Java 是单继承的,如果你的类已经继承了其他父类,那就没法用了,限制了灵活性。设计模式通常推荐“面向接口编程”,而不是“面向实现编程”。
  2. API 设计问题:
    • setChanged() 方法:你需要先调用 setChanged() 方法,然后调用 notifyObservers() 才能真正发出通知。这个 protectedsetChanged() 很容易被忘记调用,导致通知发不出去,排查问题时会很懵逼。
    • notifyObservers(Object arg): 传递数据是通过一个 Object 参数,不够类型安全,接收方需要进行类型转换。
  3. 线程安全问题: Observable 中的 Vector (用于存储 observers) 是线程安全的,但其 notifyObservers 方法的迭代过程并非完全安全,如果在迭代时有其他线程修改观察者列表(虽然 Vector 本身同步,但迭代逻辑不是原子性的),可能出现问题。更重要的是,你的 update 方法和主题状态的改变需要你自己保证线程安全。

简单示例 (了解即可,不推荐使用):

import java.util.Observable; // 注意是类
import java.util.Observer;   // 注意是接口

// 主题类,必须继承 Observable
class WeatherStation extends Observable {
    private String weather;

    public void setWeather(String weather) {
        this.weather = weather;
        System.out.println("气象站发布新天气: " + weather);
        // 必须先调用 setChanged()
        setChanged();
        // 然后才能通知,可以传递数据
        notifyObservers(weather);
    }
}

// 观察者类,实现 Observer 接口
class PhoneDisplay implements Observer {
    private String name;
    public PhoneDisplay(String name) { this.name = name; }

    @Override
    public void update(Observable o, Object arg) { // 注意参数是 Observable 和 Object
        if (o instanceof WeatherStation) {
            String weather = (String) arg; // 需要类型转换
            System.out.println("[" + name + " 手机显示] 天气更新: " + weather);
        }
    }
}

public class OldJdkObserverDemo {
    public static void main(String[] args) {
        WeatherStation station = new WeatherStation();

        PhoneDisplay display1 = new PhoneDisplay("我的手机");
        PhoneDisplay display2 = new PhoneDisplay("老婆的手机");

        // 添加观察者
        station.addObserver(display1);
        station.addObserver(display2);

        // 发布天气
        station.setWeather("晴天,25度");
        System.out.println("---");
        station.setWeather("多云转阴,20度");

        // 如果忘记 setChanged()
        System.out.println("\n--- 忘记调用 setChanged() ---");
        station.weather = "下雨了,15度"; // 直接修改状态(不推荐)
        System.out.println("气象站状态改为: " + station.weather);
        station.notifyObservers(station.weather); // 通知发不出去!
        System.out.println("(可以看到,手机没有收到更新)");

        // 正确做法还是通过 set 方法
        System.out.println("\n--- 再次正确发布 ---");
        station.setWeather("雨停了,彩虹!");
    }
}

替代方案:

  • 自己动手实现: 就像我们前面的模板代码一样,使用接口,更灵活。
  • java.beans.PropertyChangeSupportPropertyChangeListener: 这是 JavaBeans 规范的一部分,更健壮,支持属性级别的监听。
  • 第三方库:
    • Guava EventBus: Google Guava 库提供的强大的事件总线,解耦更彻底,使用注解,更方便。
    • RxJava / Project Reactor: 面向响应式编程的库,观察者模式是其核心思想之一,功能更强大,适用于复杂的异步事件流处理。
    • Spring Framework: Spring 的事件机制 (ApplicationEvent, ApplicationListener) 也是观察者模式的一种应用。
更多例子
  • GUI 事件监听: Swing/AWT/JavaFX 中的按钮点击、鼠标移动等事件处理,就是典型的观察者模式应用。JButton 是主题,ActionListener 是观察者。
  • MVC/MVP/MVVM 架构: Model(模型)是主题,View(视图)是观察者。当 Model 数据变化时,通知 View 更新显示。

4. 灵魂拷问(面试题)

面试官微微一笑,掏出了他的祖传问题…

问题 1:请解释一下什么是观察者模式,它的主要优点是什么?

解答:
观察者模式是一种 行为设计模式,它定义了对象之间的一种 一对多 的依赖关系。当一个对象(称为 主题 Subject被观察者 Observable)的状态发生改变时,所有依赖于它的对象(称为 观察者 Observer)都会得到 自动通知 并被更新。

主要优点:

  1. 解耦 (Decoupling): 这是最核心的优点。主题和观察者之间是松耦合的。主题只知道观察者实现了某个接口,不关心其具体类型或内部逻辑。观察者也不知道主题的内部细节,只关心自己感兴趣的事件通知。这使得双方可以独立地变化和扩展。
  2. 易于扩展 (Extensibility): 可以很容易地增加新的观察者,而无需修改主题的代码,符合 开闭原则
  3. 支持广播通信 (Broadcast Communication): 主题状态的改变可以方便地通知到所有相关的观察者。
  4. 符合依赖倒置原则 (Dependency Inversion Principle): 主题依赖于抽象的 Observer 接口,而不是具体的观察者类。

问题 2:JDK 自带的 java.util.ObservableObserver 有什么缺点?现在推荐使用什么替代方案?

解答:
JDK 自带的 java.util.ObservableObserver 虽然实现了观察者模式,但在现代 Java 开发中 不推荐使用,主要缺点有:

  1. Observable 是类而非接口: 这强制要求主题类必须继承 Observable,限制了类的继承体系(Java 单继承),不够灵活。违反了“面向接口编程”的原则。
  2. API 设计缺陷:
    • 需要先调用 setChanged() 方法,然后 notifyObservers() 才能生效,容易忘记调用 setChanged() 导致通知失败。
    • notifyObservers(Object arg) 使用 Object 传递数据,缺乏类型安全,需要强制类型转换。
  3. 潜在的线程安全问题: 虽然内部使用了 Vector,但在某些并发场景下(如迭代时修改列表)仍可能存在问题,且需要开发者自行保证状态变更和 update 方法的线程安全。

推荐的替代方案:

  1. 手动实现: 根据项目需求,自己编写基于接口的观察者模式实现(如本文开头的模板)。
  2. java.beans.PropertyChangeSupport / PropertyChangeListener: JDK 内置的 JavaBeans 事件机制,更健壮,适用于属性监听。
  3. 使用成熟的第三方库:
    • Guava EventBus: Google Guava 库提供,基于注解,使用方便,解耦更彻底。
    • RxJava / Project Reactor: 强大的响应式编程库,观察者模式是其基础,适合处理复杂的异步事件流。
    • Spring Framework Event: 如果使用 Spring,其内置的事件机制 (ApplicationEvent, ApplicationListener) 是很好的选择。

5. 扩展阅读:发布订阅模式

5.1 发布订阅 vs 观察者模式

扩展阅读,我们来详细聊聊观察者模式 (Observer Pattern) 和发布-订阅模式 (Publish-Subscribe Pattern, Pub/Sub) 之间的区别与联系。

它们俩确实非常相似,核心思想都是 解耦 消息的发送者(事件源)和接收者(处理者),实现一种 事件驱动 的机制。因为太像了,所以在很多场景下,大家会把它们混用,或者认为观察者模式就是一种简单的发布-订阅实现。

但严格来说,它们在 实现架构耦合程度 上存在关键区别,尤其是在引入 消息中间件/事件总线 的场景下,发布-订阅模式的特征会更加明显。

主要的区别点在于 是否存在一个中心化的代理/中介(Broker/Event Bus)

  1. 观察者模式 (Observer Pattern):

    • 直接通信: 主题 (Subject/Observable) 直接持有 观察者 (Observer) 的引用列表。当主题状态改变时,它会 直接遍历 这个列表,并调用每个观察者的 update 方法。
    • 耦合关系: 主题和观察者之间虽然通过接口解耦了具体实现,但它们在运行时 直接知道对方的存在(至少主题知道观察者的接口,观察者通常也持有主题的引用或知道如何获取相关状态)。耦合度相对较高。
    • 典型场景: 通常在 同一个应用程序或进程内部 使用。例如:
      • 图形用户界面 (GUI) 事件监听(按钮点击 -> 监听器响应)。
      • 模型-视图-控制器 (MVC) 架构中,模型 (Subject) 状态改变通知视图 (Observer) 更新。
      • 我们之前例子中的用户注册后发送短信/优惠券(在同一个服务内部解耦)。
    • 关注点: 主题关心的是“有哪些观察者需要被通知”,观察者关心的是“我关注的那个主题发生了变化”。
    • 类比: 你直接订阅了某个杂志社的杂志(Subject),杂志社有你的地址(Observer 引用),每次出新刊就直接寄给你。
  2. 发布-订阅模式 (Publish-Subscribe Pattern):

    • 通过中介通信: 发布者 (Publisher) 和订阅者 (Subscriber) 之间 不直接 通信。它们都只与一个 中心化的消息代理 (Message Broker) 或事件总线 (Event Bus) 进行交互。
    • 发布者: 将消息/事件发送到 Broker 的特定 主题 (Topic)频道 (Channel),它不关心谁会接收这个消息。
    • 订阅者: 向 Broker 订阅 自己感兴趣的 Topic/Channel,它不关心消息是谁发布的。
    • Broker/Event Bus: 负责接收来自发布者的消息,并将这些消息 路由 给所有订阅了对应 Topic/Channel 的订阅者。
    • 耦合关系: 发布者和订阅者之间是 完全解耦 的,它们互相不知道对方的存在,只依赖于中间的 Broker。耦合度非常低。
    • 典型场景:
      • 分布式系统、微服务架构: 服务间的异步通信,一个服务发布事件,多个其他服务可以订阅并处理。
      • 消息队列 (MQ): 如 Kafka, RabbitMQ, RocketMQ 等都是典型的 Pub/Sub 实现。
      • 应用程序内部的事件总线: 如 Guava EventBus, Spring ApplicationEvent 等,在单个应用内提供更彻底的解耦。
    • 关注点: 发布者关心的是“把这个消息发到某个主题”,订阅者关心的是“订阅某个主题的消息”,Broker 关心的是“正确地路由消息”。
    • 类比: 你去报刊亭(Broker)买报纸。报社(Publisher)把报纸送到报刊亭,你(Subscriber)去报刊亭买你感兴趣的报纸。报社和你不需要认识对方。

总结对比:

特性观察者模式 (Observer)发布-订阅模式 (Publish-Subscribe)
核心区别 中心化 Broker/Event Bus 中心化 Broker/Event Bus
通信方式主题 直接调用 观察者的 update 方法发布者/订阅者 通过 Broker 间接通信
耦合关系主题与观察者 直接 (基于接口) 耦合发布者与订阅者 完全解耦 (只依赖 Broker)
感知对方主题知道观察者,观察者常知道主题发布者与订阅者 互相不知道 对方存在
通信模型通常是 同步 调用 (但可改为异步)通常是 异步 通信 (由 Broker 支持)
典型范围单个应用/进程内部跨应用、跨进程、分布式系统,也可用于单应用内部解耦

联系:

  • 观察者模式可以看作是发布-订阅模式的一种 简化实现特定形式,尤其是在没有引入独立 Broker 的情况下。
  • 核心思想都是 事件驱动解耦
  • 在某些简单的、进程内的场景下,使用观察者模式(自己实现或用类似 Spring Event 的简单事件机制)可能比引入重量级的消息队列更合适。而对于需要跨服务通信、高可靠性、消息持久化、复杂路由的场景,则必须采用基于 Broker 的发布-订阅模式。

简单来说:观察者模式是“你若安好,便是晴天,我会直接告诉你”,发布-订阅模式是“我在世界中心呼唤爱(发给中介),谁爱听谁听(通过中介订阅)”。

5.2 发布订阅代码模版

我们来看一个经典的发布-订阅模式 (Publish-Subscribe Pattern) 的代码结构示例。

这个例子将实现一个简单的 事件总线 (Event Bus) 作为消息代理 (Broker)。发布者将事件发布到总线,订阅者向总线订阅特定类型的事件,总线负责将事件分发给对应的订阅者。

核心组件:

  1. Event (事件): 表示发生的某件事情,通常包含相关数据。可以是一个基类或接口。
  2. Publisher (发布者): 创建事件并将其发布到 Event Bus。它不直接与订阅者交互。
  3. Subscriber (订阅者): 对特定类型的事件感兴趣,并向 Event Bus 注册自己。它定义了处理事件的逻辑。它不直接与发布者交互。
  4. EventBus (事件总线 / 消息代理): 核心的中介。维护订阅关系(哪个订阅者对哪种事件感兴趣),接收发布者发布的事件,并将事件路由给所有相关的订阅者。

代码示例:

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

// --- 1. 定义事件基类/接口 ---
// 可以是空接口作为标记,也可以包含通用字段(如时间戳)
interface Event {
    long timestamp = System.currentTimeMillis();
    default long getTimestamp() {
        return timestamp;
    }
}

// --- 具体事件类型 ---
class OrderPlacedEvent implements Event {
    final String orderId;
    final double amount;

    public OrderPlacedEvent(String orderId, double amount) {
        this.orderId = orderId;
        this.amount = amount;
    }

    @Override
    public String toString() {
        return "OrderPlacedEvent [orderId=" + orderId + ", amount=" + amount + ", ts=" + getTimestamp() + "]";
    }
}

class UserLoggedInEvent implements Event {
    final String userId;

    public UserLoggedInEvent(String userId) {
        this.userId = userId;
    }

     @Override
    public String toString() {
        return "UserLoggedInEvent [userId=" + userId + ", ts=" + getTimestamp() + "]";
    }
}


// --- 2. 定义订阅者接口 ---
// 使用泛型来指定感兴趣的事件类型,增强类型安全
interface Subscriber<T extends Event> {
    void handleEvent(T event);
    // 可以添加一个方法获取感兴趣的事件类型,或者在注册时指定
    // Class<T> getEventType(); // 一种方式
}

// --- 3. 实现事件总线 (核心中介) ---
class SimpleEventBus {
    // 维护订阅关系: 事件类型 -> 订阅者列表
    // 使用 ConcurrentHashMap 和 CopyOnWriteArrayList 保证基本的线程安全
    private final Map<Class<? extends Event>, List<Subscriber<?>>> subscribers = new ConcurrentHashMap<>();

    /**
     * 订阅事件
     * @param eventType 感兴趣的事件类型 Class 对象
     * @param subscriber 订阅者实例
     * @param <T> 事件类型
     */
    public <T extends Event> void subscribe(Class<T> eventType, Subscriber<T> subscriber) {
        // 获取或创建该事件类型的订阅者列表
        // computeIfAbsent 保证原子性操作
        List<Subscriber<?>> subscriberList = subscribers.computeIfAbsent(eventType, k -> new CopyOnWriteArrayList<>());
        subscriberList.add(subscriber);
        System.out.println(subscriber.getClass().getSimpleName() + " subscribed to " + eventType.getSimpleName());
    }

    /**
     * 取消订阅事件
     * @param eventType 事件类型 Class 对象
     * @param subscriber 订阅者实例
     * @param <T> 事件类型
     */
    public <T extends Event> void unsubscribe(Class<T> eventType, Subscriber<T> subscriber) {
        List<Subscriber<?>> subscriberList = subscribers.get(eventType);
        if (subscriberList != null) {
            subscriberList.remove(subscriber);
            System.out.println(subscriber.getClass().getSimpleName() + " unsubscribed from " + eventType.getSimpleName());
        }
    }

    /**
     * 发布事件
     * @param event 要发布的事件实例
     */
    public void publish(Event event) {
        Class<?> eventType = event.getClass();
        System.out.println("\nPublishing event: " + event.toString());

        // 获取对该事件类型感兴趣的订阅者列表
        List<Subscriber<?>> specificSubscribers = subscribers.get(eventType);

        if (specificSubscribers != null && !specificSubscribers.isEmpty()) {
            System.out.println("Notifying " + specificSubscribers.size() + " subscribers for " + eventType.getSimpleName());
            for (Subscriber<?> subscriber : specificSubscribers) {
                try {
                    // 这里需要进行类型转换,因为 Map 中存储的是 List<Subscriber<?>>
                    // 在 subscribe 方法的类型约束下,这里的转换理论上是安全的
                    @SuppressWarnings("unchecked") // 抑制未检查转换警告
                    Subscriber<Event> specificSubscriber = (Subscriber<Event>) subscriber;
                    specificSubscriber.handleEvent(event);
                } catch (Exception e) {
                    // 实际应用中应添加异常处理逻辑,避免一个订阅者的失败影响其他订阅者
                    System.err.println("Error handling event in " + subscriber.getClass().getSimpleName() + ": " + e.getMessage());
                }
            }
        } else {
             System.out.println("No subscribers for " + eventType.getSimpleName());
        }

        // 可以选择性地通知订阅了所有事件(Event.class)的订阅者
        // List<Subscriber<?>> allEventSubscribers = subscribers.get(Event.class);
        // if (allEventSubscribers != null) { ... }
    }
}


// --- 4. 实现具体订阅者 ---
class EmailService implements Subscriber<OrderPlacedEvent> {
    @Override
    public void handleEvent(OrderPlacedEvent event) {
        System.out.println("[EmailService] Received OrderPlacedEvent: Sending confirmation email for order " + event.orderId + ".");
        // 模拟发送邮件逻辑...
    }
}

class InventoryService implements Subscriber<OrderPlacedEvent> {
     @Override
    public void handleEvent(OrderPlacedEvent event) {
        System.out.println("[InventoryService] Received OrderPlacedEvent: Updating inventory for order " + event.orderId + ".");
        // 模拟库存更新逻辑...
    }
}

class AuditLogger implements Subscriber<UserLoggedInEvent> {
    @Override
    public void handleEvent(UserLoggedInEvent event) {
        System.out.println("[AuditLogger] Received UserLoggedInEvent: Logging user login for userId " + event.userId + " at " + event.getTimestamp());
        // 模拟记录日志...
    }
}

// --- 5. 实现发布者 (不需要是特定类,任何需要发事件的地方都可以) ---
class OrderService {
    private final SimpleEventBus eventBus;

    public OrderService(SimpleEventBus eventBus) {
        this.eventBus = eventBus;
    }

    public void placeOrder(String orderId, double amount) {
        System.out.println("\n--- Placing Order: " + orderId + " ---");
        // 核心业务逻辑...
        System.out.println("Order " + orderId + " processing complete.");

        // **发布事件到总线**
        OrderPlacedEvent event = new OrderPlacedEvent(orderId, amount);
        eventBus.publish(event); // 只与 EventBus 交互
    }
}

class AuthenticationService {
     private final SimpleEventBus eventBus;

    public AuthenticationService(SimpleEventBus eventBus) {
        this.eventBus = eventBus;
    }

     public void loginUser(String userId) {
         System.out.println("\n--- User Logging In: " + userId + " ---");
         // 核心认证逻辑...
         System.out.println("User " + userId + " authenticated successfully.");

         // **发布事件到总线**
         UserLoggedInEvent event = new UserLoggedInEvent(userId);
         eventBus.publish(event); // 只与 EventBus 交互
     }
}

// --- 6. 演示如何使用 ---
public class PubSubDemo {
    public static void main(String[] args) {
        // 1. 创建事件总线实例 (全局唯一或按需注入)
        SimpleEventBus eventBus = new SimpleEventBus();

        // 2. 创建订阅者实例
        EmailService emailService = new EmailService();
        InventoryService inventoryService = new InventoryService();
        AuditLogger auditLogger = new AuditLogger();

        // 3. 订阅者向总线注册自己感兴趣的事件
        eventBus.subscribe(OrderPlacedEvent.class, emailService);
        eventBus.subscribe(OrderPlacedEvent.class, inventoryService);
        eventBus.subscribe(UserLoggedInEvent.class, auditLogger);

        System.out.println("\n--- Initial Subscriptions Set Up ---");

        // 4. 创建发布者实例 (它们依赖 EventBus)
        OrderService orderService = new OrderService(eventBus);
        AuthenticationService authService = new AuthenticationService(eventBus);

        // 5. 发布者执行业务操作,并发布事件 (发布者不知道谁在监听)
        orderService.placeOrder("ORD123", 99.99);
        authService.loginUser("user-alice");

        // 6. 演示取消订阅
        System.out.println("\n--- InventoryService Unsubscribing ---");
        eventBus.unsubscribe(OrderPlacedEvent.class, inventoryService);

        // 7. 再次发布同类型事件,看效果
        orderService.placeOrder("ORD456", 150.50); // InventoryService 不会收到通知了

    }
}

代码关键点说明:

  1. 解耦: OrderServiceAuthenticationService (发布者) 只依赖 SimpleEventBus,完全不知道 EmailService, InventoryService, AuditLogger (订阅者) 的存在。同样,订阅者也不知道是哪个服务发布的事件,它们只关心自己订阅的事件类型。
  2. 中心化的 EventBus: SimpleEventBus 是核心,负责维护订阅关系和事件分发。这是与观察者模式最主要的区别(观察者模式中主题直接持有观察者引用)。
  3. 类型安全: 使用 Class<T> 和泛型 Subscriber<T> 提高了类型安全,订阅者可以明确指定处理的事件类型,并在 handleEvent 方法中直接使用该类型,减少了强制类型转换的需要(虽然在 EventBus 内部为了通用性进行了一次转换)。
  4. 线程安全: 示例中使用了 ConcurrentHashMapCopyOnWriteArrayList 来提供基本的线程安全,适用于多线程环境下的订阅/取消订阅和发布操作。CopyOnWriteArrayList 特别适合读多写少的场景(发布事件是读,订阅/取消是写)。
  5. 关注点分离: 每个组件职责清晰:发布者负责产生事件,订阅者负责处理事件,事件总线负责连接两者。

这个例子展示了发布-订阅模式的基本结构和优点,是构建更复杂事件驱动系统的基础。实际应用中,可能会使用更成熟的库(如 Guava EventBus、Spring ApplicationEvent、Akka Event Bus)或消息队列(Kafka, RabbitMQ 等)来实现,但核心的模式思想是一致的。


好了,今天关于观察者模式的“祛魅”就到这里。希望通过这次“庖丁解牛”,大家对它有了更清晰、更深入的认识。记住,设计模式不是银弹,也不是用来炫技的,而是前辈们总结出来的解决特定问题的“套路”。理解其原理,掌握其精髓,在合适的场景下灵活运用,才能真正提升我们的代码质量和开发效率。

下次遇到需要“一对多”通知、希望解耦模块的场景,别忘了咱们的观察者模式这位老朋友!

觉得有收获?点赞、收藏、转发走起!我们下期再见! Peace out! ✌️

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值