系列定位:软考软件设计师 / 系统架构设计师 --- 行为型模式专题第 9 讲
考察分值:上午题 1-2 分,下午题常作为代码填空或类图识别出现
难度等级:⭐⭐⭐☆☆(结构清晰,Subject 与 Observer 的一对多关联是填空重点)
一、考纲定位与模式定义
1.1 考纲要求
观察者模式在软考中属于 行为型模式 的基础内容,也是 MVC 架构的核心模式之一。考察形式包括:
-
上午选择题:判断场景描述所属模式;识别观察者模式与中介者模式、迭代器模式的区别;判断 Subject、Observer、ConcreteSubject、ConcreteObserver 四个角色;识别 "一对多依赖"、"状态变化自动通知" 的设计意图
-
下午设计题 :补全
Subject接口中attach()、detach()、notify()方法的代码;补全ConcreteSubject中状态变化时调用notify()的逻辑;补全Observer接口中update()方法的代码;补全ConcreteObserver中接收通知并更新自身状态的逻辑;识别推模式与拉模式的代码差异
1.2 模式定义
观察者模式:定义对象间的一种一对多依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
核心意图 :当一个对象(Subject)的状态变化需要 自动通知 多个其他对象(Observer),且这些对象的数量和类型可能 动态变化 时,如果 Subject 直接持有每个 Observer 的引用并逐个调用,会导致:
-
Subject 与 Observer 紧耦合,新增 Observer 需要修改 Subject
-
Subject 职责过重(既要管理自身状态,又要管理通知列表)
-
无法动态增删 Observer,灵活性差
观察者模式将 通知机制 抽象化:Subject 只维护一个 Observer 列表,状态变化时统一调用 notify() 广播通知。Observer 通过 attach() 订阅、detach() 取消订阅。Subject 不知道也不关心具体有哪些 Observer,只面向 Observer 接口编程。
通俗理解:
你关注了一个微信公众号(Subject)。每次公众号发文(状态变化),所有关注它的粉丝(Observer)都会收到微信推送(自动通知)。你不需要每天去刷新公众号主页看它有没有更新------它更新了会自动告诉你。新增一个粉丝?粉丝点个关注(attach)就行,公众号不需要修改任何代码。粉丝取关?点取消关注(detach)就行,公众号照样不用改。公众号只负责发文和群发,粉丝各自决定收到文章后做什么(有的转发、有的收藏、有的看完就关),这就是一对多的订阅-发布机制。
二、UML 类图与角色划分
┌─────────────────────────────────────────────────────────┐
│ Client │
│ + main() │
│ ├── ConcreteSubject subject = new ConcreteSubject() │
│ ├── Observer obs1 = new ConcreteObserver("观察者A") │
│ ├── Observer obs2 = new ConcreteObserver("观察者B") │
│ ├── subject.attach(obs1) // 订阅 │
│ ├── subject.attach(obs2) // 订阅 │
│ ├── subject.setState("新状态") // 状态变化 │
│ │ // 内部自动调用 notify() → 所有 Observer 收到通知 │
│ ├── subject.detach(obs1) // 取消订阅 │
│ └── subject.setState("再变化") // 只有 B 收到通知 │
└───────────────────────┬─────────────────────────────────┘
│ uses
▼
┌─────────────────────────────────────────────────────────┐
│ <<interface>> │
│ Subject │
│ + attach(Observer o) ◄── 订阅/注册观察者 │
│ + detach(Observer o) ◄── 取消订阅/移除观察者 │
│ + notify() ◄── 状态变化时通知所有观察者 │
└───────────────────────┬─────────────────────────────────┘
│ implements
▼
┌─────────────────────────────────────────────────────────┐
│ ConcreteSubject │
│ - observers: List<Observer> ◄── 观察者列表 │
│ - state: String ◄── 被观察的状态 │
│ + setState(String s) { │
│ this.state = s; │
│ notify(); // 状态变化后自动通知所有观察者 │
│ } │
│ + getState(): String │
│ + attach(o) { observers.add(o); } │
│ + detach(o) { observers.remove(o); } │
│ + notify() { │
│ for (Observer o : observers) { │
│ o.update(this); // 广播通知 │
│ } │
│ } │
└───────────────────────┬─────────────────────────────────┘
│ notifies
▼
┌─────────────────────────────────────────────────────────┐
│ <<interface>> │
│ Observer │
│ + update(Subject sub) ◄── 接收通知,更新自身状态 │
└───────────────────────┬─────────────────────────────────┘
│ implements
▼
┌──────────────────────────────────────────────────────────────┐
│ ConcreteObserver │
│ - name: String │
│ - observerState: String ◄── 观察者自身状态 │
│ + update(Subject sub) { │
│ observerState = sub.getState(); │
│ System.out.println(name + " 收到更新: " + observerState);│
│ } │
└──────────────────────────────────────────────────────────────┘
| 角色 | 职责 | 软考填空关键词 |
|---|---|---|
| Subject(抽象主题) | 定义管理 Observer 的接口,声明 attach()、detach()、notify() 方法 |
interface Subject + void attach(Observer o) + void detach(Observer o) + void notify() |
| ConcreteSubject(具体主题) | 实现 Subject 接口,维护具体的业务状态,状态变化时调用 notify() 通知所有注册的 Observer |
implements Subject + List<Observer> observers + setState() 内部调用 notify() |
| Observer(抽象观察者) | 定义更新接口,声明 update(Subject) 方法,供 Subject 状态变化时调用 |
interface Observer + void update(Subject sub) |
| ConcreteObserver(具体观察者) | 实现 Observer 接口,在 update() 中接收通知,根据需要从 Subject 获取状态并更新自身 |
implements Observer + update() 中调用 subject.getState() |
| Client(客户端) | 创建 ConcreteSubject 和 ConcreteObserver,调用 attach() 建立订阅关系,触发状态变化观察通知效果 |
subject.attach(observer) + subject.setState() |
类图识别要点 :软考类图中,观察者模式的核心特征是 Subject 类持有一个 Observer 列表(一对多关联),且 Subject 中有
notify()方法遍历列表调用observer.update()。如果类图里有 "一个类持有多个观察者引用,状态变化时广播通知" → 观察者模式。
三、场景一:新闻订阅系统(最经典的观察者模式)
业务背景:新闻发布平台(NewsPublisher)发布新闻,多个订阅者(Subscriber)通过邮件、短信、APP 推送等不同渠道接收通知。订阅者可以动态订阅和取消订阅。新闻发布时,所有当前订阅者自动收到通知。
说明:这是观察者模式最经典的例子,也是软考下午题最爱考的 "订阅-发布" 场景。
3.1 代码实现
java
// Observer:观察者接口
public interface Subscriber {
void update(String newsTitle); // 推模式:直接接收新闻标题
}
// ConcreteObserver:邮件订阅者
public class EmailSubscriber implements Subscriber {
private String email;
public EmailSubscriber(String email) {
this.email = email;
}
@Override
public void update(String newsTitle) {
System.out.println("【邮件推送】向 " + email + " 发送邮件: 新文章《" + newsTitle + "》已发布");
}
}
// ConcreteObserver:短信订阅者
public class SmsSubscriber implements Subscriber {
private String phone;
public SmsSubscriber(String phone) {
this.phone = phone;
}
@Override
public void update(String newsTitle) {
System.out.println("【短信推送】向 " + phone + " 发送短信: 新文章《" + newsTitle + "》已发布");
}
}
// ConcreteObserver:APP 推送订阅者
public class AppSubscriber implements Subscriber {
private String userId;
public AppSubscriber(String userId) {
this.userId = userId;
}
@Override
public void update(String newsTitle) {
System.out.println("【APP推送】用户 " + userId + " 收到推送: 新文章《" + newsTitle + "》");
}
}
// Subject:新闻发布者接口
public interface NewsPublisher {
void subscribe(Subscriber subscriber);
void unsubscribe(Subscriber subscriber);
void publish(String newsTitle);
}
// ConcreteSubject:具体新闻发布者
public class TechNewsPublisher implements NewsPublisher {
private List<Subscriber> subscribers = new ArrayList<>();
private String latestNews;
@Override
public void subscribe(Subscriber subscriber) {
subscribers.add(subscriber);
System.out.println("【系统】新订阅者加入,当前订阅数: " + subscribers.size());
}
@Override
public void unsubscribe(Subscriber subscriber) {
subscribers.remove(subscriber);
System.out.println("【系统】订阅者退出,当前订阅数: " + subscribers.size());
}
@Override
public void publish(String newsTitle) {
this.latestNews = newsTitle;
System.out.println("【发布者】发布新闻: " + newsTitle);
notifySubscribers(newsTitle);
}
// 通知所有订阅者(推模式:直接传递新闻标题)
private void notifySubscribers(String newsTitle) {
for (Subscriber subscriber : subscribers) {
subscriber.update(newsTitle);
}
}
public String getLatestNews() {
return latestNews;
}
}
// Client:客户端
public class Client {
public static void main(String[] args) {
TechNewsPublisher publisher = new TechNewsPublisher();
// 创建订阅者
Subscriber emailSub = new EmailSubscriber("user@example.com");
Subscriber smsSub = new SmsSubscriber("13800138000");
Subscriber appSub = new AppSubscriber("User001");
// 订阅
publisher.subscribe(emailSub);
publisher.subscribe(smsSub);
publisher.subscribe(appSub);
System.out.println("---");
// 发布新闻:所有订阅者自动收到通知
publisher.publish("Java 22 正式发布");
System.out.println("---");
// 短信订阅者取消订阅
publisher.unsubscribe(smsSub);
// 再发布新闻:只有邮件和 APP 订阅者收到
publisher.publish("Spring Boot 3.3 新特性详解");
}
}
关键体会:
-
TechNewsPublisher(ConcreteSubject)只维护一个List<Subscriber>,不知道也不关心具体是邮件订阅者还是短信订阅者。它只面向Subscriber接口调用update()。 -
新增一种订阅渠道(如微信订阅者),只需新增一个
implements Subscriber的类,不需要修改TechNewsPublisher的代码。这符合 开闭原则。 -
发布新闻时,
publish()内部自动调用notifySubscribers(),所有订阅者 同步 收到通知。这就是 "状态变化自动更新" 的机制。 -
本例使用的是 推模式 (Push):Subject 直接把新闻标题通过
update(String)推送给 Observer。Observer 不需要再回头问 Subject 要数据。
四、场景二:股票价格监控(实际开发场景)
业务背景:股票交易系统中有股票对象(Stock),价格实时变动。多个监控组件需要响应价格变化:①价格显示器(实时显示当前价格)②涨跌幅报警器(价格涨跌超过 5% 时报警)③交易量统计器(记录价格变化时的交易量)。要求监控组件可以动态增删,股票对象不需要知道有哪些监控组件。
说明:这是实际开发中观察者模式的典型应用,也是软考下午题中 "状态变化驱动多组件响应" 场景的代表。
4.1 代码实现
java
// Observer:股票观察者接口(拉模式:Observer 自己从 Subject 拉取需要的数据)
public interface StockObserver {
void update(Stock stock); // 拉模式:传入 Subject 引用,Observer 自行获取数据
}
// ConcreteObserver:价格显示器
public class PriceDisplay implements StockObserver {
private String displayName;
public PriceDisplay(String name) {
this.displayName = name;
}
@Override
public void update(Stock stock) {
System.out.println("【价格显示器 " + displayName + "】股票 " + stock.getSymbol() +
" 当前价格: ¥" + stock.getPrice());
}
}
// ConcreteObserver:涨跌幅报警器
public class PriceAlert implements StockObserver {
private double lastPrice;
@Override
public void update(Stock stock) {
double currentPrice = stock.getPrice();
if (lastPrice != 0) {
double change = Math.abs(currentPrice - lastPrice) / lastPrice * 100;
if (change > 5) {
System.out.println("【报警器】股票 " + stock.getSymbol() +
" 价格剧烈波动!涨跌幅度: " + String.format("%.2f", change) + "%");
}
}
lastPrice = currentPrice;
}
}
// ConcreteObserver:交易量统计器
public class VolumeTracker implements StockObserver {
private int updateCount = 0;
@Override
public void update(Stock stock) {
updateCount++;
System.out.println("【交易量统计】股票 " + stock.getSymbol() +
" 第 " + updateCount + " 次价格更新,当前交易量: " + stock.getVolume());
}
}
// Subject:股票接口
public interface StockSubject {
void addObserver(StockObserver observer);
void removeObserver(StockObserver observer);
void notifyObservers();
}
// ConcreteSubject:具体股票
public class Stock implements StockSubject {
private String symbol;
private double price;
private int volume;
private List<StockObserver> observers = new ArrayList<>();
public Stock(String symbol, double price, int volume) {
this.symbol = symbol;
this.price = price;
this.volume = volume;
}
public String getSymbol() { return symbol; }
public double getPrice() { return price; }
public int getVolume() { return volume; }
// 价格变动:set 方法内部自动通知
public void setPrice(double price) {
this.price = price;
System.out.println("【市场】股票 " + symbol + " 价格变动为: ¥" + price);
notifyObservers();
}
// 交易量变动
public void setVolume(int volume) {
this.volume = volume;
notifyObservers();
}
@Override
public void addObserver(StockObserver observer) {
observers.add(observer);
}
@Override
public void removeObserver(StockObserver observer) {
observers.remove(observer);
}
@Override
public void notifyObservers() {
for (StockObserver observer : observers) {
observer.update(this); // 拉模式:传入自身引用
}
}
}
// Client
public class Client {
public static void main(String[] args) {
Stock stock = new Stock("600519", 1688.00, 10000);
// 注册多个观察者
stock.addObserver(new PriceDisplay("大屏1"));
stock.addObserver(new PriceAlert());
stock.addObserver(new VolumeTracker());
System.out.println("=== 第一次价格变动 ===");
stock.setPrice(1750.00);
System.out.println("=== 第二次价格变动(剧烈波动)===");
stock.setPrice(1850.00);
}
}
五、场景三:MVC 模型通知视图(架构模式场景)
业务背景:一个基于 MVC 架构的学生信息管理系统。模型(StudentModel)保存学生数据,多个视图(ListView、ChartView、DetailView)需要显示这些数据。当模型中的学生数据发生变化(增删改)时,所有已注册的视图需要自动刷新显示。使用观察者模式实现模型与视图的解耦。
说明:这是观察者模式在架构设计场景中的典型应用,也是软考上午题中 "MVC 架构" 场景的代表。
5.1 代码实现
java
// Observer:视图接口
public interface DataView {
void refresh(Model model); // 拉模式:从模型拉取数据刷新显示
}
// ConcreteObserver:列表视图
public class ListView implements DataView {
@Override
public void refresh(Model model) {
System.out.println("【列表视图】刷新显示,当前学生数: " + model.getStudentCount());
}
}
// ConcreteObserver:图表视图
public class ChartView implements DataView {
@Override
public void refresh(Model model) {
System.out.println("【图表视图】刷新统计图表,平均分: " + model.getAverageScore());
}
}
// ConcreteObserver:详情视图
public class DetailView implements DataView {
@Override
public void refresh(Model model) {
System.out.println("【详情视图】刷新当前选中学生: " + model.getSelectedStudent());
}
}
// Subject:模型接口
public interface Model {
void registerView(DataView view);
void removeView(DataView view);
void notifyViews();
}
// ConcreteSubject:学生模型
public class StudentModel implements Model {
private List<DataView> views = new ArrayList<>();
private List<Student> students = new ArrayList<>();
private Student selectedStudent;
public void addStudent(Student student) {
students.add(student);
System.out.println("【模型】添加学生: " + student.getName());
notifyViews(); // 数据变化,自动通知所有视图
}
public void deleteStudent(int id) {
students.removeIf(s -> s.getId() == id);
System.out.println("【模型】删除学生 ID: " + id);
notifyViews();
}
public void selectStudent(int id) {
this.selectedStudent = students.stream()
.filter(s -> s.getId() == id)
.findFirst()
.orElse(null);
notifyViews();
}
@Override
public void registerView(DataView view) {
views.add(view);
}
@Override
public void removeView(DataView view) {
views.remove(view);
}
@Override
public void notifyViews() {
for (DataView view : views) {
view.refresh(this); // 拉模式:传入模型自身
}
}
// 供视图拉取的数据方法
public int getStudentCount() {
return students.size();
}
public double getAverageScore() {
return students.stream().mapToInt(Student::getScore).average().orElse(0);
}
public String getSelectedStudent() {
return selectedStudent != null ? selectedStudent.getName() : "无";
}
}
// Student 实体
public class Student {
private int id;
private String name;
private int score;
public Student(int id, String name, int score) {
this.id = id;
this.name = name;
this.score = score;
}
public int getId() { return id; }
public String getName() { return name; }
public int getScore() { return score; }
}
// Client
public class Client {
public static void main(String[] args) {
StudentModel model = new StudentModel();
// 注册多个视图
DataView listView = new ListView();
DataView chartView = new ChartView();
DataView detailView = new DetailView();
model.registerView(listView);
model.registerView(chartView);
model.registerView(detailView);
System.out.println("=== 添加学生张三 ===");
model.addStudent(new Student(1, "张三", 85));
System.out.println("=== 添加学生李四 ===");
model.addStudent(new Student(2, "李四", 92));
System.out.println("=== 选中学生 ===");
model.selectStudent(1);
System.out.println("=== 移除图表视图 ===");
model.removeView(chartView);
System.out.println("=== 删除学生 ===");
model.deleteStudent(1);
}
}
六、三种场景对比与演进思路
| 维度 | 场景一:新闻订阅 | 场景二:股票监控 | 场景三:MVC 模型视图 |
|---|---|---|---|
| Subject | NewsPublisher(新闻发布者) |
Stock(股票) |
StudentModel(学生模型) |
| Observer | Subscriber(订阅者) |
StockObserver(股票观察者) |
DataView(数据视图) |
| ConcreteObserver | EmailSubscriber、SmsSubscriber、AppSubscriber |
PriceDisplay、PriceAlert、VolumeTracker |
ListView、ChartView、DetailView |
| 通知模式 | 推模式 (Push):update(String newsTitle) |
拉模式 (Pull):update(Stock stock) |
拉模式 (Pull):refresh(Model model) |
| 状态变化触发 | publish() 方法内部调用通知 |
setPrice() 内部调用通知 |
addStudent() 等数据操作方法内部调用通知 |
| 观察者数量 | 动态增减(用户订阅/退订) | 固定注册(系统组件) | 固定注册(视图组件) |
| 典型应用 | 消息推送、RSS 订阅 | 实时监控、事件驱动 | MVC 架构、数据绑定 |
| 软考考法 | 下午大题(补全订阅/通知逻辑) | 下午大题(补全拉模式 update) | 上午选择题(识别 MVC 中的观察者) |
七、观察者模式 vs 中介者模式 vs 迭代器模式:核心对比
| 对比项 | 观察者模式 | 中介者模式 | 迭代器模式 |
|---|---|---|---|
| 模式分类 | 行为型 | 行为型 | 行为型 |
| 核心思想 | 一对多依赖,状态变化自动通知 | 多对多交互 解耦为星型协调 | 封装遍历逻辑,不暴露聚合内部结构 |
| 通信方向 | 单向广播(Subject → Observer) | 双向协调(Colleague ↔ Mediator ↔ Colleague) | 单向顺序访问(Iterator 遍历聚合) |
| 对象关系 | Subject 持有多个 Observer 引用(一对多) | Mediator 持有多个 Colleague,Colleague 也持有 Mediator(多对多转星型) | Iterator 持有 Aggregate 引用(一对一) |
| 触发方式 | Subject 状态变化 自动触发通知 | Colleague 主动请求 或事件触发,Mediator 协调转发 | 客户端 主动调用 hasNext()/next() |
| 关注点 | 谁发生了变化,通知给谁 | 谁和谁需要交互,如何协调 | 如何按顺序访问每个元素 |
| 动态性 | Observer 可动态 订阅/退订 | Colleague 可动态 注册/注销 | 迭代器可动态 创建/销毁 |
| 典型应用 | 消息订阅、事件监听、MVC 模型通知 | 聊天室、航空管制、UI 组件协调 | 集合遍历、文件系统遍历、树形结构遍历 |
| 软考判断 | 看到 "一对多"、"状态变化自动通知"、"订阅发布" → 观察者 | 看到 "多对多解耦"、"网状变星型"、"集中协调" → 中介者 | 看到 "顺序访问"、"不暴露内部"、"hasNext/next" → 迭代器 |
记忆口诀:观察者是 "公众号推送"(一对多广播),中介者是 "物业协调"(多对多转星型),迭代器是 "导游带路"(顺序访问不暴露内部)。
八、软考高频考点与易混淆辨析
8.1 高频考点
| 考点 | 内容 |
|---|---|
| 模式分类 | 行为型模式(GoF 23 正式成员) |
| 核心四角色 | Subject(抽象主题)、ConcreteSubject(具体主题)、Observer(抽象观察者)、ConcreteObserver(具体观察者) |
| 核心思想 | 定义对象间的一对多依赖,状态变化时自动通知所有依赖对象 |
| 一对多关系 | Subject 持有 List<Observer>,一对多关联 |
| 订阅机制 | Observer 通过 attach()/subscribe() 注册,通过 detach()/unsubscribe() 注销 |
| 自动通知 | Subject 状态变化时(通常在 setter 方法内)自动调用 notify() 广播通知 |
| 推模式(Push) | Subject 在 notify() 中把 具体数据 通过 update(data) 推送给 Observer |
| 拉模式(Pull) | Subject 在 notify() 中只传递 自身引用 update(this),Observer 自行调用 getter 拉取数据 |
| 与中介者模式区别 | 观察者:一对多广播,单向通知;中介者:多对多协调,双向转发 |
| 与迭代器模式区别 | 观察者:状态驱动通知;迭代器:客户端驱动遍历 |
| 适用场景 | ① 一个对象状态变化需要通知多个对象 ② 观察者数量动态变化 ③ 需要解耦 Subject 与 Observer |
| Java 内置 | java.util.Observable 和 java.util.Observer(JDK 9 已标记弃用,推荐使用 java.beans.PropertyChangeSupport 或响应式编程) |
8.2 易混淆辨析:观察者 vs 中介者 vs 迭代器
| 对比项 | 观察者模式 | 中介者模式 | 迭代器模式 |
|---|---|---|---|
| UML 结构 | Subject → Observer 单向一对多 | Mediator ↔ Colleague 双向星型 | Aggregate → Iterator 工厂创建 |
| 触发源 | Subject 状态变化 自动触发 | Colleague 主动请求 或事件触发 | 客户端 主动调用 遍历方法 |
| 通信方向 | 单向(Subject 广播给 Observer) | 双向(Mediator 转发 Colleague 请求) | 单向(Iterator 按序返回元素) |
| 类图箭头 | Subject 聚合 Observer(空心菱形) | Mediator 关联 Colleague,Colleague 关联 Mediator | Aggregate 关联 Iterator(工厂方法) |
| 软考考法 | 代码填空(补全 attach/detach/notify + update) |
代码填空(补全 Mediator 转发逻辑 + Colleague 委托) | 代码填空(补全 hasNext/next + createIterator) |
九、真题风格模拟与代码填空
模拟题 1(上午选择题)
以下关于观察者模式的叙述中,正确的是()。
A. 观察者模式属于结构型模式,主要用于简化复杂子系统的接口
B. 在观察者模式中,ConcreteSubject 必须知道每个 ConcreteObserver 的具体类型才能发送通知
C. 观察者模式定义对象间的一种一对多依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新
D. 观察者模式与中介者模式的核心区别在于:观察者模式用于多对多交互协调,中介者模式用于一对多状态通知
答案:C
解析:
-
A 错误:观察者模式属于 行为型 模式,不是结构型。简化复杂子系统接口的是 外观模式。
-
B 错误:ConcreteSubject 不需要知道 ConcreteObserver 的具体类型。它只面向 Observer 接口调用
update(),具体是哪个观察者、观察者做什么,Subject 完全不知道。这是解耦的核心。 -
C 正确:这是观察者模式的核心定义(GoF 原书定义)。
-
D 错误:说反了。观察者模式是 一对多状态通知 ,中介者模式是 多对多交互协调。
模拟题 2(下午代码填空 --- 补全 Subject 的订阅/通知逻辑)
某系统使用观察者模式实现温度监控系统。
TemperatureSensor是具体主题,TemperatureDisplay是具体观察者。请补全(1)~(4)。
java
// Observer
interface TemperatureObserver {
void update(double temperature);
}
// Subject
interface TemperatureSensor {
void (1)______(TemperatureObserver observer);
void (2)______(TemperatureObserver observer);
void notifyObservers();
}
// ConcreteSubject
class RoomSensor implements TemperatureSensor {
private List<TemperatureObserver> observers = new ArrayList<>();
private double temperature;
@Override
public void (1)______(TemperatureObserver observer) {
observers.add(observer);
}
@Override
public void (2)______(TemperatureObserver observer) {
observers.remove(observer);
}
public void setTemperature(double temperature) {
this.temperature = temperature;
System.out.println("【传感器】室温变化为: " + temperature + "°C");
(3)______; // 温度变化后通知所有观察者
}
@Override
public void notifyObservers() {
for (TemperatureObserver observer : observers) {
(4)______; // 通知每个观察者
}
}
}
// ConcreteObserver
class Display implements TemperatureObserver {
private String name;
public Display(String name) {
this.name = name;
}
@Override
public void update(double temperature) {
System.out.println("【显示器 " + name + "】显示温度: " + temperature + "°C");
}
}
public class Client {
public static void main(String[] args) {
RoomSensor sensor = new RoomSensor();
sensor.addObserver(new Display("客厅"));
sensor.addObserver(new Display("卧室"));
sensor.setTemperature(25.5);
sensor.setTemperature(26.0);
}
}
答案:
-
(1)
addObserver(或attach、register、subscribe) -
(2)
removeObserver(或detach、unregister、unsubscribe) -
(3)
notifyObservers() -
(4)
observer.update(temperature)
阅卷要点:
(1) 必须是
addObserver(或attach/register/subscribe)。这是 Subject 接口中注册观察者的标准方法名。软考中通常用attach(GoF 标准)或addObserver(Java 风格)。如果写add或registerObserver可能给分,但attach或addObserver是标准答案。(2) 必须是
removeObserver(或detach/unregister/unsubscribe)。这是注销观察者的标准方法名。如果写delete或remove可能给分,但detach或removeObserver是标准答案。(3) 必须是
notifyObservers()。Subject 状态变化后必须调用通知方法广播给所有观察者。如果写update()或notify()会 零分 (除非 Subject 接口中定义的是notify())。(4) 必须是
observer.update(temperature)。通知每个观察者时,调用 Observer 接口的update方法并传入当前状态(推模式)。如果写observer.notify()或observer.refresh()会 零分(除非 Observer 接口中定义的是这些方法)。
模拟题 3(下午代码填空 --- 补全拉模式 Observer)
某系统使用观察者模式(拉模式)实现订单状态通知。
OrderSubject是主题,OrderObserver是观察者。请补全(1)~(2)。
java
// Observer
interface OrderObserver {
void update(OrderSubject subject); // 拉模式:传入 Subject 引用
}
// Subject
interface OrderSubject {
void attach(OrderObserver observer);
void detach(OrderObserver observer);
void notifyObservers();
String getStatus();
String getOrderId();
}
// ConcreteSubject
class OnlineOrder implements OrderSubject {
private List<OrderObserver> observers = new ArrayList<>();
private String orderId;
private String status;
public OnlineOrder(String orderId) {
this.orderId = orderId;
this.status = "待支付";
}
public void setStatus(String status) {
this.status = status;
notifyObservers();
}
@Override
public String getStatus() { return status; }
@Override
public String getOrderId() { return orderId; }
@Override
public void attach(OrderObserver observer) { observers.add(observer); }
@Override
public void detach(OrderObserver observer) { observers.remove(observer); }
@Override
public void notifyObservers() {
for (OrderObserver observer : observers) {
observer.update(this);
}
}
}
// ConcreteObserver
class EmailNotifier implements OrderObserver {
@Override
public void update(OrderSubject subject) {
// 拉模式:从 Subject 中拉取需要的数据
String status = (1)______;
String orderId = (2)______;
System.out.println("【邮件通知】订单 " + orderId + " 状态更新为: " + status);
}
}
public class Client {
public static void main(String[] args) {
OnlineOrder order = new OnlineOrder("ORD20240714");
order.attach(new EmailNotifier());
order.setStatus("已支付");
order.setStatus("已发货");
}
}
答案:
-
(1)
subject.getStatus() -
(2)
subject.getOrderId()
阅卷要点:
(1) 必须是
subject.getStatus()。拉模式的核心就是 Observer 通过传入的 Subject 引用,自行调用 getter 方法拉取需要的数据。如果写subject.status(直接访问私有字段)或"已支付"(硬编码)会 零分。(2) 必须是
subject.getOrderId()。与 (1) 同理,Observer 通过 Subject 的 getter 方法获取订单 ID。如果写subject.orderId或"ORD20240714"会 零分。
十、常见陷阱与注意事项
陷阱 1:误认为观察者是结构型模式
观察者模式属于 行为型 模式,因为它关注的是 对象之间的行为交互 (状态变化时的通知机制),而不是类与类之间的结构关系。软考上午题如果问"以下属于结构型模式的是",选项里出现"观察者" 不能选。
陷阱 2:Subject 直接调用 Observer 的具体方法
这是软考 最高频的失分点。观察者模式的核心是面向接口编程:
- ❌ 错误:
java
class NewsPublisher {
private EmailSubscriber emailSub; // 直接持有具体类!
private SmsSubscriber smsSub;
public void publish(String news) {
emailSub.receiveEmail(news); // 调用具体方法
smsSub.sendSms(news); // 调用具体方法
}
}
- ✅ 正确:
java
class NewsPublisher {
private List<Observer> observers; // 面向接口,持有抽象观察者
public void notifyObservers() {
for (Observer o : observers) {
o.update(news); // 调用接口方法
}
}
}
Subject 必须只面向 Observer 接口编程,不能依赖任何 ConcreteObserver 的具体类型。如果 Subject 里出现了 EmailSubscriber 或 SmsSubscriber 类型的字段,就违背了观察者模式的设计意图。
陷阱 3:与中介者模式混淆
这是软考 最难的辨析题 之一。两者的核心区别:
| 维度 | 观察者模式 | 中介者模式 |
|---|---|---|
| 关系结构 | 一对多(Subject 广播给 Observer) | 多对多(Colleague 通过 Mediator 互相通信) |
| 通信方向 | 单向(Subject → Observer) | 双向(Colleague ↔ Mediator ↔ Colleague) |
| 触发方式 | Subject 状态变化 自动触发 | Colleague 主动请求 或事件触发 |
| Observer/Colleague 关系 | Observer 之间 无直接关联,互不感知 | Colleague 之间 不直接通信,但通过 Mediator 间接协调 |
| 核心方法 | attach()、detach()、notify()、update() |
sendMessage()、register()、转发逻辑 |
终极区分方法:看通信的触发者和方向。
-
如果是 一个对象状态变了,自动通知多个对象 → 观察者。
-
如果是 多个对象之间需要互相通信、协调 → 中介者。
陷阱 4:循环通知导致死循环
如果 Observer 在 update() 中修改了 Subject 的状态,而 Subject 的 setter 中又调用了 notify(),可能导致无限循环:
- ❌ 错误:
java
class BadObserver implements Observer {
public void update(Subject sub) {
sub.setState("新状态"); // 又触发了 Subject 的 setter,导致再次 notify!
}
}
- ✅ 正确:
java
class GoodObserver implements Observer {
public void update(Subject sub) {
// 只读取 Subject 状态,不修改 Subject 状态
System.out.println("收到通知: " + sub.getState());
}
}
软考中,Observer 的 update() 方法应该 只读取不修改 Subject 的状态,避免循环通知。
陷阱 5:推模式 vs 拉模式混淆
| 模式 | 特点 | 适用场景 | 软考代码特征 |
|---|---|---|---|
| 推模式(Push) | Subject 把 具体数据 通过 update(data) 推给 Observer |
数据简单、固定 | observer.update(data) |
| 拉模式(Pull) | Subject 只传 自身引用 update(this),Observer 自行 getter 拉取 |
数据复杂、Observer 按需获取 | observer.update(this) |
软考中如果题目说 "Observer 需要根据不同情况获取不同数据" → 应该用 拉模式 。如果题目说 "Subject 状态简单,直接通知即可" → 应该用 推模式。
陷阱 6:忘记 detach 导致内存泄漏
如果 Observer 被销毁或不再需要接收通知,但没有调用 detach() 从 Subject 的列表中移除,Subject 会一直持有 Observer 的引用,导致 Observer 无法被垃圾回收,造成 内存泄漏。
- ❌ 错误:
java
public static void main(String[] args) {
Subject subject = new Subject();
Observer observer = new Observer();
subject.attach(observer);
// observer 不再使用,但没有 detach!
observer = null; // 实际上 subject 还持有引用,无法回收
}
- ✅ 正确:
java
public static void main(String[] args) {
Subject subject = new Subject();
Observer observer = new Observer();
subject.attach(observer);
// 使用完毕,显式注销
subject.detach(observer);
}
软考中,如果题目问"观察者模式的注意事项",选项中出现 "不需要的观察者应及时注销,避免内存泄漏" → 这是正确答案。
陷阱 7:notify 时修改观察者列表
如果在 notify() 遍历过程中,有 Observer 调用 detach() 或 attach() 修改了观察者列表,会导致 ConcurrentModificationException。
- ❌ 错误:
java
public void notifyObservers() {
for (Observer o : observers) { // 遍历时修改列表!
o.update(this);
// 某个 Observer 的 update 内部调用了 subject.detach(this)
}
}
- ✅ 正确:
java
public void notifyObservers() {
// 创建快照副本,避免遍历时修改原列表
List<Observer> snapshot = new ArrayList<>(observers);
for (Observer o : snapshot) {
o.update(this);
}
}
软考中,如果给出了 notify() 的代码框架,最佳实践是 先复制列表再遍历。
十一、总结
| 要点 | 内容 |
|---|---|
| 定义 | 定义对象间的一种一对多依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新 |
| 分类 | 行为型模式(GoF 23 正式成员) |
| 核心四角色 | Subject(抽象主题)、ConcreteSubject(具体主题)、Observer(抽象观察者)、ConcreteObserver(具体观察者) |
| 核心思想 | 将 Subject 与 Observer 解耦,Subject 状态变化时自动广播通知所有 Observer |
| 一对多关系 | Subject 持有 List<Observer>,一对多聚合关联 |
| 订阅机制 | attach()/subscribe() 注册,detach()/unsubscribe() 注销 |
| 自动通知 | Subject 状态 setter 方法内自动调用 notifyObservers() |
| 推模式 | update(data):Subject 把数据直接推送给 Observer |
| 拉模式 | update(this):Subject 传自身引用,Observer 自行 getter 拉取数据 |
| 与中介者模式区别 | 观察者:一对多广播,单向通知;中介者:多对多协调,双向转发 |
| 与迭代器模式区别 | 观察者:状态驱动通知;迭代器:客户端驱动遍历 |
| Java 内置 | java.util.Observable/Observer(已弃用);推荐使用 PropertyChangeSupport |
| 适用场景 | ① 一个对象状态变化需要通知多个对象 ② 观察者数量动态变化 ③ 需要解耦 Subject 与 Observer |
| 注意事项 | 避免循环通知;及时 detach 防止内存泄漏;notify 时复制列表防止并发修改 |
| 软考重点 | 代码填空(补全 attach/detach/notify + update);推/拉模式识别;与中介者模式的辨析 |
| 答题技巧 | 看到 "一对多"、"状态变化自动通知"、"订阅发布"、"MVC 模型通知视图" → 观察者;看到 Subject 聚合 Observer + notify() 遍历调用 update() → 确认观察者 |
系列预告 :下一篇将讲 责任链模式 ------ 当多个对象都有机会处理同一个请求,且请求的发送者和接收者需要解耦时,如何将请求沿着一条链传递,直到有对象处理它为止。责任链模式与观察者模式、装饰模式的辨析是软考高频考点,咱们下回重点拆解。咱们下回见。