设计模式第六章(观察者模式)

设计模式第六章(观察者模式)

​ 观察者模式是一种行为设计模式,它定义了对象之间的一对对多依赖关系:当一个对象(被观察者)的状态发生变化时,所有依赖它的对象(观察者)会自动收到通知并更新。

这种模式的核心思想是解耦被观察者和观察者,让它们可以独立变化,同时保持联动。

前言

关键角色

  1. 被观察者(Observable/Subject)
    • 维护一个观察者列表,提供添加、移除观察者的方法
    • 当自身状态变化时,主动通知所有注册的观察者
  2. 观察者(Observer)
    • 定义一个更新接口,当收到被观察者的通知时,执行相应的处理逻辑
    • 可以有多个不同的观察者实现,各自处理方式不同

工作流程

  1. 观察者通过被观察者提供的方法(如 addObserver())注册自己
  2. 被观察者状态发生改变时,调用自身的通知方法(如 notifyObservers()
  3. 被观察者遍历所有注册的观察者,调用它们的更新方法(如 update()
  4. 观察者收到通知后,根据被观察者的状态变化执行具体操作

典型场景

  • 消息订阅:公众号(被观察者)更新文章后,所有订阅者(观察者)收到推送
  • 数据监控:传感器(被观察者)检测到温度变化,仪表盘、报警器(观察者)分别响应
  • GUI 事件处理:按钮(被观察者)被点击时,关联的回调函数(观察者)执行

优点

  • 降低耦合:被观察者无需知道观察者的具体实现,只需调用统一的更新接口
  • 扩展性好:新增观察者时,无需修改被观察者代码,符合 "开闭原则"
  • 联动灵活:可以动态添加 / 移除观察者,实时调整响应关系

例如,在天气系统中:

  • 气象站(被观察者)收集到温度变化
  • 手机 APP、显示屏、报警器(多个观察者)会同时收到通知并更新显示或触发警报
  • 若后续需要添加新的观察者(如恒温控制器),只需让它实现观察者接口并注册即可,无需修改气象站代码。

实战第一版本

故事背景:一个天气站如果更新了天气,需要将天气通知订阅了天气更新的某些人和事。我们来用一个初级版本直接堆功能来实现。

用户信息

java 复制代码
public class User {

    private final String name;

    private final Consumer<String> consumer;

    public User(String name, Consumer<String> consumer) {
        this.name = name;
        this.consumer = consumer;
    }


    public void notify(String inf) {
        consumer.accept(inf);
    }
}

天气站

java 复制代码
public class WeatherStation {


    private final List<User> users = new ArrayList<User>();

    public void addUser(User user) {
        users.add(user);
    }


    public String getInfo() {
        if (new Random().nextBoolean()) {
            return "晴天";
        }
        return "雨天";
    }


    public void start() {
        while (true) {
            String info = getInfo();
            for (User user : users) {
                user.notify(info);
            }
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

测试用例

java 复制代码
public class Main {
    public static void main(String[] args) {
        //TIP Press <shortcut actionId="ShowIntentionActions"/> with your caret at the highlighted text
        // to see how IntelliJ IDEA suggests fixing it.
        System.out.println("Hello and welcome!");

        User tom = new User("tom", inf -> {
            if (inf.equals("晴天")) {
                System.out.println("晴天了,tom快出来玩啊~~~");
            } else {
                System.out.println("下雨天,tom 你需要在家待着了~~~");
            }
        });

        User jerry = new User("jerry", inf -> {
            if (inf.equals("晴天")) {
                System.out.println("晴天了,jerry快出来游泳了~~");
            }
        });

        //订阅天气信息
        WeatherStation station = new WeatherStation();
        station.addUser(tom);
        station.addUser(jerry);

        station.start();


        // 问题点,我本来只是一个天气站,怎么通知的事情还需要我来做呢,
        // 通知的事情是不是需要电视台来做呢?如果按照单一职责,那么通知的事情就不应该是我来做,如果找我订阅的是一个组织,我是不是又需要在里面加上一个组织的消费呢
        // 接下来,我们就需要一个tvstation来进行通知


    }
}

问题点

  • 问题点,我本来只是一个天气站,怎么通知的事情还需要我来做呢

  • 通知的事情是不是需要电视台来做呢?如果按照单一职责,那么通知的事情就不应该是我来做,如果找我订阅的是一个组织,我是不是又需要在里面加上一个组织的消费呢

  • 接下来,我们就需要一个tvstation来进行通知

实战第二版本

​ 我们增加了一个电视台的角色,所有的订阅都去电视台,天气信息的更新我只负责通知到电视台,由电视台再通知被订阅的人

用户信息

java 复制代码
public class User {

    private final String name;

    private final Consumer<String> consumer;

    public User(String name, Consumer<String> consumer) {
        this.name = name;
        this.consumer = consumer;
    }


    public void notify(String inf) {
        consumer.accept(inf);
    }
}

电视台

  • 提供一个订阅的入口
  • 提供一个发布的入口
java 复制代码
public class TvStation {

    private final List<User> userList = new ArrayList<>();

    // 订阅的入口
    public void addUser(User user) {
        userList.add(user);
    }


    public void publish(String inf) {
        for (User user : userList) {
            user.notify(inf);
        }
    }

}

天气站

java 复制代码
public class WeatherStation {

    //电视台
    private final TvStation tvStation;

    public WeatherStation(TvStation tvStation) {
        this.tvStation = tvStation;
    }


    public String getInfo() {
        if (new Random().nextBoolean()) {
            return "晴天";
        }
        return "雨天";
    }


    public void start() {
        while (true) {
            String info = getInfo();
            //我只需要把我得消息给到电视台,由电视台进行这个通知的动作
            tvStation.publish(info);
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

测试用例

java 复制代码
public class Main {
    public static void main(String[] args) {
        //TIP Press <shortcut actionId="ShowIntentionActions"/> with your caret at the highlighted text
        // to see how IntelliJ IDEA suggests fixing it.
        System.out.println("Hello and welcome!");

        User tom = new User("tom", inf -> {
            if (inf.equals("晴天")) {
                System.out.println("晴天了,tom快出来玩啊~~~");
            } else {
                System.out.println("下雨天,tom 你需要在家待着了~~~");
            }
        });

        User jerry = new User("jerry", inf -> {
            if (inf.equals("晴天")) {
                System.out.println("晴天了,jerry快出来游泳了~~");
            }
        });

        TvStation tvStation = new TvStation();
        //两个用户订阅了电视台的消息
        tvStation.addUser(tom);
        tvStation.addUser(jerry);

        // 获取天气信息
        WeatherStation weatherStation = new WeatherStation(tvStation);
        weatherStation.start();

        // 天气只负责获取信息,通知由电视台进行
        // 优化点,如果用户需要订阅 增加了一个新闻类的信息该怎么办呢
        // 下一节,我们将抽象发布布订阅模式

    }
}

问题点

  • 如果用户需要订阅 一个新闻类的信息该怎么办呢,我们是不是需要在里面再继续加一个新闻的类的,下一章节使用订阅发布模式来解耦

实战第三版本

  • 事件,所有的需要通知的事件都需要实现该接口。
  • 事件监听,所有希望得到通知的都需要实现该接口

事件总线接口

java 复制代码
public interface Event {


    /**
     *  时间戳
     * @return
     */
    long timestamp();

    /**
     *  数据个格式
     * @return
     */
    Object source();
}

事件总线抽象基类

java 复制代码
public abstract class BaseEvent implements Event {

    @Override
    public long timestamp() {
        return System.currentTimeMillis();
    }
}

天气更新的事件

如果天气更新了,那么我事先事件这个接口,信息都包装为一个 event 事件信息

复制代码
public class WeatherUpdEvent extends BaseEvent{

    //天气信息
    private final String info;

    public WeatherUpdEvent(String info) {
        this.info = info;
    }

    @Override
    public Object source() {
        return info;
    }
}

事件监听

java 复制代码
public interface ListenerEvent {


    /**
     *  监听事件
     * @param event
     */
    void onEvent(Event event);
}

订阅感兴趣的事件

实际为最终消费端。拿到事件,然后做什么

java 复制代码
public class User implements ListenerEvent {

  private final String name;

  private final Consumer<String> consumer;

  public User(String name, Consumer<String> consumer) {
    this.name = name;
    this.consumer = consumer;
  }


  private void notify(String inf) {
    consumer.accept(inf);
  }

  @Override
  public void onEvent(Event event) {
    if (event instanceof WeatherUpdEvent) {
      notify(event.source().toString());
    }
  }


}

事件总线

我们监听订阅了event时间下所有的订阅者。

java 复制代码
public class TvStation {


    private final Map<Class<? extends Event>,List<ListenerEvent>> listenerEventMap = new HashMap<>();


    // 订阅的入口
    public void subscribe(ListenerEvent event,Class<? extends Event> eventType) {
        listenerEventMap.computeIfAbsent(eventType,k -> new ArrayList<>()).add(event);
    }


    public void publish(Event event) {
        Class<? extends Event> evnetClass = event.getClass();
        List<ListenerEvent> listenerEvents = listenerEventMap.get(evnetClass);
        if (listenerEvents != null) {
            for (ListenerEvent listener : listenerEvents) {
                listener.onEvent(event);
            }
        }
    }

}

天气站

我们需要将我们的天气信息包装为一个event 的事件,这个时候就是天气更新的事件,我们进行包装一次。

java 复制代码
public class WeatherStation {

    //电视台  我们成为消息总线
    private final TvStation tvStation;

    public WeatherStation(TvStation tvStation) {
        this.tvStation = tvStation;
    }


    public String getInfo() {
        if (new Random().nextBoolean()) {
            return "晴天";
        }
        return "雨天";
    }


    public void start() {
        while (true) {
            String info = getInfo();
            WeatherUpdEvent weatherUpdEvent = new WeatherUpdEvent(info);
            tvStation.publish(weatherUpdEvent);
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

测试用例

java 复制代码
public class Main {
    public static void main(String[] args) {
        User tom = new User("tom", inf -> {
            if (inf.equals("晴天")) {
                System.out.println("晴天了,tom快出来玩啊~~~");
            } else {
                System.out.println("下雨天,tom 你需要在家待着了~~~");
            }
        });

        User jerry = new User("jerry", inf -> {
            if (inf.equals("晴天")) {
                System.out.println("晴天了,jerry快出来游泳了~~");
            }
        });


        TvStation tvStation = new TvStation();
        tvStation.subscribe(tom, WeatherUpdEvent.class);
        tvStation.subscribe(jerry, WeatherUpdEvent.class);

        WeatherStation station = new WeatherStation(tvStation);
        station.start();


    }
}

spring使用观察者模式

复制代码
@Autowired
private ApplicationContext context;


// 事件发布器
@Autowired
private ApplicationEventPublisher publisher;

// 事件广播
@Autowired
private ApplicationEventMulticaster multicaster;

ApplicationContext

我们看到 ApplicationContext 继承ApplicationEventPublisher,那么这两个是不是就是一个对象呢。

实战部分

我们定义了一个controller,当用户注册的时候,我们需要像用户发送邮件,送新手礼物。

java 复制代码
   @GetMapping("regiest")
    public String regiest(@RequestParam("userName") String userName) {
         publisher.publishEvent(new RegisterEvent(userName));

        //context.publishEvent(new RegisterEvent(userName));

        //multicaster.multicastEvent(new RegisterEvent(userName));
        return "success";
    }
定义用户注册的事件
java 复制代码
public class RegisterEvent extends ApplicationEvent {



    public RegisterEvent(Object user) {
        super(user);
    }

    public String getUserName() {
        return getSource().toString();
    }
}
监听用户注册成功事件
  • 发放礼包

    java 复制代码
    @Service
    public class GiftService {
    
    
        @EventListener
        public void registerEvent(RegisterEvent event) {
            String userName = event.getUserName();
            System.out.println("发放新手礼品给: " + userName);
        }
    }
  • 邮件通知

    java 复制代码
    @Service
    public class EmailService {
    
    
        @EventListener
        public void registerEvent(RegisterEvent event) {
            String userName = event.getUserName();
            System.out.println("给用户: " + userName + "发送邮件");
        }
    }
启动类测试

源码分析部分

  • org.springframework.context.support.AbstractApplicationContext#publishEvent(java.lang.Object, org.springframework.core.ResolvableType)
    • org.springframework.context.event.SimpleApplicationEventMulticaster#multicastEvent(org.springframework.context.ApplicationEvent, org.springframework.core.ResolvableType)
      • org.springframework.context.event.SimpleApplicationEventMulticaster#multicastEvent(org.springframework.context.ApplicationEvent, org.springframework.core.ResolvableType)
        • org.springframework.context.event.ApplicationEventMulticaster#multicastEvent(org.springframework.context.ApplicationEvent, org.springframework.core.ResolvableType)
相关推荐
sunfove7 小时前
光网络的立交桥:光开关 (Optical Switch) 原理与主流技术解析
网络
xiaolyuh1237 小时前
Spring 框架 核心架构设计 深度详解
spring·设计模式·spring 设计模式
Kevin Wang72710 小时前
欧拉系统服务部署注意事项
网络·windows
min18112345610 小时前
深度伪造内容的检测与溯源技术
大数据·网络·人工智能
汤愈韬10 小时前
Full Cone Nat
网络·网络协议·网络安全·security·huawei
zbtlink11 小时前
现在还需要带电池的路由器吗?是用来干嘛的?
网络·智能路由器
桌面运维家11 小时前
vDisk配置漂移怎么办?VOI/IDV架构故障快速修复
网络·架构
dalerkd11 小时前
忙里偷闲叙-谈谈最近两年
网络·安全·web安全
汤愈韬12 小时前
NAT ALG (应用层网关)
网络·网络协议·网络安全·security·huawei
运维栈记13 小时前
虚拟化网络的根基-网络命名空间
网络·docker·容器