使用模板方法模式动态绑定多实现类实例

摘要

  摘要: 由于业务场景复杂,一个算法需要开发行为变化多端的多个实现类,然后在系统运行时根据不同场景装载不同的类实例。为了使源码具有更好的可扩展性和可重用性,在借鉴前人处理方法的基础上,介绍在Spring项目中,基于模板方法模式介绍一个接口被多个实现类实现时,Spring框架怎样从容器中正确取出我们想要的实例。

前言

  除了《Spring注解之@Autowired:按类型自动装配Bean到数组、集合和Map》介绍的几种注入bean方法之外,还有其它方法,本文绍如何使用模板方法模式装配 bean。

  在阎宏博士的《JAVA与模式》一书中开头是这样描述模板方法模式(Template Method Pattern)的:

  模板方法模式是类的行为模式。准备一个抽象类,将部分逻辑以具体方法以及具体构造函数的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法(类的多态实现),从而对剩余的逻辑有不同的实现。这就是模板方法模式的用意。

  欲了解更多相关知识点请移步《Spring 动态绑定多实现类实例综述》。

业务场景回顾

  需求描述:定制一个绘图工具,她根据客户端发送的指令可以画出正方形、矩形、圆形和三角形等各种各样的几何图形。例如,当客户端需要绘制三角形的时候,就调用绘制三角形的方法;当需要绘制圆形的时候,就调用绘制圆形的方法。

模板方法模式中的方法

  模板方法中的方法可以分为两大类:模板方法和基本方法。
 

模板方法

  一个模板方法是定义在抽象类中的,把基本操作方法组合在一起形成一个总算法或一个总行为的方法。

  一个抽象类可以有任意多个模板方法,而不限于一个。每一个模板方法都可以调用任意多个具体方法。

基本方法

 基本方法又可以分为三种:抽象方法(Abstract Method)、具体方法(Concrete Method)和钩子方法(Hook Method)。
  ● 抽象方法:一个抽象方法由抽象类声明,由具体子类实现。在Java语言里抽象方法由关键字abstract修饰。
  ● 具体方法:一个具体方法由抽象类声明并实现,而子类并不实现或覆盖。
  ● 钩子方法:一个钩子方法由抽象类声明,而子类负责实现。

public abstract class ShapeTem {
    //模板方法
    public final void getTime() {
        long start = System.currentTimeMillis();
        draw();
        System.out.println("耗时 " + (System.currentTimeMillis()  - start) + " 毫秒");
    }
    /**
     * 画图实现方法
     */
    public abstract void draw();
    /**
     * 基本方法(空方法)<br/>
     * 检验实现类是否支持 beanName 指定的画图方式
     * @param beanName 画图工具具体实现类的 bean 名称
     * @return
     */
    public abstract Boolean hook(String beanName);
}

  抽象类ShapeTem中的模板方法实际上是提供了一个外部可访问接口,使得外部环境由该接口获得服务;之所以为模板方法添加修饰符 final,是因为这样它就不会被重写,避免被恶意操作。在每个具体模板角色类中实现抽象类所声明的两个基本方法,下面以画三角形和长方形为例进行说明,圆形和正方形等的实现类请自行添加:

@Component
public class TriangleTem extends ShapeTem {
    /**
     * 画图实现方法
     */
    @Override
    public void draw() {
        System.out.println("Inside Triangle::draw() method.");
    }

    /**
     * 基本方法<br/>
     * 检验实现类是否支持 beanName 指定的画图方式
     *
     * @param beanName 画图工具具体实现类的 bean 名称
     * @return
     */
    @Override
    public Boolean hook(String beanName) {
        return "triangleTem".equals(beanName);
    }
}
@Component
public class RectangleTem extends ShapeTem {
    /**
     * 画图实现方法
     */
    @Override
    public void draw() {
        System.out.println("Inside Rectangle::draw() method.");
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 基本方法<br/>
     * 检验实现类是否支持 beanName 指定的画图方式
     *
     * @param beanName 画图工具具体实现类的 bean 名称
     * @return
     */
    @Override
    public Boolean hook(String beanName) {
        return "rectangleTem".equals(beanName);
    }
}

  客户端可根据需要使用不同的模板实现。新增 ShapeTemplate 类,通过 applicationContext 获取接口不同实现类的bean,并由方法 toDraw(String beanName)根据beanName确认画图模板,最终找到画图的具体方法。

import com.eg.wiener.service.ShapeTem;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * 模板方法模式
 *
 * @author Wiener
 * @date 2021/1/18
 */
@Component
public class ShapeTemplate implements ApplicationContextAware, InitializingBean {

    private ApplicationContext applicationContext;
    /**
     * 1610543593
     * 通过 applicationContext 获取抽象类的bean,和通过@Autowired注解的方式获取接口实现类的bean有着异曲同工之妙
     */
    private List<ShapeTem> shapeList = null;

    @Autowired
    private Map<String, ShapeTem> shapeTemMap;
    
    @Override
    public void afterPropertiesSet() throws Exception {
        if (shapeList == null) {
            shapeList = new ArrayList<>();
            Map<String, ShapeTem> beansOfType = applicationContext.getBeansOfType(ShapeTem.class);
            beansOfType.forEach((key, value) -> shapeList.add(value));
        }
        if (CollectionUtils.isEmpty(shapeList)) {
            System.out.println("--------- 获取抽象类的bean失败 --------------");
        }
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    public void toDraw(String beanName) {
        System.out.println("通过 @Autowired 注解的方式获取实现类:" + shapeTemMap);
        System.out.println("通过 ApplicationContext 的方式获取实现类:" + shapeList);
        for (ShapeTem oneShape : shapeList) {
            if (oneShape.hook(beanName)) {
                oneShape.getTime();
                return;
            }
        }
        System.out.println("beanName非法!");
    }
}

  有了上面的代码之后,我们在控制层创建测试用例来看一下效果,测试代码片段如下:

    /**
     * 由模板方法模式实现
     * @param code
     * @return
     */
    @GetMapping("/drawByTemplate")
    public String drawByTemplate(String code) {

        shapeTemplate.toDraw(code);
        return "由模板方法模式实现成功";
    }

    执行结果:

通过 @Autowired 注解的方式获取实现类:{rectangleTem=com.eg.wiener.service.impl.RectangleTem@1f6ce7aa, triangleTem=com.eg.wiener.service.impl.TriangleTem@11ab1a37}
通过 ApplicationContext 的方式获取实现类:[com.eg.wiener.service.impl.RectangleTem@1f6ce7aa, com.eg.wiener.service.impl.TriangleTem@11ab1a37]
Inside Rectangle::draw() method.
耗时 300 毫秒

服务端根据不同的请求参数code,调用不同的示例。

    执行结果分析

  1. 通过两种方式所得到的实现类的bean一模一样,说明二者有着异曲同工之妙。但是,通过注解的方法获取时,实现逻辑更精简。
  2. 通过 applicationContext 获取实现类的bean后,我们把这些bean放入集合shapeList。在客户端画图的时候,如果传入的参数beanName跟自己定义的一样,则调用当前画图类实例的draw方法。

    当增加一个新的实现类时,不要按照控制流程的思路去思考问题,而应当按照“责任”的思路去规划。换言之,应当考虑哪些操作是新的实现类必须置换掉的,哪些操作是可以置换掉的,以及哪些操作是不可以置换掉的。使用模板方法设计模式可以使这些责任变得清晰。

结束语

    这篇文章主要向大家介绍如何使用模板方法模式实现多个接口,就算每个实现类的逻辑变化多端,只要执行流程是固定的,使用该模式都能让我们的编码效率、维护效率和扩展效率更上一层楼。

应用实例 做试卷时,考生题目都是一样的,只是答案千差万别。
  

Reference

posted @ 2021-02-08 20:37  楼兰胡杨  阅读(1468)  评论(0编辑  收藏  举报