设计模式-观察者模式

写软件的时候,你一定遇到过这样的场景:某个地方的数据一变,更上层一大堆界面、缓存、日志都要跟着变。比如"消息未读数"从 9 变成 10,要更新标题栏的小红点、底部 Tab 的角标、通知栏里的提醒,甚至还要把这个变化写一条日志。如果每个地方都手动去改,很快你就会被各种"联动更新"折磨到怀疑人生。

于是你最开始可能会这么写:在数据变化的地方,直接把所有相关逻辑都写进去。

复制代码
public class MessageService {

    private int unreadCount;

    public void receiveNewMessage() {
        unreadCount++;

        // 1. 更新标题栏
        TitleBar.updateUnread(unreadCount);

        // 2. 更新底部 Tab 角标
        BottomTab.updateUnread(unreadCount);

        // 3. 更新通知栏
        NotificationHelper.showUnread(unreadCount);

        // 4. 记录日志
        LogUtil.getInstance().debug("未读消息数变更为:" + unreadCount);
    }
}

刚开始你觉得挺爽:一个方法里全搞定,逻辑清清楚楚,哪儿需要更新就往里面加代码。结果没多久,leader 找你谈话了:"这段代码,问题有点大啊。"

你一脸无辜:"不就是收消息的时候,把需要更新的地方都更新一下吗?有什么问题?"

leader 一笑:"问题可多了。你现在能想到的更新点也就这几个,将来如果还有别的地方要跟着'未读数'变化怎么办?继续在这个方法里加?那这个类迟早变成'上帝类'。而且这些 UI 组件、日志模块本来和消息服务是解耦的,你现在硬生生把它们全绑死在一起了。"

你仔细一看,确实如此:

  1. 只要有一个观察方变动(比如新增一个"桌面小组件角标"),就得改 MessageService。

  2. 如果某天通知栏的展示逻辑挪到别的模块,还得再来改这里。

  3. 单元测试也很痛苦,想单测"未读数变更逻辑",却总是被一堆 UI 更新、通知栏展示拖着走。

leader 叹了口气,说:"你现在写的是'主动通知 + 硬编码依赖'。这种场景特别适合用观察者模式,专门解决'一个对象状态变化,通知一群依赖它的对象'的问题。"

你第一次正式听到"观察者模式"这个名字,于是决定趁机好好梳理一下。

一、先想清楚:谁在"被观察",谁是"观察者"

leader 提醒你:"观察者模式里有两个关键角色:被观察者(Subject)和观察者(Observer)。"

在"未读消息数"的例子中:

被观察者(Subject):MessageSubject(内部持有未读数,当未读数变化时发出通知)

观察者(Observer):标题栏、底部 Tab、通知栏、日志模块等等,这些都对"未读数变化"感兴趣。

换句话说:

MessageSubject 不应该关心"有哪些人要更新、怎么更新",它只负责维护状态和发出"变了"的通知;

每个具体的观察者关心的是"当未读数变化时,我自己要做什么"。

你点点头,决定先抽象出观察者接口。

二、抽象出观察者和被观察者接口

你先定义一个通用的观察者接口,表示"对未读数变化感兴趣的人":

复制代码
public interface MessageObserver {

    void onUnreadCountChanged(int newCount);
}

然后定义一个被观察者接口,负责管理观察者的注册、移除和通知:

复制代码
public interface MessageSubject {

    void registerObserver(MessageObserver observer);

    void removeObserver(MessageObserver observer);

    void notifyObservers();
}

接下来就是核心的被观察者实现类,它内部维护未读数和观察者列表:

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

public class MessageData implements MessageSubject {

    private int unreadCount;
    private List<MessageObserver> observers = new ArrayList<>();

    @Override
    public void registerObserver(MessageObserver observer) {
        if (observer == null) return;
        if (!observers.contains(observer)) {
            observers.add(observer);
        }
    }

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

    @Override
    public void notifyObservers() {
        for (MessageObserver observer : observers) {
            observer.onUnreadCountChanged(unreadCount);
        }
    }

    public void receiveNewMessage() {
        unreadCount++;
        // 只负责更新自己的状态和发通知
        notifyObservers();
    }

    public int getUnreadCount() {
        return unreadCount;
    }
}

现在你会发现一个很关键的变化:MessageData 里已经不再出现标题栏、底部 Tab、通知栏、日志类的任何字眼,只保留了对抽象接口 MessageObserver 的依赖。

三、实现各种具体观察者

接下来,你给每个需要"跟着未读数变化而变化的东西"实现一个观察者类。

比如标题栏小红点:

复制代码
public class TitleBar implements MessageObserver {

    @Override
    public void onUnreadCountChanged(int newCount) {
        System.out.println("标题栏更新未读数:" + newCount);
        // 这里更新 UI 逻辑
    }
}

底部 Tab 角标:

复制代码
public class BottomTab implements MessageObserver {

    @Override
    public void onUnreadCountChanged(int newCount) {
        System.out.println("底部 Tab 角标更新未读数:" + newCount);
    }
}

通知栏:

复制代码
public class NotificationObserver implements MessageObserver {

    @Override
    public void onUnreadCountChanged(int newCount) {
        System.out.println("通知栏展示新的未读数:" + newCount);
    }
}

日志模块也可以作为一个观察者:

复制代码
public class LogObserver implements MessageObserver {

    @Override
    public void onUnreadCountChanged(int newCount) {
        LogUtil.getInstance().debug("未读消息数变更为:" + newCount);
    }
}

每个观察者只关心"当未读数变化到 X 时,我要干什么",完全不知道还有哪些兄弟也在一起订阅它。

四、客户端如何组装和使用?

万事俱备,只差最后一步:把被观察者和一群观察者"连起来"。

复制代码
public class Client {

    public static void main(String[] args) {
        // 1. 创建被观察者
        MessageData messageData = new MessageData();

        // 2. 创建观察者
        TitleBar titleBar = new TitleBar();
        BottomTab bottomTab = new BottomTab();
        NotificationObserver notificationObserver = new NotificationObserver();
        LogObserver logObserver = new LogObserver();

        // 3. 注册观察者
        messageData.registerObserver(titleBar);
        messageData.registerObserver(bottomTab);
        messageData.registerObserver(notificationObserver);
        messageData.registerObserver(logObserver);

        // 4. 收到新消息
        messageData.receiveNewMessage();
        messageData.receiveNewMessage();

        // 5. 某些观察者不想再监听了,也可以随时移除
        messageData.removeObserver(notificationObserver);
        messageData.receiveNewMessage();
    }
}

在这个过程中,你会注意到几个重要点:

  1. MessageData 只负责维护未读数和通知观察者,根本不关心有多少个观察者,也不关心它们具体在干什么;

  2. 任何时候要增加一个新的"联动更新项"(比如桌面小组件角标),只需要新增一个实现 MessageObserver 的类,并在初始化阶段注册一下即可;

  3. 要"取消某个更新项"(比如产品决定不再在通知栏展示未读数了)也很简单:不再注册或从列表移除就行,无需修改 MessageData 本身。

leader 看完你的实现,点点头:"这才像样。现在'变化的地方'是观察者集合,新增、删除观察者都不需要动被观察者逻辑,真正做到了解耦。"

五、观察者模式的正式定义

实战一圈下来,你也可以给观察者模式一个比较正规的定义了:

观察者模式:定义对象间一种一对多的依赖关系,当一个对象状态发生改变时,所有依赖它的对象都会自动得到通知并被更新。

这里的几个关键词非常重要:

一对多依赖:一个被观察者,多个观察者;

状态变化自动通知:被观察者不需要知道观察者是谁,只是"发通知",具体怎么更新由观察者自己决定;

解耦:被观察者只依赖于抽象观察者接口,不依赖于任何具体实现。

六、观察者模式的优点与缺点

leader 照例又让你总结了一遍优缺点。

优点:

  1. 解耦"状态主体"和"响应逻辑":被观察者只关心自己的状态变化,观察者决定如何响应,双方通过接口解耦。

  2. 符合开闭原则:增加新的观察者,不需要修改被观察者代码,只需新增类 + 注册即可。

  3. 支持广播通信:一次状态变化,自动通知所有观察者,避免"一个个手动调用"的代码堆积。

  4. 更容易复用:同一个被观察者可以被不同场景下的观察者复用,同一个观察者也可以监听不同的被观察者(通过多处注册)。

缺点:

  1. 可能造成通知链复杂难以追踪:观察者多了以后,很难一眼看清某个状态变化到底触发了什么后果,调试不当容易迷路。

  2. 存在性能风险:如果观察者数量很多,或者观察者做的事情很重,一次通知会触发大量操作,可能带来性能问题。

  3. 容易出现循环依赖:如果设计不当,观察者更新过程中又去修改被观察者状态,可能导致"通知 → 更新 → 再通知"的循环。

leader 总结说:"用观察者模式的时候,脑子里要有张图:'一改,多响'。一旦'多响'逻辑大到你难以掌控,就要重新梳理结构了。"

七、观察者模式在实际项目中的常见应用

你查了一些资料,再结合自己做项目的经历,发现观察者模式几乎无处不在:

  1. 事件监听机制

GUI 编程里的按钮点击监听、输入框文本改变监听,本质上都是观察者模式:按钮是被观察者,监听器是观察者。

  1. 订阅 / 发布(Publish-Subscribe)模型

订阅者订阅某个主题,当主题有新消息时推送给所有订阅者。各种消息中间件(MQ)、事件中枢(EventBus)都是这个思想的扩展。

  1. 数据绑定 / 响应式编程

一些前端框架和数据绑定库,当数据变化时自动更新视图,也是使用了观察者模式。

  1. 缓存同步

某些系统里,数据库数据变化之后,需要通知本地缓存、分布式缓存、多级缓存去更新,这也是典型的"一个变,多处跟着变"。

  1. 业务状态广播

比如登录状态变化时,多个模块(用户中心、订单中心、消息中心)都要做各自的更新和刷新逻辑,就可以通过观察者统一通知。

你这时才发现,其实自己早就"半只脚"踩在观察者模式上了,只不过之前是随手写监听代码,并没有系统地抽象为一个模式。

八、观察者模式小结

最后,leader 用一句非常直白的话帮你加深记忆:

当你在系统里发现:"一个对象状态一改变,就要手动去改很多地方"的时候,就该考虑用观察者模式,让这些地方自己来"订阅"变化。

你点点头,在笔记本上写下这句话,并在旁边补充了一句自己的理解:

观察者模式:让该知道的人自己来订阅消息,状态变化时只需"广播",不用一个个点名通知,也不用记住每个人在干什么。

备注:以上内容基于AI生成

相关推荐
砍光二叉树3 小时前
【设计模式】结构型-组合模式
设计模式·组合模式
砍光二叉树4 小时前
【设计模式】结构型-享元模式
设计模式·享元模式
电子科技圈4 小时前
SmartDV展示汽车IP解决方案以赋能智驾创芯并加速规模化普及
嵌入式硬件·设计模式·硬件架构·软件工程·软件构建·设计规范
砍光二叉树5 小时前
【设计模式】结构型-桥接模式
设计模式·桥接模式
姓蔡小朋友5 小时前
Agent Skill设计模式
开发语言·javascript·设计模式
砍光二叉树5 小时前
【设计模式】结构型-外观模式
设计模式·外观模式
zhaoshuzhaoshu6 小时前
设计模式6大原则详细对比(含场景举例)
设计模式·设计语言
砍光二叉树6 小时前
【设计模式】行为型-观察者模式
java·观察者模式·设计模式
泯仲19 小时前
Ragent项目7种设计模式深度解析:从源码看设计模式落地实践
java·算法·设计模式·agent