白话设计模式之(26):观察者模式——解锁对象交互的高效密码
大家好!在技术学习的道路上,我们都在不断探索如何让代码更优雅、更高效。设计模式作为编程领域的宝贵财富,能为我们提供解决复杂问题的巧妙思路。今天,咱们继续深入探索观察者模式,它就像是对象交互之间的“高效密码”,一旦掌握,就能让对象之间的信息传递和协同工作变得顺畅有序。希望通过这篇博客,能和大家一起更全面地理解观察者模式,在实际编程中灵活运用,提升代码的质量和开发效率。
一、写作初衷
在软件开发的过程中,对象之间的交互关系错综复杂,一个对象的状态变化往往需要牵动多个相关对象做出相应调整。如果没有合适的设计模式来管理这种关系,代码很容易变得混乱不堪,难以维护和扩展。观察者模式为我们提供了一种清晰、简洁的解决方案,通过定义对象间的依赖关系和消息传递机制,实现对象间的解耦和高效协作。我希望通过分享这篇博客,能帮助大家深入理解观察者模式的原理、应用场景和实现细节,让大家在面对类似问题时能够轻松应对,编写出更健壮、更易读的代码。
二、观察者模式解析
(一)定义与核心概念
观察者模式定义了对象间一种一对多的依赖关系,当一个对象(被观察者,也叫目标对象)的状态发生改变时,所有依赖它的对象(观察者)都会得到通知并自动更新。打个比方,就像一场演唱会,歌手(被观察者)在舞台上的每一个动作、每一句歌声(状态改变)都被台下的观众(观察者)关注着。一旦歌手有新的表现,观众们就会做出相应的反应,比如欢呼、鼓掌等。在这个模式中,关键的两个角色是:
- Subject(目标对象/被观察者):它就像演唱会的主办方,知道有哪些观众(观察者)在关注这场演出,负责管理观众的入场(注册观察者)和退场(删除观察者)。当歌手有新的表演环节(自身状态改变)时,会通过广播(通知)的方式让观众知晓。例如在一个电商系统中,商品就是被观察者,它要管理关注该商品的用户(观察者),并在商品的库存、价格等状态发生变化时通知用户。
- Observer(观察者):类似于台下的观众,定义了一个更新接口。当收到被观察者的通知时,会执行这个接口中的方法,进行相应的业务处理。比如观众在听到歌手的精彩表演(收到通知)后,会按照自己的方式欢呼、鼓掌(执行更新方法)。
(二)代码示例
为了让大家更直观地理解,我们以一个简单的在线课堂直播系统为例。在这个系统中,老师(被观察者)在直播过程中发布新的知识点(状态改变)时,观看直播的学生(观察者)会收到通知。
首先定义观察者接口:
// 观察者接口
public interface StudentObserver {
void update(String teacherName, String newKnowledge);
}
接着创建具体的观察者类:
// 具体观察者类 - 学生
public class Student implements StudentObserver {
private String name;
public Student(String name) {
this.name = name;
}
@Override
public void update(String teacherName, String newKnowledge) {
System.out.println(name + ",老师 " + teacherName + " 发布了新知识点:" + newKnowledge);
}
}
然后定义目标对象接口:
// 目标对象接口 - 老师接口
public interface TeacherSubject {
void registerObserver(StudentObserver observer);
void removeObserver(StudentObserver observer);
void notifyObservers(String newKnowledge);
}
再创建具体的目标对象类:
// 具体目标对象类 - 老师
import java.util.ArrayList;
import java.util.List;
public class Teacher implements TeacherSubject {
private String name;
private List<StudentObserver> observers = new ArrayList<>();
public Teacher(String name) {
this.name = name;
}
@Override
public void registerObserver(StudentObserver observer) {
observers.add(observer);
}
@Override
public void removeObserver(StudentObserver observer) {
observers.remove(observer);
}
@Override
public void notifyObservers(String newKnowledge) {
for (StudentObserver observer : observers) {
observer.update(name, newKnowledge);
}
}
// 模拟老师发布新知识点
public void publishKnowledge(String newKnowledge) {
System.out.println(name + " 发布了新知识点:" + newKnowledge);
notifyObservers(newKnowledge);
}
}
最后在客户端代码中使用观察者模式:
public class OnlineClassSystem {
public static void main(String[] args) {
Teacher teacher = new Teacher("李老师");
Student student1 = new Student("小明");
Student student2 = new Student("小红");
teacher.registerObserver(student1);
teacher.registerObserver(student2);
teacher.publishKnowledge("设计模式的概念");
teacher.removeObserver(student2);
teacher.publishKnowledge("观察者模式的应用场景");
}
}
在这个示例中,老师(Teacher)是目标对象,维护着一个学生(观察者)列表。当老师发布新知识点(调用publishKnowledge
方法)时,会通知所有注册的学生,学生收到通知后会执行update
方法进行相应处理。如果某个学生(如小红)退出直播(调用removeObserver
方法),那么后续老师发布的知识点就不会通知到她。
(三)应用场景
- 图形用户界面(GUI)开发:在GUI编程中,观察者模式广泛应用于组件之间的交互。例如,当用户在文本框中输入内容(文本框状态改变)时,可能会触发其他组件(如按钮、标签等)的状态更新。文本框就是被观察者,其他受影响的组件就是观察者。当文本框的内容发生变化时,会通知相关的观察者组件进行相应的处理,比如根据输入内容启用或禁用按钮,更新标签显示的提示信息等。
- 消息订阅与发布系统:这是观察者模式最典型的应用场景之一。像邮件订阅、RSS订阅、即时通讯软件的群组消息通知等。用户订阅了特定的主题或频道(成为观察者),当有新的消息发布(被观察者状态改变)时,系统会自动将消息推送给订阅用户。例如,在一个新闻资讯APP中,用户订阅了不同的新闻类别(如科技、体育、娱乐等),当有新的相关新闻发布时,APP会及时通知订阅该类新闻的用户。
- 游戏开发:在游戏中,观察者模式也起着重要作用。比如游戏角色的状态变化(如生命值、魔法值、等级提升等)会影响到游戏中的其他元素。角色就是被观察者,而游戏界面的显示组件、游戏逻辑模块等可以作为观察者。当角色的生命值降低时,会通知界面显示更新血条,通知游戏逻辑判断是否触发游戏失败条件等;当角色等级提升时,会通知界面显示升级特效,通知游戏系统解锁新的技能或关卡等。
(四)观察者模式的特性与要点
- 推模型和拉模型
- 推模型:目标对象主动向观察者推送目标的详细信息,不管观察者是否需要,推送的信息通常是目标对象的全部或部分数据,类似广播通信。例如,在一个天气预警系统中,气象站(目标对象)将最新的天气预警信息(如暴雨预警、大风预警等详细内容)直接推送给所有订阅的用户(观察者)。
- 拉模型:目标对象在通知观察者的时候,只传递少量信息。如果观察者需要更具体的信息,由观察者主动到目标对象中获取。比如在上述在线课堂直播系统中,老师发布新知识点时只通知学生知识点的简要内容,学生如果想深入了解,需要主动查看老师提供的资料(从目标对象获取数据)。
- 比较与选择:推模型的优点是观察者能及时获取详细信息,但可能会造成数据冗余,且观察者的
update
方法可能因数据固定而缺乏通用性;拉模型的优点是灵活性高,观察者可按需获取数据,但可能增加观察者获取数据的复杂度。在实际开发中,应根据具体情况选择合适的模型。
- Java中的观察者模式
- Java的支持:Java在
java.util
包中提供了Observable
类和Observer
接口,简化了观察者模式的实现。使用Java的这些功能,我们无需自己定义观察者和目标的接口,也不用手动维护观察者的注册信息。触发通知时,需要先调用setChanged
方法,这有助于实现更精确的触发控制。 - 示例:以在线课堂直播系统为例,使用Java内置功能实现时,老师类继承
java.util.Observable
,学生类实现Observer
接口。在老师发布新知识点时,先调用setChanged
方法,再调用notifyObservers
方法通知观察者。具体代码如下:
- Java的支持:Java在
import java.util.Observable;
import java.util.Observer;
// 学生类,实现Observer接口
public class Student implements Observer {
private String name;
public Student(String name) {
this.name = name;
}
@Override
public void update(Observable o, Object arg) {
if (o instanceof Teacher) {
Teacher teacher = (Teacher) o;
System.out.println(name + ",老师 " + teacher.name + " 发布了新知识点:" + arg);
}
}
}
// 老师类,继承Observable
public class Teacher extends Observable {
String name;
public Teacher(String name) {
this.name = name;
}
// 模拟老师发布新知识点
public void publishKnowledge(String newKnowledge) {
System.out.println(name + " 发布了新知识点:" + newKnowledge);
setChanged();
notifyObservers(newKnowledge);
}
}
public class OnlineClassSystemWithJavaUtil {
public static void main(String[] args) {
Teacher teacher = new Teacher("李老师");
Student student1 = new Student("小明");
Student student2 = new Student("小红");
teacher.addObserver(student1);
teacher.addObserver(student2);
teacher.publishKnowledge("设计模式的概念");
teacher.deleteObserver(student2);
teacher.publishKnowledge("观察者模式的应用场景");
}
}
- 目标和观察者的关系
- 一对多关系:目标和观察者之间通常是一对多的关系,但也可以是一对一(只有一个观察者)。一个观察者也可以观察多个目标,不过这种情况下需要在观察者的更新方法中区分通知来自哪个目标,可通过扩展
update
方法传递参数或定义不同回调方法来实现。 - 单向依赖:观察者依赖于目标,目标不依赖于观察者。目标掌握通知的主动权,观察者被动等待通知。在特殊情况下,目标可以有区别地对待观察者,但这不属于标准的原始观察者模式。
- 一对多关系:目标和观察者之间通常是一对多的关系,但也可以是一对一(只有一个观察者)。一个观察者也可以观察多个目标,不过这种情况下需要在观察者的更新方法中区分通知来自哪个目标,可通过扩展
- 实现要点
- 具体的目标实现对象要维护观察者的注册信息,通常用集合来保存。
- 目标实现对象需要维护引起通知的状态,一般是自身状态,特殊情况也可以是其他对象的状态。
- 具体的观察者实现对象要能接收目标的通知,接收传递的数据或主动获取数据并处理。
- 注意触发通知的时机,一般在完成状态维护后触发,避免观察者获取到旧数据,导致状态不一致。
- 在相互观察的情况下(如A对象观察B对象,B对象也观察A对象),要小心处理,防止出现死循环。
- 多个观察者接收通知的顺序是不确定的,观察者的功能不应依赖于通知顺序。
- 优缺点
- 优点
- 抽象耦合与解耦:通过抽象出观察者接口,目标对象只知道观察者接口,而不知道具体的观察者类,实现了目标类和具体观察者类之间的解耦,提高了代码的可维护性和扩展性。
- 动态联动:可以在运行期间动态控制注册的观察者,从而灵活控制某个动作的联动范围,实现动态联动。例如,在一个游戏中,可以根据玩家的不同操作,动态添加或删除某些观察者,控制游戏中不同元素的联动效果。
- 支持广播通信:目标对象可以向所有注册的观察者发送通知,实现广播通信。虽然可能存在相互广播造成死循环的问题,但通过合理设计可以避免,并且可以通过添加功能限制广播范围。
- 缺点
- 可能引起无谓操作:由于是广播通信,不管观察者是否需要,每个观察者都会被调用
update
方法。如果观察者不需要执行相应处理,就会造成资源浪费,甚至可能引起误更新,导致错误的操作结果。
- 可能引起无谓操作:由于是广播通信,不管观察者是否需要,每个观察者都会被调用
- 优点
- 观察者模式的本质:观察者模式的本质是触发联动。当修改目标对象的状态时,会触发相应的通知,循环调用所有注册观察者对象的相应方法,实现动态联动。通过注册和取消注册观察者,可以在程序运行期间动态控制联动的功能,同时目标对象和观察者对象的解耦保证了无论观察者如何变化,目标对象都能正确地联动。
- 何时选用观察者模式
- 当一个抽象模型有两个相互依赖的方面,其中一个方面的操作依赖于另一个方面的状态变化时,可以选用观察者模式,将这两个方面封装成观察者和目标对象,使它们可以独立地改变和复用。
- 当更改一个对象时,需要连带改变多个其他对象,且不确定具体有多少对象需要改变时,可选用观察者模式,被更改的对象作为目标对象,需要连带修改的对象作为观察者对象。
- 当一个对象必须通知其他对象,但又希望与被通知对象保持松散耦合时,可选用观察者模式,该对象作为目标对象,被通知对象作为观察者对象。
(五)Swing中的观察者模式及变形示例
- Swing中的应用:Java的Swing中广泛应用了观察者模式,例如事件处理。Swing组件是被观察的目标,实现监听器的类就是观察者,监听器接口就是观察者接口。调用
addXXXListener
方法相当于注册观察者,当组件状态发生改变(如被单击)时,会调用注册的观察者的方法(即监听器的方法)。这为我们处理Swing组件的交互提供了便利,同时也展示了一个观察者观察多个目标对象的实现方式,即不同的目标对象使用不同的观察者接口,接口中的方法也不同,从而在具体实现观察者时能够区分不同目标对象的通知。 - 简单变形示例——区别对待观察者:在实际应用中,观察者模式常常需要根据具体需求进行变形。以水质监测系统为例,不同程度的水质污染需要通知不同的人员进行处理。这种情况下,我们可以在目标对象中进行判断,根据不同的状态有选择地通知观察者,而不是像标准模式那样通知所有观察者。通过定义观察者接口,并在具体观察者实现中根据自身职责进行相应处理,实现对不同观察者的区别对待,这体现了观察者模式的灵活性和可扩展性。
五、总结
观察者模式作为一种强大的设计模式,在处理对象间的依赖关系和消息传递方面具有显著的优势。通过合理运用观察者模式,我们可以使代码结构更加清晰,降低耦合度,提高系统的可维护性和扩展性。在实际开发中,要根据具体的业务需求和场景,选择合适的实现方式(如推模型或拉模型),并充分利用Java等编程语言提供的相关功能,同时注意避免观察者模式可能带来的问题。
写作不易,如果这篇文章对你有所帮助,希望大家能关注我的博客,点赞评论支持一下!你的每一个点赞、评论和关注都是对我最大的鼓励,我会持续为大家带来更多设计模式相关的优质内容,咱们下次再见!