一个利用观察者模式和策略模式对代码进行解耦优化的例子

Scroll Down

#2019年12月30日更新,Spring自带的时间编程是同步的,如果你有异步的需求需要自己配置一下:

@Configuration
public class AsynchronousSpringEventsConfig {
    @Bean(name = "applicationEventMulticaster")
    public ApplicationEventMulticaster simpleApplicationEventMulticaster() {
        SimpleApplicationEventMulticaster eventMulticaster =
          new SimpleApplicationEventMulticaster();
         
        eventMulticaster.setTaskExecutor(new SimpleAsyncTaskExecutor());
        return eventMulticaster;
    }
}

参考

1. 前言

观察者模式(Observer Pattern):当一个对象被修改时,则会自动通知它的依赖对象。属于行为模式。(一对多模式适用)

策略模式(Strategy Pattern):一个类的行为或其算法可以在运行时更改。属于行为模式。

关于这两种设计模式详细的介绍,网上一搜一大把,不复赘述。

本文基于这两种设计模式,提供一个具体的业务场景作为实例,表述一种模块间解耦和优化的思路。

查看项目代码

2. 业务需求

在我们日常开发中,肯定会遇到需要做多平台分发推送的时候,你是做SMS的,那你肯定每天都在做消息质检的传递推送工作,你要是做电商的,你肯定得向用户推送物料情况诸如此类等等。

对于我们做MES系统的,管理层编派任务给工人,工人要能够实时收到推送。

推送这一块我们是使用的第三方平台,这一块我一开始直接写了个工具类,集成推送平台提供的SDK,在主业务里面直接引用对应SDK工具类就可以了。

后来老大给了我一些建议,说是要考虑将来平台的拓展,我目前的代码,将来如果要增加新的平台很可能就得在主业务逻辑里修改业务代码,不符合开闭原则,即使通过工具类进行解耦,将来有新的平台引入,修改推送工具类,本质上推送工具类在这里也不符合开闭原则,因此需要一个方案,能够将推送解耦,同时让系统更严格符合开闭原则

在这里,正如标题所说,利用设计模式,我们进行解耦。

3. 基于Spring提供的类的实现

3.1. 简介

springframework中提供了叫做ApplicationEventApplicationListener的类,这两个类用来实现bean与bean质检的通信机制,其实也就是spring提供的观察者模式实现。
ApplicationEvent有点类似被观察者,我们看下这个类的方法:

比较简单,我们着重关注一下这个类的构造方法,参数为source,文档中的解释大概意思是说这是一个event被初始化的地方或者关联的对象,不用纠结,其实就是在调用的地方用this

ApplicationListener 有点类似观察者,我们看下这个类的方法:

![image-20191012171644504](/Users/sq.ma/Library/Application Support/typora-user-images/image-20191012171644504.png)

这个类有且只有一个抽象方法,所以是一个函数式接口(functionInterface)。

下面我们用具体的代码来看一下这两个类怎么用。

3.2. 开始码代码

3.2.1. 创建一个枚举类PushPlatform

/**
 * 推送平台枚举
 *
 * @author sq.ma
 * @date 2019/10/12 下午5:50
 */
public enum PushPlatform {
    /**
     * 全平台
     */
    ALL_PLATFORM
}

这个枚举类用来标志推送到哪个平台(目前就一个,全平台)

3.2.2. 创建一个推送谓词接口(推送条件)PushPredicate

/**
 * 推送谓词(推送条件)
 * 
 * @author sq.ma
 * @date 2019/10/12 下午6:09
 */
@FunctionalInterface
public interface PushPredicate {

    /**
     * @return 推送平台
     */
    PushPlatform doPush();
}

这是一个函数式接口,后续可以通过lambda表达式简化代码。

后面推送的代码中,我们可以根据这个方法具体的回调实现不同的条件不同处理,也就是我们的策略模式

3.2.3. 创建一个ApplicationEvent的子类PushEvent

/**
 * 推送Event
 *
 * @author sq.ma
 * @date 2019/10/12 下午5:39
 */
@Getter
public class PushEvent extends ApplicationEvent {

    private PushPlatform platform;

    public PushEvent(Object source, PushPredicate predicate) {
        super(source);
        this.platform = predicate.doPush();
    }

}

3.2.4. 创建一个ApplicationListener的实现类PushListener

/**
 * 推送Listener
 *
 * @author sq.ma
 * @date 2019/10/12 下午6:14
 */
@Component
public class PushListener implements ApplicationListener<PushEvent> {

    @Override
    public void onApplicationEvent(PushEvent pushEvent) {
        PushPlatform platform = pushEvent.getPlatform();
        if (PushPlatform.ALL_PLATFORM.equals(platform)) {
            System.err.println("进行全平台推送");
        }
    }

}

这里如果想避免线程阻塞的话可以在方法或类上标志@Async注解

3.2.5. 主业务中使用

/**
 * 主业务
 *
 * @author sq.ma
 * @date 2019/10/14 上午11:27
 */
@Component
public class MainServiceA {

    /**
     * ApplicationContext be supported too
     * ApplicationEventPublisher recommended
     * because ApplicationEventPublisher is smaller range then ApplicationContext
     */
    @Resource
    ApplicationEventPublisher applicationEventPublisher;

    public void doMainService() {
        System.err.println("主业务执行中");
        applicationEventPublisher.publishEvent(new PushEvent(this, () -> PushPlatform.ALL_PLATFORM));
        System.err.println("主业务执行和完毕");
    }
}

借助于lambda表达式,我们可以更优雅的用一行解决问题。

这里要注意的是,ApplicationEventPublisher这个类也可以用他的子类ApplicationContext来代替,但是ApplicationContext比``ApplicationEventPublisher多更多功能,本着刚好服务于所需求功能,减少资源调度和增加安全的原则,更推荐用ApplicationEventPublisher`

3.2.6. 测试是否成功

@RunWith(SpringRunner.class)
@SpringBootTest
public class ObserverTrategyDemoApplicationTests {

    @Resource
    MainServiceA mainServiceA;

    @Test
    public void contextLoads() {
       	/**
         * Spring提供的方式
         */
        mainServiceA.doMainService();
        mainServiceA.doMainService();
    }

}

测试结果如下:

程序正确执行。

3.2.7 设计小结

利用Spring提供的ApplicationEventApplicationListener类我们实现了多种推送平台(例如极光推送,阿里推送等)和主业务的解耦,后续如果有新的平台引入,只需要写新的ApplicationListener的实现类就可以实现,而不需要改变原有的主业务逻辑代码和原有的推送平台代码,符合开闭原则(对修改关闭,对扩展开放),这一点基于我们的观察者模式。

通过PushPredicate接口,我们实现了不同目标推送平台(例如安卓,iOS等)条件不同的推送代码。其实这里就是将原有的if语句中的判断条件抽象出去,这样模块上更独立。这样的一个优势是后续如果有其他同事要使用这个模块只需要实现抽象方法,而对于本模块的维护者而言,他不需要关心主业务逻辑,只需要根据这个接口的结果进行不同目标推送平台处理就行,实现了与主业务之间的解耦。

PushPlatform这个枚举类主要是约束用户的数据格式,避免所谓的魔法值的出现。

这里其实还有个问题,调用推送的模块必须是交给Spring进行注入管理的bean,我们可以看到上面的主业务代码我们用了@Componet让交托给Spring进行创建管理,在测试类中我们使用的是Spring的依赖注入获得主业务bean。如果你尝试直接通过new一个实例的方式,你会得到一个空指针异常,原因是主业务中的this为null(没有上下文信息,姑且这么理解)。

4. 备选方案

  • 基于JDK自带的Observer相关类进行实现(JDK9+已过时)基于JDK自带的Observer相关类进行实现。需要说明的是,这个方法已经过时,jdk9以后更推荐用java.util.concurrent.Flow进行实现。

  • 基于JDK9以后的java.util.concurrent.Flow进行实现