通俗易懂的故事解说观察者模式

4 阅读9分钟

通俗易懂的故事解说观察者模式

想象一下,你是一位报社的老板,你的报社每天都会出版最新的报纸。为了让报纸能够卖出去,你需要让更多的人知道报纸的更新。于是,你想出了一个办法:让读者订阅你的报纸

  • 读者订阅:读者来到报社,填写订阅表格,留下自己的联系方式(比如邮箱地址)。这样,你就知道哪些读者想要收到你的报纸。
  • 报纸出版:每当你的报社出版了新的报纸,你不需要挨家挨户去送报纸,而是只需要给所有订阅了报纸的读者群发一封邮件,邮件里附带报纸的电子版或者告知他们报纸已经出版,可以前来购买。
  • 读者阅读:订阅了报纸的读者收到邮件后,就可以下载电子版报纸阅读,或者直接去报摊购买纸质版报纸。

在这个故事中:

  • 报社就相当于被观察者(Subject) ,也叫做主题。它负责维护订阅者列表,并在状态发生变化时通知订阅者。
  • 读者就相当于观察者(Observer) 。他们订阅了报社(被观察者),并等待接收报社的通知。
  • 订阅报纸的行为就相当于注册观察者。读者通过订阅,将自己的联系方式注册到报社的观察者列表中。
  • 报纸出版并通知读者的行为就相当于被观察者状态改变,并通知所有观察者
  • 读者收到邮件并阅读报纸就相当于观察者接收到通知,并执行相应的操作(比如下载报纸、购买报纸)。

观察者模式就像报社和订阅者之间的关系,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象(被观察者)。当主题对象的状态发生变化时,它会通知所有的观察者对象,使它们能够自动更新自己的状态。

Java实现几种常用的观察者模式

下面,我用Java代码来实现几种常用的观察者模式,并讲解它们的优缺点。

1. 手动实现观察者模式(推模型 - Push Model)

场景:报社(被观察者)在报纸出版时,不仅通知订阅者(观察者)报纸出版了,还主动推送报纸的标题和摘要给订阅者。

java
	import java.util.ArrayList;

	import java.util.List;

	 

	// 1. 定义观察者接口

	interface Observer {

	    void update(String title, String summary);

	}

	 

	// 2. 定义被观察者接口

	interface Subject {

	    void registerObserver(Observer observer);

	    void removeObserver(Observer observer);

	    void notifyObservers(String title, String summary);

	}

	 

	// 3. 实现被观察者(报社)

	class NewspaperPublisher implements Subject {

	    private List<Observer> observers;

	    private String title;

	    private String summary;

	 

	    public NewspaperPublisher() {

	        observers = new ArrayList<>();

	    }

	 

	    @Override

	    public void registerObserver(Observer observer) {

	        observers.add(observer);

	    }

	 

	    @Override

	    public void removeObserver(Observer observer) {

	        observers.remove(observer);

	    }

	 

	    @Override

	    public void notifyObservers(String title, String summary) {

	        this.title = title;

	        this.summary = summary;

	        for (Observer observer : observers) {

	            observer.update(title, summary); // 推送标题和摘要给观察者

	        }

	    }

	 

	    // 报社出版报纸的方法

	    public void publishNewspaper(String title, String summary) {

	        System.out.println("报社正在出版报纸: " + title);

	        notifyObservers(title, summary); // 通知所有订阅者

	    }

	}

	 

	// 4. 实现观察者(读者)

	class Reader implements Observer {

	    private String name;

	 

	    public Reader(String name) {

	        this.name = name;

	    }

	 

	    @Override

	    public void update(String title, String summary) {

	        System.out.println(name + " 收到报纸更新通知: 标题 - "" + title + "", 摘要 - "" + summary + """);

	        // 读者可以根据标题和摘要决定是否下载或购买报纸

	    }

	}

	 

	// 5. 客户端代码

	public class ObserverPatternDemo {

	    public static void main(String[] args) {

	        NewspaperPublisher publisher = new NewspaperPublisher();

	 

	        Observer reader1 = new Reader("读者A");

	        Observer reader2 = new Reader("读者B");

	 

	        publisher.registerObserver(reader1);

	        publisher.registerObserver(reader2);

	 

	        publisher.publishNewspaper("今日要闻", "这是今天的头条新闻摘要...");

	 

	        publisher.removeObserver(reader1);

	 

	        publisher.publishNewspaper("明日预告", "这是明天的报纸内容预告...");

	    }

	}

优点

  • 解耦:被观察者和观察者之间完全解耦,它们之间只通过观察者接口进行通信。被观察者不需要知道观察者的具体实现细节,观察者也不需要知道被观察者的内部状态。
  • 灵活:可以动态地注册和移除观察者,实现灵活的通知机制。
  • 可扩展:可以方便地添加新的观察者,而无需修改被观察者的代码,符合开闭原则。
  • 推模型:被观察者可以主动推送数据给观察者,观察者可以直接使用这些数据,而不需要自己再去获取。

缺点

  • 推模型:如果被观察者推送的数据量很大,或者观察者不需要所有数据,可能会造成网络带宽和资源的浪费。
  • 性能:如果观察者数量很多,通知所有观察者可能会影响性能。

2. 手动实现观察者模式(拉模型 - Pull Model)

场景:报社(被观察者)在报纸出版时,只通知订阅者(观察者)报纸出版了,但不主动推送报纸内容。订阅者收到通知后,主动去报社获取报纸的标题和摘要。

java
	import java.util.ArrayList;

	import java.util.List;

	 

	// 1. 定义观察者接口(与推模型相同)

	interface Observer {

	    void update(); // 不再接收推送的数据

	}

	 

	// 2. 定义被观察者接口(与推模型相同)

	interface Subject {

	    void registerObserver(Observer observer);

	    void removeObserver(Observer observer);

	    void notifyObservers(); // 不再推送数据

	}

	 

	// 3. 实现被观察者(报社)

	class NewspaperPublisher implements Subject {

	    private List<Observer> observers;

	    private String title;

	    private String summary;

	 

	    public NewspaperPublisher() {

	        observers = new ArrayList<>();

	    }

	 

	    @Override

	    public void registerObserver(Observer observer) {

	        observers.add(observer);

	    }

	 

	    @Override

	    public void removeObserver(Observer observer) {

	        observers.remove(observer);

	    }

	 

	    @Override

	    public void notifyObservers() {

	        for (Observer observer : observers) {

	            observer.update(); // 仅通知观察者,不推送数据

	        }

	    }

	 

	    // 报社出版报纸的方法

	    public void publishNewspaper(String title, String summary) {

	        System.out.println("报社正在出版报纸: " + title);

	        this.title = title;

	        this.summary = summary;

	        notifyObservers(); // 通知所有订阅者

	    }

	 

	    // 提供给观察者获取报纸信息的方法

	    public String getTitle() {

	        return title;

	    }

	 

	    public String getSummary() {

	        return summary;

	    }

	}

	 

	// 4. 实现观察者(读者)

	class Reader implements Observer {

	    private String name;

	    private NewspaperPublisher publisher;

	 

	    public Reader(String name, NewspaperPublisher publisher) {

	        this.name = name;

	        this.publisher = publisher;

	    }

	 

	    @Override

	    public void update() {

	        // 主动去报社获取报纸信息

	        String title = publisher.getTitle();

	        String summary = publisher.getSummary();

	        System.out.println(name + " 收到报纸更新通知,并主动获取报纸: 标题 - "" + title + "", 摘要 - "" + summary + """);

	        // 读者可以根据标题和摘要决定是否下载或购买报纸

	    }

	}

	 

	// 5. 客户端代码(与推模型类似,只是创建Reader时需要传入NewspaperPublisher实例)

	public class ObserverPatternDemo {

	    public static void main(String[] args) {

	        NewspaperPublisher publisher = new NewspaperPublisher();

	 

	        Observer reader1 = new Reader("读者A", publisher);

	        Observer reader2 = new Reader("读者B", publisher);

	 

	        publisher.registerObserver(reader1);

	        publisher.registerObserver(reader2);

	 

	        publisher.publishNewspaper("今日要闻", "这是今天的头条新闻摘要...");

	 

	        publisher.removeObserver(reader1);

	 

	        publisher.publishNewspaper("明日预告", "这是明天的报纸内容预告...");

	    }

	}

优点

  • 解耦、灵活、可扩展:与推模型相同。
  • 拉模型:观察者可以根据需要主动获取数据,避免了推模型中可能存在的数据浪费问题。
  • 被观察者无需知道观察者需要哪些数据:被观察者只需要通知观察者状态发生了变化,而不需要关心观察者需要哪些具体的数据。

缺点

  • 拉模型:观察者需要主动去获取数据,可能增加观察者的复杂度。
  • 性能:如果观察者获取数据的操作比较耗时,可能会影响性能。
  • 数据一致性:如果被观察者的状态在观察者获取数据的过程中发生了变化,观察者获取到的数据可能不是最新的。

3. 使用Java内置的观察者模式(已过时,仅作了解)

Java曾经在java.util包中提供了Observable类和Observer接口,用于实现观察者模式。但是,从Java 9开始,这些类和接口被标记为过时(Deprecated),不推荐在新代码中使用。这里仅作了解:

java
	import java.util.Observable;

	import java.util.Observer;

	 

	// 1. 被观察者(继承自Observable)

	class NewspaperPublisher extends Observable {

	    private String title;

	    private String summary;

	 

	    public void publishNewspaper(String title, String summary) {

	        System.out.println("报社正在出版报纸: " + title);

	        this.title = title;

	        this.summary = summary;

	        setChanged(); // 标记状态已改变

	        notifyObservers(); // 通知所有观察者(拉模型)

	        // notifyObservers(new Object()); // 通知所有观察者(推模型,可以推送一个对象)

	    }

	 

	    // 提供给观察者获取报纸信息的方法(拉模型)

	    public String getTitle() {

	        return title;

	    }

	 

	    public String getSummary() {

	        return summary;

	    }

	}

	 

	// 2. 观察者(实现Observer接口)

	class Reader implements Observer {

	    private String name;

	 

	    public Reader(String name) {

	        this.name = name;

	    }

	 

	    @Override

	    public void update(Observable o, Object arg) {

	        NewspaperPublisher publisher = (NewspaperPublisher) o;

	        // 拉模型:主动获取数据

	        String title = publisher.getTitle();

	        String summary = publisher.getSummary();

	        System.out.println(name + " 收到报纸更新通知,并主动获取报纸: 标题 - "" + title + "", 摘要 - "" + summary + """);

	 

	        // 推模型:如果使用 notifyObservers(new Object()),可以通过 arg 参数获取推送的数据

	        // if (arg != null) {

	        //     // 处理推送的数据

	        // }

	    }

	}

	 

	// 3. 客户端代码

	public class ObserverPatternDemo {

	    public static void main(String[] args) {

	        NewspaperPublisher publisher = new NewspaperPublisher();

	 

	        Observer reader1 = new Reader("读者A");

	        Observer reader2 = new Reader("读者B");

	 

	        publisher.addObserver(reader1);

	        publisher.addObserver(reader2);

	 

	        publisher.publishNewspaper("今日要闻", "这是今天的头条新闻摘要...");

	 

	        publisher.deleteObserver(reader1);

	 

	        publisher.publishNewspaper("明日预告", "这是明天的报纸内容预告...");

	    }

	}

优点

  • 简单易用:Java内置了观察者模式的实现,使用起来非常方便。

缺点

  • 已过时:从Java 9开始,ObservableObserver被标记为过时,不推荐在新代码中使用。
  • 功能有限:Java内置的观察者模式功能相对简单,可能无法满足复杂场景的需求。
  • 推模型和拉模型混合:Java内置的观察者模式同时支持推模型和拉模型,但使用起来可能不够清晰。

总结

观察者模式实现方式推模型/拉模型线程安全实现难度优点缺点
手动实现(推模型)推模型简单解耦、灵活、可扩展、推模型(主动推送数据)推模型(可能造成数据浪费)、性能(观察者数量多时)
手动实现(拉模型)拉模型简单解耦、灵活、可扩展、拉模型(观察者主动获取数据,避免数据浪费)拉模型(观察者需要主动获取数据,可能增加复杂度)、性能、数据一致性
Java内置(已过时)推/拉混合简单简单易用已过时、功能有限、推模型和拉模型混合

在实际开发中,可以根据具体的需求和场景,选择最合适的观察者模式实现方式。如果对线程安全有较高要求,或者需要更强大的功能,可以考虑使用第三方库(如Guava的EventBus)或框架(如Spring的事件机制)来实现观察者模式。如果只是需要简单的观察者模式,手动实现也是一个不错的选择。