【设计模式】循序渐进的理解观察者模式&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.观察者模式的通用代码实现

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

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

/**
 * 观察者接口
 */
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);
    }
}

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

/**
 * 被观察者抽象类
 */
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方法做个测试

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个步骤完成这个功能:

  • 定义消息发送事件:
    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;
        }
    }
    
  • 定义事件监听器:
    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,并推送事件:
    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中的使用方式,可以在日常开放中参照上面的使用方式来处理业务。需要注意一个点就是观察者也可以作为另一个观察者的被观察者,可以无限套娃,但为了减少后续维护的难度,套娃尽可能不要超过两层。

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

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

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

挥之以墨

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值