【设计模式】循序渐进的理解观察者模式&Spring事件机制的运用

文章目录

1. 概述

观察者模式(Observer Pattern) 是一种应用广泛的设计模式,有时候也被叫做发布/订阅模式 或者监听器模式 ,如果你用过消息队列 或者写过原生JS和JQuery中的Listener的话,对这种模式一定不陌生,接下来你会非常容易理解这种模式,如果没有使用过也没关系可以通过本文来了解一下这种模式的运用。

本文中会通过大量的类图来描述观察者模式中的不同角色,对类型不太熟悉的同学可以参考者这篇文章一起看《【UML建模】类图(Class Diagram)》

2.循序渐进的理解观察者模式

2.1 观察者模式概念引入

观察者模式就是一个对象的内部状态发生改变 时(就是做了点事),依赖它的一个或多个对象会得到通知并自动更新状态(就是跟着做点事),定义上就是这么简单。

我们先看一下观察者模式中的两种角色:观察者被观察者

观察者在被观察者doSomething()的时候,自动的触发doSomething()应该怎么做呢?

最简单的做法就是被观察者执行doSomething()时,调用一下观察者的doSomething()方法,也就是最常见的方法调用。从这里可以看出,所谓的自动更新 并不是观察者主观上的自动运行,而是依赖于其他对象的触发。

这种触发机制我们一般把它叫做事件机制 ,在软件工程中大量运用,例如使用Jenkins中的CICD配置当git提交时自动构建服务到测试环境就是一种体现。

2.2.观察者接口抽象

那上述的这种方法调用就叫做观察者模式了?这和平时写的代码也没啥区别嘛!

当然不仅仅是这样的,我们顺着这个类图分析,假设我们现在新增两个观察者,类图就变成了这样:

可以想象一下,如果这个时候需要触发所有观察者的方法,就需要写3次方法调用,分别调用3个不同的观察者,但假如有10个、20个观察者呢?

针对这种情况,我们当然不可能去写20次方法调用,更好的方式是事先将所有观察者都放到集合中,当被观察者执行了指定的方法时,直接循环这个集合依次触发就好了。

要想让所有的观察者都可以放入到同一个集合中,我们需要对观察者做抽象,定义一个观察者接口,类图如下:

2.3 被观察者接口抽象

现在我们已经有了观察者集合,接下来就是对这个集合的操作,相信大家很容易就可以想到至少应该包含3种不同的操作:

  • 新增集合元素:可动态的加载观察者
  • 移除集合元素:可动态的卸载观察者
  • 循环集合元素触发事件:触发观察者更新状态

在实际的业务中,不同的业务流程中会定义不同的被观察者,每个被观察者都有上述的集合以及3个操作,这就需要我们将其提取到父类 中。

抽取共性中的实例方法一般使用抽象类(Java8以后也可以使用Interface中的default方法),最终形成了下面的类图:

执行流程如下:

复制代码
1.创建观察者对象
2.创建被观察者对象,并将观察者add到集合中
3.调用被观察者的doSomething
4.被观察者调用父类中的trigger,循环集合获取观察者,并调用观察者中的doSomething
5.如果不需要某个观察者后,通过remove将其从集合中移除,后续就不再触发这个观察者的doSomething方法了

2.4 观察者模式的通用类图

根据上面的分析,我们可以得出观察者模式的4种角色,分别为:

  • 观察者(Observer):上述的观察者接口
  • 被观察者(Subject):被观察者抽象类
  • 具体的观察者(Concrete Observer):观察者实例
  • 具体的被观察者(Concrete Subject):被观察者实例

综上,可以得出观察者模式的通用类图:

需注意的是,trigger方法不一定是空参,也可以根据业务的需要传输几个必要的参数,观察者可以根据获取到的参数采取不同针对性的处理。


再插一句题外话,由于观察者的生命周期并不需要与被观察者一直(即可以运行时动态的增减),所以此处的关联关系采用聚合 而不是组合

2.5.观察者模式的通用代码实现

通过上面的通用类图,写一个通用代码的实现,并验证在运行时动态的移除观察者。

观察者: 提供一个观察者接口与两个实现类

java 复制代码
/**
 * 观察者接口
 */
public interface Observer {
    void doSomething(String msg);
}

/**
 * 观察者实现1
 */
public class ConcreteObserver1 implements Observer {
    @Override
    public void doSomething(String msg) {
        System.out.println("触发观察者1,参数:" + msg);
    }
}

/**
 * 观察者实现2
 */
public class ConcreteObserver2 implements Observer {
    @Override
    public void doSomething(String msg) {
        System.out.println("触发观察者2,参数:" + msg);
    }
}

被观察者: 提供一个抽象父类与一个被观察者实现

java 复制代码
/**
 * 被观察者抽象类
 */
public abstract class Subject {

    List<Observer> observerList = new ArrayList<>();

    public void addObserver(Observer observer) {
        observerList.add(observer);
    }

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

    public void trigger(String msg) {
        for (Observer observer : observerList) {
            observer.doSomething(msg);
        }
    }
}
/**
 * 被观察者实现
 */
public class ConcreteSubject extends Subject {
    public void doSomething() {
        System.out.println("被观察者执行方法");
        String msg = "被观察者执行方法";
        super.trigger(msg);
    }
}

随便写个main方法做个测试

java 复制代码
public class Test {

    public static void main(String[] args) {
        ConcreteSubject subject = new ConcreteSubject();
        ConcreteObserver1 observer1 = new ConcreteObserver1();
        ConcreteObserver2 observer2 = new ConcreteObserver2();

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

        System.out.println("-----第一次运行-----");
        subject.doSomething();

        System.out.println("移除观察者1");
        subject.removeObserver(observer1);
        System.out.println("-----第二次运行-----");
        subject.doSomething();
    }
}

3.Spring中的事件运用

虽然我们在日常的开发过程中也能使用上面的通用代码来实现观察者模式,但如果在开发的过程中使用了Spring框架的话,我们也可以使用Spring中的事件机制(Event) 来替代上述的通用代码。

Spring的事件机制其实就是对观察者模式的封装,达到更加通用且实现更加简单的效果。

3.1.Spring事件中的几个角色介绍

使用Spring的事件,其实就是通过继承、实现、聚合、依赖等方式,将Spring封装好的功能对象集成到业务代码中,在集成之前有必要先了解一下这些功能对象。

  • ApplicationEvent:用于定义事件对象,事件对象既是事件触发的标识类型,也是事件中信息传输的载体,可简单的立即为事件触发后向观察者传递的参数。
  • ApplicationListener:用于接收事件的监听器,其实就是观察者这个角色。
  • ApplicationEventPublisher:事件推送器,用于将事件推送给事件监听器(其实是推送给了事件投递器,下面有详细的解释)。
  • ApplicationEventPublisherAware:用于将事件推送器聚合到业务对象中,Spring中有很多不同的Aware接口都是这个作用,属于是Spring的扩展点。

以上是我们在编码过程中会直接使用到的角色,如果仔细对比了上面的通用类图中的角色,一定会发现这些角色还少了一部分功能,即:新增、移除观察者,以及触发方法trigger。这部分"缺失"的功能在另一个接口ApplicationEventMulticaster中,看下面的截图就明白了。

至于如何使用这些功能对象,可以看下面的代码实现。

3.2.代码实现

假设现在有这样一个需求:提供一个消息发送事件,当系统中完成了某些业务流程时,触发这个时间给对应的负责人发送消息,现有一个更新会员的业务功能,在功能完成时推送更新完成消息,在更新异常时,推送更新失败消息。

我们可以通过3个步骤完成这个功能:

  • 定义消息发送事件:

    java 复制代码
    import org.springframework.context.ApplicationEvent;
    
    /**
     * 消息发送事件
     */
    public class MsgSendEvent extends ApplicationEvent {
        /**
         * 需要发送的消息
         */
        private String msg;
    
        public MsgSendEvent(Object source) {
            super(source);
        }
    
        public MsgSendEvent(Object source, String msg) {
            super(source);
            this.msg = msg;
        }
    
        public String getMsg() {
            return msg;
        }
    }
  • 定义事件监听器:

    java 复制代码
    import org.springframework.context.ApplicationListener;
    import org.springframework.stereotype.Component;
    
    /**
     * 消息发送事件监听器
     */
    @Component
    public class MsgEventSendListener implements ApplicationListener<MsgSendEvent> {
        @Override
        public void onApplicationEvent(MsgSendEvent event) {
            Object source = event.getSource();
            System.out.println("MsgEventSendListener-触发消息发送的被观察者:" + source + ",消息内容:" + event.getMsg());
        }
    }
  • 在业务对象中引入Publisher,并推送事件:

    java 复制代码
    import org.springframework.context.ApplicationEventPublisher;
    import org.springframework.context.ApplicationEventPublisherAware;
    import org.springframework.stereotype.Service;
    
    @Service
    public class MemberService implements ApplicationEventPublisherAware {
    
        private ApplicationEventPublisher publisher;
    
        @Override
        public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
            this.publisher = publisher;
        }
    
        public void batchUpdate() {
            try {
                System.out.println("MemberService-批量更新数据");
                publisher.publishEvent(new MsgSendEvent(this, "数据更新完成"));
            } catch (Exception e) {
                e.printStackTrace();
                publisher.publishEvent(new MsgSendEvent(this, "数据更新异常"));
            }
        }
    }

调用下memberService中的batchUpdate方法做个测试:

在上面的代码中,我们不需要再手动的注入或移除观察者 ,只需要在ApplicationListener<T>的泛型中写定义好的Event类,然后再业务对象中创建对应的Event对象,ApplicationEventMulticaster就可以根据Event的类型找到对应的观察者。

在我们的实际开发中,在Listener对象中可以拓展出更多的业务逻辑,例如将消息发送改成异步,通过envent传递标识执行不同的逻辑等等,可以根据自己的业务需求和功能设计做自由的组合。毕竟,观察者的存在目的只是为了与被观察者解耦,并不局限于只做一些简单的逻辑。

4.总结

本篇讲述了观察者模式的概念及在Java中的使用方式,可以在日常开放中参照上面的使用方式来处理业务。需要注意一个点就是观察者也可以作为另一个观察者的被观察者,可以无限套娃,但为了减少后续维护的难度,套娃尽可能不要超过两层。

除了使用之外,更重要的是理解观察者模式的设计思想,在软件工程中不管是本篇所说的观察者、监听器、事件 ,还是在其他地方使用到的发布/订阅、触发器等,其本质都是由一个角色完成某项功能时,向其他依赖它的对象发送了一个完成通知,这些依赖对象接收到通知之后再做出一些特定的操作,仅此而已,不用考虑的过于复杂。

总之,在对观察者模式有一定的理解之后,可以发现它也是一种比较简单的设计模式,希望本篇文章的内容对大家在日后的开发中能够有所帮助。

相关推荐
地瓜伯伯4 小时前
Nginx终极配置指南:负载均衡、限流、反向代理、IP白名单、SSL、云原生、DNS解析、缓存加速全都有
spring boot·nginx·spring·spring cloud·微服务·云原生·负载均衡
代码栈上的思考8 小时前
深入解析Spring IoC核心与关键注解
java·后端·spring
蓝瑟9 小时前
告别重复造轮子!业务组件多场景复用实战指南
前端·javascript·设计模式
It's now10 小时前
BeanRegistrar 的企业级应用场景及最佳实践
java·开发语言·spring
是一个Bug11 小时前
Spring事件监听器在电商订单系统中的应用
java·python·spring
Arva .12 小时前
讲一下 Spring 中用到的设计模式
java·spring·设计模式
enjoy编程13 小时前
Spring-AI 利用KeywordMetadataEnricher & SummaryMetadataEnricher 构建文本智能元数据
java·人工智能·spring
繁华似锦respect13 小时前
lambda表达式中的循环引用问题详解
java·开发语言·c++·单例模式·设计模式·哈希算法·散列表
雨中飘荡的记忆14 小时前
Spring Test详解
java·后端·spring