设计模式--观察者模式 (Observer)
观察者模式定义了一种一对多的依赖关系,当一个对象(主题)状态改变时,所有依赖它的对象(观察者)都会自动收到通知。核心是实现主题与观察者之间的解耦。
我们来详细、清晰地解析一下设计模式中的观察者模式(Observer Pattern)。
1. 什么是观察者模式?
观察者模式是一种行为型设计模式(Behavioral Pattern)。
核心定义: 它定义了对象之间一种一对多的依赖关系。当一个对象(被称为“主题”或“被观察者”)的状态发生改变时,所有依赖于它的对象(被称为“观察者”)都会得到通知并自动更新。
通俗比喻:
想象一下你在B站或YouTube上关注(Subscribe)了一个UP主。
- UP主 (Subject / 被观察者): 他是内容的发布者。
- 你和其他粉丝 (Observers / 观察者): 你们是内容的接收者。
当UP主发布一个新视频时(状态改变),B站的系统会自动向所有关注他的粉丝发送通知(“你关注的UP主更新啦!”)。你不需要每天去刷新UP主的主页,系统会自动“推”送消息给你。
在这个过程中:
- UP主不需要知道具体是“张三”还是“李四”关注了他,他只管发布视频。
- 你也不需要直接和UP主联系,你只管关注和接收通知。
- UP主和粉丝之间通过“关注”这个行为解耦了。
这就是观察者模式的核心思想:解耦主题与观察者。
2. 为什么需要观察者模式?(它解决了什么问题)
如果没有观察者模式,当UP主发布新视频时,他可能需要一个“粉丝列表”,然后手动一个一个去通知他们。
// 伪代码:紧耦合的方式
class UpUploader {
FanA fanA;
FanB fanB;
FanC fanC;
void publishNewVideo() {
// ...发布视频的逻辑...
// 紧密耦合:UpUploader 必须知道每一个具体的粉丝对象
fanA.receiveUpdate("新视频来了!");
fanB.receiveUpdate("新视频来了!");
fanC.receiveUpdate("新视频来了!");
}
}
这种方式的缺点非常明显:
- 紧密耦合:
UpUploader类依赖于具体的FanA,FanB等类。 - 难以扩展: 每当有新粉丝
FanD加入,就必须修改UpUploader类的代码。这违反了开闭原则(对扩展开放,对修改关闭)。 - 职责不清:
UpUploader不仅要负责发布视频,还要负责通知粉丝,职责过重。
观察者模式通过引入一个抽象层,完美地解决了这些问题。
3. 观察者模式的结构与参与者
观察者模式通常包含以下四个核心角色:
Subject (主题 / 抽象被观察者):
- 定义了一个接口,用于附加(Attach)、分离(Detach)和通知(Notify)观察者。
- 它维护了一个观察者对象的列表。
ConcreteSubject (具体主题 / 具体被观察者):
- 实现了
Subject接口。 - 它包含着可能会发生变化的状态。
- 当其状态改变时,会向所有已注册的观察者发出通知。
- 实现了
Observer (观察者 / 抽象观察者):
- 定义了一个更新接口(通常是
update()方法),当接收到主题的通知时,该方法被调用。
- 定义了一个更新接口(通常是
ConcreteObserver (具体观察者):
- 实现了
Observer接口。 - 它维护着一个指向
ConcreteSubject对象的引用。 - 在
update()方法中实现具体的业务逻辑,以响应主题状态的变化。
- 实现了
UML 类图
4. 代码实现示例 (Java)
让我们用代码来实现上面那个“UP主与粉丝”的例子。
第1步:定义抽象接口
// Observer (抽象观察者)
interface Observer {
void update(String message);
}
// Subject (抽象主题)
interface Subject {
void attach(Observer observer); // 附加观察者(关注)
void detach(Observer observer); // 分离观察者(取关)
void notifyObservers(); // 通知所有观察者
}
第2步:创建具体实现类
import java.util.ArrayList;
import java.util.List;
// ConcreteSubject (具体主题 - UP主)
class UpUploader implements Subject {
private List<Observer> fans = new ArrayList<>();
private String latestVideoTitle;
@Override
public void attach(Observer observer) {
fans.add(observer);
System.out.println(observer + " 关注了UP主!");
}
@Override
public void detach(Observer observer) {
fans.remove(observer);
System.out.println(observer + " 取消了关注!");
}
@Override
public void notifyObservers() {
// 遍历粉丝列表,通知每一个粉丝
for (Observer fan : fans) {
fan.update(latestVideoTitle);
}
}
// UP主发布新视频
public void publishNewVideo(String videoTitle) {
this.latestVideoTitle = videoTitle;
System.out.println("\nUP主发布了新视频: 《" + videoTitle + "》");
// 视频发布后,通知所有粉丝
notifyObservers();
}
}
// ConcreteObserver (具体观察者 - 粉丝)
class Fan implements Observer {
private String name;
public Fan(String name) {
this.name = name;
}
@Override
public void update(String message) {
System.out.println(name + " 收到了通知: " + "UP主的新视频是《" + message + "》, 马上就去看!");
}
@Override
public String toString() {
return "粉丝[" + name + "]";
}
}
第3步:客户端测试
public class Demo {
public static void main(String[] args) {
// 1. 创建一个UP主 (具体主题)
UpUploader up = new UpUploader();
// 2. 创建几个粉丝 (具体观察者)
Observer fanA = new Fan("张三");
Observer fanB = new Fan("李四");
Observer fanC = new Fan("王五");
// 3. 粉丝关注UP主 (将观察者附加到主题)
up.attach(fanA);
up.attach(fanB);
up.attach(fanC);
// 4. UP主发布新视频 (主题状态改变,并通知观察者)
up.publishNewVideo("观察者模式的详细教学");
// 5. 有一个粉丝取关了
System.out.println("\n--- 李四觉得视频太难,取关了 ---");
up.detach(fanB);
// 6. UP主又发布了一个新视频
up.publishNewVideo("单例模式其实很简单");
}
}
运行结果:
粉丝[张三] 关注了UP主!
粉丝[李四] 关注了UP主!
粉丝[王五] 关注了UP主!
UP主发布了新视频: 《观察者模式的详细教学》
张三 收到了通知: UP主的新视频是《观察者模式的详细教学》, 马上就去看!
李四 收到了通知: UP主的新视频是《观察者模式的详细教学》, 马上就去看!
王五 收到了通知: UP主的新视频是《观察者模式的详细教学》, 马上就去看!
--- 李四觉得视频太难,取关了 ---
粉丝[李四] 取消了关注!
UP主发布了新视频: 《单例模式其实很简单》
张三 收到了通知: UP主的新视频是《单例模式其实很简单》, 马上就去看!
王五 收到了通知: UP主的新视频是《单例模式其实很简单》, 马上就去看!
从结果可以看出,当UP主发布新视频时,所有关注他的粉丝都收到了通知。当李四取关后,他就再也收不到新视频的通知了。整个过程,UpUploader 类完全不知道 Fan 类的存在,它只与 Observer 接口交互,实现了松耦合。
5. 优缺点
优点:
- 解耦: 完美地将主题和观察者解耦。主题只知道它有一系列观察者,但不知道它们是谁,是什么类型。
- 符合开闭原则: 可以随时增加新的观察者,而无需修改主题的代码。
- 广播机制: 一个主题的改变可以通知到任意数量的观察者,非常适合实现广播式的通信。
- 职责分离: 主题只负责维护自身状态和管理观察者列表,而观察者则负责在收到通知后执行自己的逻辑,职责清晰。
缺点:
- 通知顺序不确定: 如果观察者之间的更新顺序很重要,那么观察者模式默认的实现可能无法满足,需要额外逻辑来控制。
- 可能导致级联更新: 如果一个观察者的
update方法又会触发其他对象的状态改变,可能会形成一个复杂的更新链条,导致系统难以调试和维护(雪崩效应)。 - 内存泄漏风险(“Lapsed listener problem”): 如果一个观察者被销毁了,但没有从主题中分离(detach),主题仍然会持有对这个观察者的引用,导致它无法被垃圾回收,从而造成内存泄漏。
6. 实际应用场景
观察者模式在现实世界的软件开发中非常常见:
- GUI事件处理: 比如按钮(Subject)的点击事件。多个监听器(Observers)可以注册到按钮上,当按钮被点击时,所有监听器都会收到通知并执行相应的操作。例如 Java AWT/Swing 的
ActionListener,JavaScript 的addEventListener。 - MVC/MVVM 架构:
- 在MVC中,模型(Model)是主题,视图(View)是观察者。当模型的数据发生变化时,它会通知所有相关的视图进行更新。
- 消息队列/事件总线: 发布-订阅模式(Publish-Subscribe Pattern)是观察者模式的一个变体。生产者(Publisher)发布消息到队列(Topic/Channel),所有订阅了该主题的消费者(Subscribers)都会收到消息。
- 电子表格: 在Excel这样的应用中,一个单元格(Subject)的值发生改变,所有依赖于它的公式的单元格(Observers)都需要重新计算和更新。
7. 与发布-订阅模式的区别
虽然两者非常相似,但有一个关键区别:
- 观察者模式: 主题(Subject)直接持有观察者(Observer)的引用,并直接调用其
update方法。它们之间是直接通信的。 - 发布-订阅模式: 发布者(Publisher)和订阅者(Subscriber)之间是完全解耦的,它们通过一个中间件(如消息代理、事件总线)进行通信。发布者将消息发送到中间件,订阅者从中间件接收消息。彼此完全不知道对方的存在。
可以认为,发布-订阅模式是观察者模式的“升级版”,解耦更彻底。
希望这个详细的解释能帮助你完全理解观察者模式!