系列定位:软考软件设计师 / 系统架构设计师 — 行为型模式专题第 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() 通知所有注册的 Observerimplements 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 代码实现
// 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 代码实现
// 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 代码实现
// 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 模型视图
SubjectNewsPublisher(新闻发布者)Stock(股票)StudentModel(学生模型)
ObserverSubscriber(订阅者)StockObserver(股票观察者)DataView(数据视图)
ConcreteObserverEmailSubscriber、SmsSubscriber、AppSubscriberPriceDisplay、PriceAlert、VolumeTrackerListView、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 关联 MediatorAggregate 关联 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)。

// 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)。

// 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 的具体方法

这是软考 最高频的失分点。观察者模式的核心是面向接口编程:

  • ❌ 错误:

class NewsPublisher {
    private EmailSubscriber emailSub;  // 直接持有具体类!
    private SmsSubscriber smsSub;

    public void publish(String news) {
        emailSub.receiveEmail(news);  // 调用具体方法
        smsSub.sendSms(news);         // 调用具体方法
    }
}
  • ✅ 正确:

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(),可能导致无限循环:

  • ❌ 错误:

class BadObserver implements Observer {
    public void update(Subject sub) {
        sub.setState("新状态");  // 又触发了 Subject 的 setter,导致再次 notify!
    }
}
  • ✅ 正确:

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 无法被垃圾回收,造成 内存泄漏。

  • ❌ 错误:

public static void main(String[] args) {
    Subject subject = new Subject();
    Observer observer = new Observer();
    subject.attach(observer);
    // observer 不再使用,但没有 detach!
    observer = null;  // 实际上 subject 还持有引用,无法回收
}
  • ✅ 正确:

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。

  • ❌ 错误:

public void notifyObservers() {
    for (Observer o : observers) {  // 遍历时修改列表!
        o.update(this);
        // 某个 Observer 的 update 内部调用了 subject.detach(this)
    }
}
  • ✅ 正确:

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

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

更多推荐