设计模式-观察者模式 Observer

观察者模式

一、概述

观察者模式是一种行为设计模式,允许对象间存在一对多的依赖关系 ,当一个对象的状态发生改变时,所有依赖它的对象都会得到通知并自动更新 。在这种模式中,发生状态改变的对象被称为"主题"(Subject),依赖它的对象被称为"观察者"(Observer)。

观察者模式 (Observer Design Pattern)也被称为发布订阅模式(Publish-Subscribe Design Pattern)。在 GoF 的《设计模式》一书中,它的定义是这样的:

Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

翻译成中文就是:在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。

一般情况下,被依赖的对象叫作被观察者 (Observable),依赖的对象叫作观察者(Observer)。不过,在实际的项目开发中,这两种对象的称呼是比较灵活的,有各种不同的叫法,比如:Subject-Observer、Publisher-Subscriber、Producer-Consumer等等。不管怎么称呼,只要应用场景符合刚刚给出的定义,都可以看作观察者模式。

让通过一个简单的例子来实现观察者模式。假设有一个气象站(WeatherStation),需要向许多不同的显示设备(如手机App、网站、电子屏幕等)提供实时天气数据。

首先,需要创建一个Subject接口,表示主题

java 复制代码
public interface Subject {
    /**
     * 注册观察者
     * @param observer
     */
    void registerObserver(Observer observer);

    /**
     * 删除具体的观察者
     * @param observer
     */
    void removeObserver(Observer observer);

    /**
     * 一旦发生了观察的行为,就通知所有的观察者
     */
    void notifyObservers();
}

接下来,创建一个Observer接口,表示观察者

java 复制代码
public interface Observer {
    /**
     * 观察的行为发生了,该方法应该被调用
     * @param newTemperature 更新的温度
     */
    void update(double newTemperature);
}

现在,创建一个具体的主题 ,如WeatherStation,实现Subject接口:

java 复制代码
public class WeatherStation implements Subject{
    // 温度
    private double temperature;

    // 持有多个观察者
    private final List<Observer> observerList = new ArrayList<>();

    public void changeTemperature(double newTemperature) {
        this.temperature = newTemperature;
        notifyObservers(newTemperature);
    }

    @Override
    public void registerObserver(Observer observer) {
        observerList.add(observer);
    }

    @Override
    public void removeObserver(Observer observer) {
        observerList.remove(observer);
    }

    @Override
    public void notifyObservers(double newTemperature) {
        for (Observer observer : observerList) {
            observer.update(newTemperature);
        }
    }
}

最后,创建一个具体的观察者,如 AppClient/WebClient,实现Observer接口:

java 复制代码
public class AppClient implements Observer{
    @Override
    public void update(double newTemperature) {
        System.out.println("App获取最新温度:" + newTemperature);
    }
}
java 复制代码
public class WebClient implements Observer{
    @Override
    public void update(double newTemperature) {
        System.out.println("Web获取最新温度:" + newTemperature);
    }
}

现在可以创建一个WeatherStation实例并向其注册AppClient观察者。当WeatherStation的数据发生变化时,AppClient会收到通知并更新自己的显示。

java 复制代码
public class Main {
    public static void main(String[] args) {
        // 定义气象站
        Subject weatherStation = new WeatherStation();

        // 定义观察者
        Observer appClient = new AppClient();
        Observer webClient = new WebClient();

        // 建立监听关系
        weatherStation.registerObserver(appClient);
        weatherStation.registerObserver(webClient);

        // 气象站更新温度
        weatherStation.notifyObservers(25.4);
    }
}

在这个例子中,创建了一个WeatherStation实例,并向其注册了AppClient、WebClient观察者。当WeatherStation的数据发生变化时,所有观察者都会收到通知并更新自己的显示。 这个例子展示了观察者模式的优点:

  1. 观察者和主题之间的解耦:主题只需要知道观察者实现了Observer接口,而无需了解具体的实现细节。
  2. 可以动态添加和删除观察者:通过调用registerObserver和removeObserver方法,可以在运行时添加和删除观察者。
  3. 主题和观察者之间的通信是自动的:当主题的状态发生变化时,观察者会自动得到通知并更新自己的状态。 观察者模式广泛应用于各种场景,例如事件处理系统、数据同步和更新通知等。学习并掌握观察者模式对于成为一个优秀的Java程序员非常有帮助。

上面的小例子算是观察者模式的"模板代码",可以反映该模式大体的设计思路。在真实的软件开发中,并不需要照搬上面的模板代码。观察者模式的实现方法各式各样,函数、类的命名等会根据业务场景的不同有很大的差别,比如 register 函数还可以叫作 attach,remove 函数还可以叫作 detach 等等。不过,万变不离其宗,设计思路都是差不多的。

二、使用场景

以下是一些使用观察者设计模式的例子:

  1. 股票行情应用:股票行情应用中,当股票价格发生变化时,需要通知订阅了该股票的投资者。这里,股票价格更新可以作为被观察者,投资者可以作为观察者。当股票价格发生变化时,所有订阅了该股票的投资者都会收到通知并更新自己的投资策略。
  2. 网络聊天室:在网络聊天室中,当有新消息时,需要通知所有在线的用户。聊天室服务器可以作为被观察者,用户可以作为观察者。当有新消息时,聊天室服务器会通知所有在线用户更新聊天记录。
  3. 拍卖系统:在拍卖系统中,当出价发生变化时,需要通知所有关注该拍品的用户。这里,拍卖系统可以作为被观察者,用户可以作为观察者。当出价发生变化时,所有关注该拍品的用户都会收到通知并更新自己的出价策略。
  4. 订阅系统:在订阅系统中,当有新的内容发布时,需要通知所有订阅了该内容的用户。这里,内容发布可以作为被观察者,用户可以作为观察者。当有新内容发布时,所有订阅了该内容的用户都会收到通知并获取最新内容。
  5. 游戏中的事件系统:在游戏中,当某个事件发生时(如角色升级、道具获得等),可能需要通知多个游戏模块进行相应的处理。这里,游戏事件可以作为被观察者,游戏模块可以作为观察者。当游戏事件发生时,所有关注该事件的游戏模块都会收到通知并执行相应的逻辑。
  6. 运动比赛实时更新:在体育比赛中,实时更新比分、技术统计等信息对于球迷和分析师非常重要。在这种场景下,比赛数据更新可以作为被观察者,球迷和分析师可以作为观察者。当比赛数据发生变化时,所有关注比赛的球迷和分析师都会收到通知并更新数据。
  7. 物联网传感器系统:在物联网(IoT)系统中,有很多传感器不断地采集数据,当数据发生变化时,需要通知相关联的设备或系统。在这种场景下,传感器可以作为被观察者,关联的设备或系统可以作为观察者。当传感器数据发生变化时,所有关联的设备或系统都会收到通知并执行相应的操作。
  8. 电子邮件通知系统:在一个任务管理系统中,当任务的状态发生变化(如:新任务分配、任务完成等)时,需要通知相关的人员。这里,任务状态更新可以作为被观察者,相关人员可以作为观察者。当任务状态发生变化时,所有关注该任务的人员都会收到通知并查看任务详情。
  9. 社交网络:在社交网络中,用户关注其他用户以获取实时动态。当被关注的用户发布新动态时,需要通知所有关注者。在这种场景下,被关注的用户可以作为被观察者,关注者可以作为观察者。当被关注的用户发布新动态时,所有关注者都会收到通知并查看动态。

三、发布订阅

发布-订阅模式观察者模式 都是用于实现对象间的松耦合通信的设计模式。尽管它们具有相似之处,但它们在实现方式和使用场景上存在一些关键区别。他们在概念上有一定的相似性,都是用于实现对象间的松耦合通信 。可以将发布-订阅模式看作是观察者模式的一种变体或扩展。

我分别解释一下这两种模式。

1) 观察者模式

观察者模式定义了一种一对多的依赖关系,当一个对象(被观察者)的状态发生变化时,所有依赖于它的对象(观察者)都会得到通知并自动更新。在这个模式中,被观察者和观察者之间存在直接的关联关系。观察者模式主要包括两类对象:被观察者(Subject)和观察者(Observer)

2) 发布-订阅模式

发布-订阅模式(生产者和消费者)与观察者模式类似,但它们之间有一个关键区别:发布-订阅模式引入了一个第三方组件(通常称为消息代理或事件总线) ,该组件负责维护发布者和订阅者之间的关系。这意味着发布者和订阅者彼此不直接通信 ,而是通过消息代理进行通信。这种间接通信允许发布者和订阅者在运行时动态地添加或删除,从而提高了系统的灵活性和可扩展性。

Java中的发布-订阅模式示例:

java 复制代码
public interface Subscriber {
    void onEvent(Map<String, Object> eventContextMap);
}

public class AppSubscriber implements Subscriber{
    @Override
    public void onEvent(Map<String, Object> eventContextMap) {
        System.out.println("app -> 当前的温度是: " + eventContextMap.get("temp"));
    }
}

public class WebSubscriber implements Subscriber{
    @Override
    public void onEvent(Map<String, Object> eventContextMap) {
        System.out.println("web -> 当前的温度是: " + eventContextMap.get("temp"));
    }
}

// 创建消息总线
public class EventBus {

    // 维护事件(对象,字符串)和订阅者的关系
    private final Map<String, List<Subscriber>> subscriberMap = new HashMap<>(8);

    public void registerSubscriber(String eventType, Subscriber subscriber) {
        // 通过事件类型,来确定有没有已存在订阅者
        subscriberMap.computeIfAbsent(eventType, v -> new ArrayList<>());
        // 获取订阅者的集合
        List<Subscriber> subscriberList = subscriberMap.get(eventType);
        subscriberList.add(subscriber);
        // 注册
        subscriberMap.put(eventType, subscriberList);
    }

    public void removeSubscriber(String eventType, Subscriber subscriber) {
        List<Subscriber> subscriberList = subscriberMap.get(eventType);
        if (subscriberList != null) {
            subscriberList.remove(subscriber);
        }
    }

    public void publishEvent(String eventType, Map<String, Object> eventContextMap) {
        List<Subscriber> subscriberList = subscriberMap.get(eventType);
        for (Subscriber subscriber : subscriberList) {
            subscriber.onEvent(eventContextMap);
        }
    }
}

// 使用示例:
public class WeatherStation {
    private double temperature;
    private EventBus eventBus;

    public WeatherStation(EventBus eventBus) {
        this.eventBus = eventBus;
    }

    public void changeTemperature(double newTemperature) {
        this.temperature = newTemperature;
        Map<String, Object> eventContextMap = new HashMap<>(2);
        eventContextMap.put("temp", newTemperature);
        eventBus.publishEvent("changeTemperature", eventContextMap);
    }

    public static void main(String[] args) {
        // 创建订阅者
        AppSubscriber appSubscriber = new AppSubscriber();
        WebSubscriber webSubscriber = new WebSubscriber();
        // 构建消息总线
        EventBus eventBus = new EventBus();
        eventBus.registerSubscriber("changeTemperature", appSubscriber);
        eventBus.registerSubscriber("changeTemperature", webSubscriber);
        // 创建气象站
        WeatherStation weatherStation = new WeatherStation(eventBus);
        weatherStation.changeTemperature(25.6);
    }
}

总结一下两者的区别:

  1. 通信方式:观察者模式中,观察者与被观察者之间存在直接的关联关系,而发布-订阅模式中,发布者和订阅者通过一个第三方组件(消息代理或事件总线)进行通信,彼此之间不存在直接关联关系。
  2. 系统复杂性:发布-订阅模式引入了一个额外的组件(消息代理或事件总线),增加了系统的复杂性,但同时也提高了系统的灵活性和可扩展性。
  3. 使用场景:观察者模式适用于需要将状态变化通知给其他对象的情况,而发布-订阅模式适用于事件驱动的系统,尤其是那些需要跨越多个模块或组件进行通信的场景。 希望这个解释能帮助您理解发布-订阅模式和观察者模式之间的区别。如果您有其他问题,欢迎继续提问。

发布-订阅模式和传统的观察者模式相比,在某些方面具有优势。以下是发布-订阅模式相对于观察者模式的一些优点:

  1. 解耦:在发布-订阅模式中,发布者和订阅者之间没有直接关联,它们通过一个中间组件(消息代理或事件总线)进行通信。这种间接通信可以使发布者和订阅者在运行时动态地添加或删除,从而进一步降低了它们之间的耦合度。
  2. 可扩展性:发布-订阅模式允许您更容易地向系统中添加新的发布者和订阅者,而无需修改现有的代码。这使得系统在不同组件之间通信时具有更好的可扩展性。
  3. 模块化:由于发布者和订阅者之间的通信通过中间组件进行,您可以将系统划分为更小、更独立的模块。这有助于提高代码的可维护性和可读性。
  4. 异步通信:发布-订阅模式通常支持异步消息传递,这意味着发布者和订阅者可以在不同的线程或进程中运行。这有助于提高系统的并发性能和响应能力。
  5. 消息过滤:在发布-订阅模式中,可以利用中间组件对消息进行过滤,使得订阅者只接收到感兴趣的消息。这可以提高系统的性能,减少不必要的通信开销。

然而,发布-订阅模式也有一些缺点,例如增加了系统的复杂性,因为引入了额外的中间组件。根据具体的应用场景和需求来选择合适的设计模式是很重要的。在某些情况下,观察者模式可能更适合,而在其他情况下,发布-订阅模式可能是更好的选择。

四、源码使用

1) jdk中的观察者

java.util.Observable类实现了主题(Subject)的功能,而java.util.Observer接口则定义了观察者(Observer)的方法。

通过调用Observable对象的notifyObservers()方法,可以通知所有注册的Observer对象,让它们更新自己的状态。

一下是一个使用案例:假设有一个银行账户类,它的余额是可变的。当余额发生变化时,需要通知所有的观察者(比如说银行客户),以便它们更新自己的显示信息。

java 复制代码
// 银行账户类
public class BankAccount extends Observable {
    private double balance;

    // 构造函数
    public BankAccount(double balance) {
        this.balance = balance;
    }

    // 存款操作
    public void deposit(double amount) {
        balance += amount;
        setChanged(); // 表示状态已经改变
        notifyObservers(); // 通知所有观察者
    }

    // 取款操作
    public void withdraw(double amount) {
        balance -= amount;
        setChanged(); // 表示状态已经改变
        notifyObservers(); // 通知所有观察者
    }

    // 获取当前余额
    public double getBalance() {
        return balance;
    }

    // 主函数
    public static void main(String[] args) {
        BankAccount account = new BankAccount(1000.0);
        // 创建观察者
        Observer observer1 = new Observer() {
            @Override
            public void update(Observable o, Object arg) {
                System.out.println("客户1: 余额已更新为 " + ((BankAccount)o).getBalance());
            }
        };
        Observer observer2 = new Observer() {
            @Override
            public void update(Observable o, Object arg) {
                System.out.println("客户2: 余额已更新为 " + ((BankAccount)o).getBalance());
            }
        };
        // 注册观察者
        account.addObserver(observer1);
        account.addObserver(observer2);
        // 存款操作,触发观察者更新
        account.deposit(100.0);
        // 取款操作,触发观察者更新
        account.withdraw(50.0);
    }
}

这个案例中,BankAccount类继承了java.util.Observable类,表示它是一个主题(Subject)。在存款或取款操作时,它会调用setChanged()方法表示状态已经改变,并调用notifyObservers()方法通知所有观察者(Observer)。

在主函数中,创建了两个观察者(observer1和observer2),它们分别实现了Observer接口的update()方法。当观察者收到更新通知时,它们会执行自己的业务逻辑,比如更新显示信息。

这个案例演示了观察者模式在银行系统中的应用,通过观察者模式可以实现银行客户对自己账户余额的实时监控。

2) Guava中的消息总线

Guava 库中的 EventBus 类提供了一个简单的消息总线实现,可以帮助在 Java 应用程序中实现发布-订阅模式。以下是一个简单的示例,演示了如何使用 Guava 的 EventBus 来实现一个简单的消息发布和订阅功能。

首先,确保您已将 Guava 添加到项目的依赖项中。如果您使用 Maven,请在 pom.xml 文件中添加以下依赖项:

xml 复制代码
<dependency>
  <groupId>com.google.guava</groupId>
  <artifactId>guava</artifactId>
  <version>30.1-jre</version>
</dependency>

接下来,定义一个事件类,例如 MessageEvent

java 复制代码
public class MessageEvent {
    private String message;

    public MessageEvent(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}

现在,创建一个订阅者类,例如 MessageSubscriber。在订阅者类中,定义一个方法并使用 @Subscribe 注解标记该方法,以便 EventBus 能够识别该方法作为事件处理器:

java 复制代码
public class MessageSubscriber {
    @Subscribe
    public void handleMessageEvent(MessageEvent event) {
        System.out.println("收到消息: " + event.getMessage());
    }
}

最后,来看一个使用示例:

java 复制代码
public class Main {
    public static void main(String[] args) {
        // 创建 EventBus 实例
        EventBus eventBus = new EventBus();

        // 创建并注册订阅者
        MessageSubscriber subscriber = new MessageSubscriber();
        eventBus.register(subscriber);

        // 发布事件
        eventBus.post(new MessageEvent("Hello, EventBus!"));

        // 取消注册订阅者
        eventBus.unregister(subscriber);

        // 再次发布事件(此时订阅者已取消注册,将不会收到消息)
        eventBus.post(new MessageEvent("Another message"));
    }
}

在这个示例中,我们创建了一个 EventBus 实例,然后创建并注册了一个 MessageSubscriber 类型的订阅者。当我们使用 eventBus.post() 方法发布一个 MessageEvent 事件时,订阅者的 handleMessageEvent 方法将被调用,并输出收到的消息。

注意,如果订阅者处理事件的方法抛出异常,EventBus 默认情况下不会对异常进行处理。如果需要处理异常,可以在创建 EventBus 实例时传入一个自定义的 SubscriberExceptionHandler

五、进阶

观察者模式的应用场景非常广泛,小到代码层面的解耦,大到架构层面的系统解耦,再或者一些产品的设计思路,都有这种模式的影子。

不同的应用场景和需求下,这个模式也有截然不同的实现方式,之前所列举的所有的例子都是同步阻塞的实现方式,当然我们的观察者设计模式也有异步非阻塞的实现方式;有进程内的实现方式,也有跨进程的实现方式。

之前讲到的实现方式,是一种同步阻塞的实现方式。观察者和被观察者代码在同一个线程内执行,被观察者一直阻塞,直到所有的观察者代码都执行完成之后,才执行后续的代码。对照上面讲到的用户注册的例子,register() 函数依次调用执行每个观察者的 handleRegSuccess() 函数,等到都执行完成之后,才会返回结果给客户端。

如果注册接口是一个调用比较频繁的接口 ,对性能非常敏感,希望接口的响应时间尽可能短,那我们可以将同步阻塞的实现方式改为异步非阻塞的实现方式 ,以此来减少响应时间

1) 异步非阻塞模型

首先,我们需要创建一个通用的观察者接口Observer和一个被观察者接口Observable

Observer.java:

java 复制代码
public interface Observer {
    void update(String message);
}

Observable.java:

java 复制代码
public interface Observable {
    void addObserver(Observer observer);
    void removeObserver(Observer observer);
    void notifyObservers(String message);
}

接下来,我们需要实现一个具体的被观察者类Subject和一个具体的观察者类ConcreteObserver

Subject.java:

java 复制代码
public class Subject implements Observable {
    private List<Observer> observers;
    private ExecutorService executorService;

    public Subject() {
        observers = new ArrayList<>();
        executorService = Executors.newCachedThreadPool();
    }

    @Override
    public void addObserver(Observer observer) {
        observers.add(observer);
    }

    @Override
    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }

    @Override
    public void notifyObservers(String message) {
        for (Observer observer : observers) {
            executorService.submit(() -> observer.update(message));
        }
    }

    public void setMessage(String message) {
        notifyObservers(message);
    }
}

ConcreteObserver.java:

java 复制代码
public class ConcreteObserver implements Observer {
    private String name;

    public ConcreteObserver(String name) {
        this.name = name;
    }

    @Override
    public void update(String message) {
        System.out.println(name + " received message: " + message);
    }
}

最后,我们可以创建一个简单的示例来测试实现的异步非阻塞观察者模式。

Main.java:

java 复制代码
public class Main {
    public static void main(String[] args) {
        Subject subject = new Subject();
        ConcreteObserver observer1 = new ConcreteObserver("Observer 1");
        ConcreteObserver observer2 = new ConcreteObserver("Observer 2");
        ConcreteObserver observer3 = new ConcreteObserver("Observer 3");

        subject.addObserver(observer1);
        subject.addObserver(observer2);
        subject.addObserver(observer3);

        subject.setMessage("Hello, observers!");

        // 等待异步任务完成
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,我们使用了ExecutorService的线程池来实现异步非阻塞的通知。每个观察者更新操作都将作为一个任务提交给线程池并异步执行。这将确保性能敏感的场景不会因为观察者的通知而阻塞。

相关推荐
Allen Bright10 分钟前
Java代码操作Zookeeper(使用 Apache Curator 库)
java·zookeeper·java-zookeeper
云上星空18 分钟前
K8s调度器扩展(scheduler)
java·贪心算法·kubernetes
突然好想你之路在脚下26 分钟前
Kubernetes(k8s)入门到实战教程
java·容器·kubernetes
博观而约取30 分钟前
Jenkins升级到最新版本后无法启动
java·linux·centos·jenkins
yang_shengy1 小时前
【JavaEE】多线程(2)
java·开发语言·多线程·
lix的小鱼1 小时前
scala之全文单词统计
java·开发语言·后端·python·算法·c#·scala
GGBondlctrl1 小时前
【Spring MVC】关于Spring MVC编程中前后端交互实现简单登录跳转和留言板实例
java·spring mvc·lombok·前后端交互·登录跳转·留言板·接口文档定义
小白不太白9501 小时前
设计模式之 访问者模式
java·设计模式·访问者模式
怀旧6661 小时前
Java LinkedList 讲解
java·开发语言·后端·个人开发
冷心笑看丽美人1 小时前
setter方法注入(Java EE 学习笔记07)
java·笔记·学习·spring·java-ee·javaee