写软件的时候,你一定遇到过这样的场景:某个地方的数据一变,更上层一大堆界面、缓存、日志都要跟着变。比如"消息未读数"从 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 组件、日志模块本来和消息服务是解耦的,你现在硬生生把它们全绑死在一起了。"
你仔细一看,确实如此:
-
只要有一个观察方变动(比如新增一个"桌面小组件角标"),就得改 MessageService。
-
如果某天通知栏的展示逻辑挪到别的模块,还得再来改这里。
-
单元测试也很痛苦,想单测"未读数变更逻辑",却总是被一堆 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();
}
}
在这个过程中,你会注意到几个重要点:
-
MessageData 只负责维护未读数和通知观察者,根本不关心有多少个观察者,也不关心它们具体在干什么;
-
任何时候要增加一个新的"联动更新项"(比如桌面小组件角标),只需要新增一个实现 MessageObserver 的类,并在初始化阶段注册一下即可;
-
要"取消某个更新项"(比如产品决定不再在通知栏展示未读数了)也很简单:不再注册或从列表移除就行,无需修改 MessageData 本身。
leader 看完你的实现,点点头:"这才像样。现在'变化的地方'是观察者集合,新增、删除观察者都不需要动被观察者逻辑,真正做到了解耦。"
五、观察者模式的正式定义
实战一圈下来,你也可以给观察者模式一个比较正规的定义了:
观察者模式:定义对象间一种一对多的依赖关系,当一个对象状态发生改变时,所有依赖它的对象都会自动得到通知并被更新。
这里的几个关键词非常重要:
一对多依赖:一个被观察者,多个观察者;
状态变化自动通知:被观察者不需要知道观察者是谁,只是"发通知",具体怎么更新由观察者自己决定;
解耦:被观察者只依赖于抽象观察者接口,不依赖于任何具体实现。
六、观察者模式的优点与缺点
leader 照例又让你总结了一遍优缺点。
优点:
-
解耦"状态主体"和"响应逻辑":被观察者只关心自己的状态变化,观察者决定如何响应,双方通过接口解耦。
-
符合开闭原则:增加新的观察者,不需要修改被观察者代码,只需新增类 + 注册即可。
-
支持广播通信:一次状态变化,自动通知所有观察者,避免"一个个手动调用"的代码堆积。
-
更容易复用:同一个被观察者可以被不同场景下的观察者复用,同一个观察者也可以监听不同的被观察者(通过多处注册)。
缺点:
-
可能造成通知链复杂难以追踪:观察者多了以后,很难一眼看清某个状态变化到底触发了什么后果,调试不当容易迷路。
-
存在性能风险:如果观察者数量很多,或者观察者做的事情很重,一次通知会触发大量操作,可能带来性能问题。
-
容易出现循环依赖:如果设计不当,观察者更新过程中又去修改被观察者状态,可能导致"通知 → 更新 → 再通知"的循环。
leader 总结说:"用观察者模式的时候,脑子里要有张图:'一改,多响'。一旦'多响'逻辑大到你难以掌控,就要重新梳理结构了。"
七、观察者模式在实际项目中的常见应用
你查了一些资料,再结合自己做项目的经历,发现观察者模式几乎无处不在:
- 事件监听机制
GUI 编程里的按钮点击监听、输入框文本改变监听,本质上都是观察者模式:按钮是被观察者,监听器是观察者。
- 订阅 / 发布(Publish-Subscribe)模型
订阅者订阅某个主题,当主题有新消息时推送给所有订阅者。各种消息中间件(MQ)、事件中枢(EventBus)都是这个思想的扩展。
- 数据绑定 / 响应式编程
一些前端框架和数据绑定库,当数据变化时自动更新视图,也是使用了观察者模式。
- 缓存同步
某些系统里,数据库数据变化之后,需要通知本地缓存、分布式缓存、多级缓存去更新,这也是典型的"一个变,多处跟着变"。
- 业务状态广播
比如登录状态变化时,多个模块(用户中心、订单中心、消息中心)都要做各自的更新和刷新逻辑,就可以通过观察者统一通知。
你这时才发现,其实自己早就"半只脚"踩在观察者模式上了,只不过之前是随手写监听代码,并没有系统地抽象为一个模式。
八、观察者模式小结
最后,leader 用一句非常直白的话帮你加深记忆:
当你在系统里发现:"一个对象状态一改变,就要手动去改很多地方"的时候,就该考虑用观察者模式,让这些地方自己来"订阅"变化。
你点点头,在笔记本上写下这句话,并在旁边补充了一句自己的理解:
观察者模式:让该知道的人自己来订阅消息,状态变化时只需"广播",不用一个个点名通知,也不用记住每个人在干什么。
备注:以上内容基于AI生成