二十、观察者模式

文章目录

  • [1 基本介绍](#1 基本介绍)
  • [2 案例](#2 案例)
    • [2.1 Stock 抽象类](#2.1 Stock 抽象类)
    • [2.2 StockA 类](#2.2 StockA 类)
    • [2.3 Website 抽象类](#2.3 Website 抽象类)
    • [2.4 WebsiteA 类](#2.4 WebsiteA 类)
    • [2.5 Client 类](#2.5 Client 类)
    • [2.6 Client 类的运行结果](#2.6 Client 类的运行结果)
    • [2.7 总结](#2.7 总结)
  • [3 各角色之间的关系](#3 各角色之间的关系)
    • [3.1 角色](#3.1 角色)
      • [3.1.1 Subject ( 被观察的主体 )](#3.1.1 Subject ( 被观察的主体 ))
      • [3.1.2 ConcreteSubject ( 具体的主体 )](#3.1.2 ConcreteSubject ( 具体的主体 ))
      • [3.1.3 Observer ( 观察者 )](#3.1.3 Observer ( 观察者 ))
      • [3.1.4 ConcreteObserver ( 具体的观察者 )](#3.1.4 ConcreteObserver ( 具体的观察者 ))
      • [3.1.5 Client ( 客户端 )](#3.1.5 Client ( 客户端 ))
    • [3.2 类图](#3.2 类图)
  • [4 注意事项](#4 注意事项)
  • [5 在源码中的使用](#5 在源码中的使用)
  • [6 优缺点](#6 优缺点)
  • [7 适用场景](#7 适用场景)
  • [8 总结](#8 总结)

1 基本介绍

观察者模式 (Observer Pattern)是一种 行为型 设计模式,它在对象之间建立 一对多 的依赖关系,使得 当一个对象的状态发生变化时其所有依赖的对象都会得到通知并自动更新 。这个模式也称作 发布 - 订阅模式 ,多个对象 订阅 一个对象,当这个对象的内部状态发生变化时,它会 通知 多个对象,让它们更新有关这个对象的状态。

2 案例

本案例实现了一个观测股票价值的网站,当股票的价值变化时,更新网站中存储的股票价值。

2.1 Stock 抽象类

java 复制代码
import java.util.ArrayList;
import java.util.List;

public abstract class Stock { // 股票
    private List<Website> websites = new ArrayList<>(); // 储存所有观测股票价值的网站

    public void addWebsite(Website website) { // 添加网站
        websites.add(website);
    }

    public void deleteWebsite(Website website) { // 删除指定网站
        websites.remove(website);
    }

    public void notifyWebsites() { // 通知所有网站修改本股票的数据
        for (Website website : websites) {
            website.update(this);
        }
    }

    protected int value; // 股票的价值
    private String name; // 股票的名称

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

    public int getValue() { // 获取股票的价值
        return value;
    }

    public String getName() { // 获取股票的名称
        return name;
    }

    public abstract void simulate(); // 模拟股票价值的跌涨,具体实现交给子类
}

2.2 StockA 类

java 复制代码
import java.util.Random;

public class StockA extends Stock { // 股票A
    public StockA() {
        super("股票A");
    }

    private Random random = new Random();

    @Override
    public void simulate() {
        // 每隔 1 秒修改一次股票A的价值,共修改 10 次,修改完后通知各个网站修改数据
        for (int i = 0; i < 10; i++) {
            value = random.nextInt(100 + value) + 10; // 保底价值为 10
            notifyWebsites();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

2.3 Website 抽象类

java 复制代码
public abstract class Website { // 能查看股票情况的网站
    public abstract void update(Stock stock); // 更新指定股票的数据
    // 显示股票的数据,这个方法可以被外部直接调用,不过此时显示的是缓存的数据
    public abstract void print();
}

2.4 WebsiteA 类

java 复制代码
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class WebsiteA extends Website { // 监测股票数据的网站A
    private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    // 缓存股票的信息,key 是股票的名称,value 是股票的价格
    private Map<String, Integer> stocks = new HashMap<>();

    @Override
    public void update(Stock stock) {
        stocks.put(stock.getName(), stock.getValue());
        print(); // 更新完数据之后打印一次信息
    }

    @Override
    public void print() {
        System.out.println("==============「网站A」==============");
        for (Map.Entry<String, Integer> entry : stocks.entrySet()) {
            System.out.println(SDF.format(new Date())
                    + " [" + entry.getKey() + "]的价格为:" + entry.getValue());
        }
    }
}

2.5 Client 类

java 复制代码
public class Client { // 客户端,测试了 网站A 监测 股票A 的跌涨
    public static void main(String[] args) {
        Stock stockA = new StockA();
        WebsiteA websiteA = new WebsiteA();
        stockA.addWebsite(websiteA);
        stockA.simulate();
    }
}

2.6 Client 类的运行结果

==============「网站A」==============
2024-08-17 00:10:12 [股票A]的价格为:74
==============「网站A」==============
2024-08-17 00:10:13 [股票A]的价格为:110
==============「网站A」==============
2024-08-17 00:10:14 [股票A]的价格为:199
==============「网站A」==============
2024-08-17 00:10:15 [股票A]的价格为:289
==============「网站A」==============
2024-08-17 00:10:16 [股票A]的价格为:321
==============「网站A」==============
2024-08-17 00:10:17 [股票A]的价格为:89
==============「网站A」==============
2024-08-17 00:10:18 [股票A]的价格为:174
==============「网站A」==============
2024-08-17 00:10:19 [股票A]的价格为:129
==============「网站A」==============
2024-08-17 00:10:20 [股票A]的价格为:135
==============「网站A」==============
2024-08-17 00:10:21 [股票A]的价格为:143

2.7 总结

本案例中,将股票的数据缓存到网站中,一旦股票的价值发生变化,股票就通知所有订阅这支股票的网站,让它们修改股票数据,一致性比较强 (这里的一致性指的是 股票的真实价值 和 网站中缓存的价值 是相同的),这就是 发布 - 订阅 模型的优点。

如果想要再添加 一支股票 或 一个网站,则只需要继承对应的抽象类,并实现方法,然后就可以让网站订阅股票,股票一旦修改数据,订阅的网站将会更新股票的数据。可以发现,这个系统的扩展性比较强。

3 各角色之间的关系

3.1 角色

3.1.1 Subject ( 被观察的主体 )

该角色负责定义三类接口:

  • 注册 和 删除 Observer 角色
  • 通知所有已注册的 Observer 角色
  • 获取当前状态

本案例中,Stock 抽象类扮演了该角色。

3.1.2 ConcreteSubject ( 具体的主体 )

该角色负责 实现 Subject 角色定义的 接口 。本案例中,StockA 类扮演了该角色。

3.1.3 Observer ( 观察者 )

该角色负责 定义 更新本地缓存的旧数据 的接口 ,当 Subject 角色的状态发生变化时,它会调用这个接口来通知所有的 Observer 角色。本案例中,Website 抽象类扮演了该角色。

3.1.4 ConcreteObserver ( 具体的观察者 )

该角色负责 实现 Observer 角色定义的 接口 。本案例中,WebsiteA 类扮演了该角色。

3.1.5 Client ( 客户端 )

该角色负责 创建合适的 ConcreteSubject 角色和 ConcreteObserver 角色使用 Subject 角色和 Observer 角色完成具体的业务逻辑 。本案例中,Client 类扮演了该角色。

3.2 类图

说明:Subject 和 Observer 都可以使用接口实现,不过这时在 Subject 中就无法实现与 Observer 相关的三个方法了。

4 注意事项

  • 定义清晰的接口:确定 Observer 接口,包括需要通知的方法以及可能的参数。这样可以确保 Subject 和 Object 之间的松耦合关系,并提供灵活性。
  • 避免滥用:观察者模式适用于 Subject 和 Observer 之间的一对多关系,如果仅涉及两个对象之间的通信,使用观察者模式可能过于复杂。本案例中没有明显体现出这一点,这是为了避免案例过于复杂。
  • 注销通知 :在 Subject 和 Observer 进行销毁时,都要向对方发送通知 。这有助于 避免在对象销毁后还尝试进行通信,从而引发错误或异常。
  • 消息通知顺序 :当多个 Observer 监听同一个 Subject 时,Observer 接收通知的顺序可能会影响系统行为。需要明确观察者的通知顺序,以确保正确的处理顺序。
  • 谨慎处理循环依赖 :在 Subject 通知 Observer 时,如果 Observer 也改变了 Subject 的状态,接着 Subject 再通知 Observer,会导致无限循环。必要时,可以考虑使用 标志 或其他机制来避免循环依赖。
  • 观察者生命周期管理 :需要 及时 注册 和 删除 观察者 ,以避免 资源泄漏潜在的内存问题
  • 线程安全性 :如果在 多线程环境 下使用观察者模式,需要 确保 Subject 和 Observer 的并发访问是线程安全的 ,可以考虑使用 同步机制线程安全的集合 来避免数据竞争和状态不一致的问题。
  • 性能优化 :如果 Observer 的处理逻辑耗时较长,可能会影响 Subject 的性能。可以使用 异步延迟通知 的方式来优化性能,确保通知方法尽快返回控制权,避免阻塞其他操作。如果对 Observer 的通知是通过另外的线程进行 异步投递 的,系统需要避免 消息丢失重复处理

5 在源码中的使用

在 JDK 中,java.util.Observer 接口java.util.Observable 是实现观察者模式的基础。其对应的角色如下所示:

  • Subject 角色Observable 类,这个类中实现了基础的两类方法(缺少了 获取本对象状态 的方法):

    java 复制代码
    public class Observable {
        private boolean changed = false; // 标记本对象的状态是否改变
        private Vector<Observer> obs; // 存储所有 Observer 的变长数组
    
        public Observable() {
            obs = new Vector<>();
        }
    
        // 注册 Observer
        public synchronized void addObserver(Observer o) {
            if (o == null)
                throw new NullPointerException();
            if (!obs.contains(o)) {
                obs.addElement(o);
            }
        }
    
        // 删除 Observer
        public synchronized void deleteObserver(Observer o) {
            obs.removeElement(o);
        }
    
        // 删除所有 Observer
        public synchronized void deleteObservers() {
            obs.removeAllElements();
        }
    
        // 获取已注册的 Observer 个数
        public synchronized int countObservers() {
            return obs.size();
        }
    
        // 当本对象的状态变化时,通知所有的 Observer
        public void notifyObservers() {
            notifyObservers(null);
        }
    
       // 当本对象的状态变化时,通知所有的 Observer
        public void notifyObservers(Object arg) {
            // 存储当前所有的 Observer
            Object[] arrLocal;
    
            synchronized (this) {
                // 如果本对象的状态没有变化,则无需通知 Observer
                if (!changed)
                    return;
                arrLocal = obs.toArray(); // 获取当前所有的 Observer
                clearChanged(); // 清除改变标记
            }
    
    		// 倒序通知所有的 Observer
            for (int i = arrLocal.length-1; i>=0; i--)
                ((Observer)arrLocal[i]).update(this, arg);
        }
    
        // 标记本对象的状态被改变,由其子类调用
        protected synchronized void setChanged() {
            changed = true;
        }
    
        // 清除改变的标记,由本类调用
        protected synchronized void clearChanged() {
            changed = false;
        }
    
        // 检查本对象的状态是否改变
        public synchronized boolean hasChanged() {
            return changed;
        }
    }
  • ConcreteSubject 角色 :继承 Observable 类的子类就是 ConcreteSubject,一般要调用其父类的 setChanged() 方法,并且还需要实现 获取本对象状态的方法

  • Observer 角色Observer 接口,这个接口中定义了 update() 方法:

    java 复制代码
    public interface Observer {
        void update(Observable o, Object arg);
    }
  • ConcreteObserver 角色 :实现 Observer 接口的类就是 ConcreteObserver,在 Subject 发生变化时,调用 update() 方法。

6 优缺点

优点

  • 增强了数据的一致性 :当 Subject 的状态发生变化时,它可以通知所有已注册的 Observer。这种广播机制使得多个 Observer 能够 同时响应状态变化 ,增强了数据的 一致性
  • 降低系统的耦合度 :观察者模式实现了 Subject 和 Observer 之间的松耦合,这意味着 Subject 和 Observer 可以独立地改变和复用,而不需要修改对方。这种松耦合使得系统更加 灵活易于维护
  • 增强系统的扩展性 :可以在运行时动态地 添加删除 观察者,而不需要修改主题类的代码。这种灵活性使得系统能够更容易地适应变化。
  • 符合开闭原则:观察者模式对扩展开放,对修改关闭。当需要增加新的 ConcreteObserver 时,只需要实现(或 继承)Observer 并注册到 Subject 中即可,而不需要修改 Subject 的代码。

缺点

  • 性能问题 :如果一个 Subject 拥有 大量的 Observer 时,并且 状态更新非常频繁 ,那么每次状态更新时, Subject 都需要遍历整个 Observer 列表并通知它们,这可能会导致性能问题,特别是在 高并发 场景下。
  • 可能导致循环依赖 :如果 Observer 和 Subject 之间存在相互依赖的关系,那么可能会导致循环依赖问题。这种情况下,系统可能会出现 死锁无限递归 等问题。
  • 难以实现异步通信 :观察者模式通常是在 同步方式 下工作的,即 Subject 在状态更新后 立即 通知 Observer。在某些情况下,如果 Observer 需要花费较长时间来处理状态更新,那么这可能会阻塞 Subject 或 其他 Observer 的执行。虽然可以通过一些技术手段(如使用 多线程异步消息队列)来实现异步通信,但这会增加系统的复杂性和实现难度。

7 适用场景

  • 消息发布 - 订阅系统 :观察者模式可以用于 构建 消息发布 - 订阅系统 。在这种系统中,消息发布者充当 Subject而订阅者则充当 Observer。当发布者发布新消息时,所有订阅者都会收到通知并执行相应操作。例如 新闻订阅服务、实时数据监控系统 等。
  • 图形用户界面(GUI)开发 :在 GUI 开发中,观察者模式常被用于 处理用户界面组件之间的交互。当一个组件的状态发生变化时,其他依赖该组件的组件将自动更新以反映新的状态。例如 按钮点击事件、窗口状态变化 等。
  • 事件驱动系统 :观察者模式也常用于 事件驱动系统 中,如图形用户界面框架、游戏引擎等。当 特定事件发生 时,触发相应的回调函数并通知所有注册的观察者。
  • 实时数据更新 :在需要 实时更新数据的应用 中,观察者模式可以用于 将 数据源 与 数据消费者 连接起来。当数据源的数据发生变化时,观察者可以自动获取最新的数据并进行处理。例如 实时天气更新、股票价格实时推送 等。
  • 消息队列系统 :观察者模式可用于 消息队列系统 ,其中 生产者 将消息发送到队列,而 消费者 作为 Observer 订阅队列以接收和处理消息。
  • 分布式系统数据同步 :在分布式系统中,可以使用观察者模式实现 节点之间的数据同步。当任何一个节点的状态发生变化时,它会通知其他的节点进行相应的更新操作。例如 分布式数据库、缓存系统 等。

8 总结

观察者模式 是一种 行为型 设计模式,它在对象间建立了 一对多 的关系,当 的状态发生变化时, 会通知 ,更新 缓存的状态,从而实现 之间的强一致性。但是在 的数量太大 且 更新 缓存状态的操作很耗时 的情况下,系统的性能会比较低。此外,在使用本模式时,要注意 Subject 和 Observer 之间最好不要循环调用,因为这样可能会造成无限递归的问题。

相关推荐
陈大爷(有低保)16 分钟前
UDP Socket聊天室(Java)
java·网络协议·udp
nakyoooooo20 分钟前
【设计模式】工厂模式、单例模式、观察者模式、发布订阅模式
观察者模式·单例模式·设计模式
kinlon.liu30 分钟前
零信任安全架构--持续验证
java·安全·安全架构·mfa·持续验证
王哲晓1 小时前
Linux通过yum安装Docker
java·linux·docker
java6666688881 小时前
如何在Java中实现高效的对象映射:Dozer与MapStruct的比较与优化
java·开发语言
Violet永存1 小时前
源码分析:LinkedList
java·开发语言
执键行天涯1 小时前
【经验帖】JAVA中同方法,两次调用Mybatis,一次更新,一次查询,同一事务,第一次修改对第二次的可见性如何
java·数据库·mybatis
Jarlen1 小时前
将本地离线Jar包上传到Maven远程私库上,供项目编译使用
java·maven·jar
蓑 羽1 小时前
力扣438 找到字符串中所有字母异位词 Java版本
java·算法·leetcode
Reese_Cool1 小时前
【C语言二级考试】循环结构设计
android·java·c语言·开发语言