使用模板方法模式动态绑定多实现类实例
摘要
摘要: 由于业务场景复杂,一个算法需要开发行为变化多端的多个实现类,然后在系统运行时根据不同场景装载不同的类实例。为了使源码具有更好的可扩展性和可重用性,在借鉴前人处理方法的基础上,介绍在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,调用不同的示例。
执行结果分析
- 通过两种方式所得到的实现类的bean一模一样,说明二者有着异曲同工之妙。但是,通过注解的方法获取时,实现逻辑更精简。
- 通过 applicationContext 获取实现类的bean后,我们把这些bean放入集合shapeList。在客户端画图的时候,如果传入的参数beanName跟自己定义的一样,则调用当前画图类实例的draw方法。
当增加一个新的实现类时,不要按照控制流程的思路去思考问题,而应当按照“责任”的思路去规划。换言之,应当考虑哪些操作是新的实现类必须置换掉的,哪些操作是可以置换掉的,以及哪些操作是不可以置换掉的。使用模板方法设计模式可以使这些责任变得清晰。
结束语
这篇文章主要向大家介绍如何使用模板方法模式实现多个接口,就算每个实现类的逻辑变化多端,只要执行流程是固定的,使用该模式都能让我们的编码效率、维护效率和扩展效率更上一层楼。
应用实例 做试卷时,考生题目都是一样的,只是答案千差万别。