【软考设计模式】观察者模式:一对多依赖的自动通知与订阅解耦精讲

系列定位:软考软件设计师 / 系统架构设计师 --- 行为型模式专题第 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 的引用并逐个调用,会导致:

  1. Subject 与 Observer 紧耦合,新增 Observer 需要修改 Subject

  2. Subject 职责过重(既要管理自身状态,又要管理通知列表)

  3. 无法动态增删 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 EmailSubscriberSmsSubscriberAppSubscriber PriceDisplayPriceAlertVolumeTracker ListViewChartViewDetailView
通知模式 推模式 (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.Observablejava.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(或 attachregistersubscribe

  • (2) removeObserver(或 detachunregisterunsubscribe

  • (3) notifyObservers()

  • (4) observer.update(temperature)

阅卷要点

  • (1) 必须是 addObserver(或 attach/register/subscribe)。这是 Subject 接口中注册观察者的标准方法名。软考中通常用 attach(GoF 标准)或 addObserver(Java 风格)。如果写 addregisterObserver 可能给分,但 attachaddObserver 是标准答案。

  • (2) 必须是 removeObserver(或 detach/unregister/unsubscribe)。这是注销观察者的标准方法名。如果写 deleteremove 可能给分,但 detachremoveObserver 是标准答案。

  • (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 里出现了 EmailSubscriberSmsSubscriber 类型的字段,就违背了观察者模式的设计意图。

陷阱 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() → 确认观察者

系列预告 :下一篇将讲 责任链模式 ------ 当多个对象都有机会处理同一个请求,且请求的发送者和接收者需要解耦时,如何将请求沿着一条链传递,直到有对象处理它为止。责任链模式与观察者模式、装饰模式的辨析是软考高频考点,咱们下回重点拆解。咱们下回见。

相关推荐
ttod_qzstudio3 小时前
【软考设计模式】命令模式:请求封装与调用接收解耦的精讲
设计模式·命令模式
春卷同学6 小时前
HarmonyOS掌上记账APP开发实践第3篇:接口与实现分离 — MoneyTrack Types.ets 与 Models.ets 的双层设计模式
设计模式
NG4771 天前
【软件设计与体系结构】-策略设计模式
java·设计模式·软件工程
CaffeinePro1 天前
SOLID五大设计原则:重构优雅代码与架构的底层规范
设计模式·架构
饮茶三千1 天前
电子保函模板编辑器实战二:HTML→FTL 编译转换的设计与实现
前端·vue.js·设计模式
Kel1 天前
Node.js 本质:从全脉络视角理解运行时原理
javascript·设计模式·node.js
码哥字节1 天前
11 万 Star 的 Spec Kit,90% 的工程师只用它解决了 10% 的问题
设计模式·vibe coding·github spec kit
我登哥MVP1 天前
走进 Gang of Four 设计模式:访问者模式
java·设计模式·访问者模式·原型模式
ttod_qzstudio1 天前
【软考设计模式】状态模式:状态驱动行为自动转换与代码填空精讲
设计模式·状态模式