Java设计模式之策略模式学习笔记demo
基于spring boot的策略模式demo
简单的说:策略模式可以根据上下文对象的不同状态去执行不同的逻辑,即当业务处理的时候,出现了if else三次以上的判断,代码可读性就会很差,即使使用switch语句去调整,也没有使用策略模式更清晰易读。
策略模式角色
Strategy:抽象策略角色,对算法,策略的抽象,定义每个算法的所必须的方法, 通常定义为接口。
ConcreteStrategy:具体策略角色,实现抽象策略角色的接口,完成具体的算法。
Context:上下文对象,负责保存ConcreteStrategy的引用。
案例demo
虚拟一个业务需求,让大家容易理解。假设有一个订单系统,里面的一个功能是根据订单的不同类型作出不同的处理
引入pom
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--Lombok插件-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--MySQL-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.21</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!--使用guava中的Maps集合和判断-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>19.0</version>
</dependency>
<build>
<!--当Mapper接口和Mapper文件在同一个文件夹下,配置此读取Mapper文件-->
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*</include>
</includes>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
定义策略接口
/**
* @ClassName AbstractHandler
* @Description 抽象处理器
* @Author wlk
* @Date 2020/11/29 14:27
*/
public abstract class AbstractHandler {
public abstract String handle(OrderDTO order);
}
具体的处理器类
/**
* @ClassName NormalHandler
* @Description 具体订单处理器
* @Author wlk
* @Date 2020/11/29 14:28
*/
@Component
@HandlerType("1")
public class NormalHandler extends AbstractHandler {
@Override
public String handle(OrderDTO order) {
return "处理普通订单";
}
}
/**
* @ClassName NormalHandler
* @Description 具体订单处理器
* @Author wlk
* @Date 2020/11/29 14:28
*/
@Component
@HandlerType("2")
public class GroupHandler extends AbstractHandler {
@Override
public String handle(OrderDTO order) {
return "处理团购订单";
}
}
/**
* @ClassName NormalHandler
* @Description 具体订单处理器
* @Author wlk
* @Date 2020/11/29 14:28
*/
@Component
@HandlerType("3")
public class PromotionHandler extends AbstractHandler {
@Override
public String handle(OrderDTO order) {
return "处理促销订单";
}
}
此处我是定义了自定义注解HandlerType,主要是后续根据注解获取具体的处理类对象
使用Component注解是将三个处理器交给spring容器管理
service层的接口和实现
没啥好解释的,直接贴出代码
/**
* @ClassName OrderServiceImpl
* @Description TODO
* @Author wlk
* @Date 2020/11/29 14:18
*/
@Service
public class OrderServiceImpl implements IOrderService {
/**
* 传统实现方式:根据订单的类型写一大堆的if else
* @param order 订单实体
* @return
*/
// @Override
// public String handle(OrderDTO order) {
//
// String type = order.getType();
// if (type.equals("1")){
// return "处理普通订单";
// }else if (type.equals("2")){
// return "处理团购订单";
// }else if (type.equals("3")){
// return "处理促销订单";
// }
// return null;
// }
@Autowired
private HandlerContext context;
@Override
public String handle(OrderDTO order){
AbstractHandler handler = context.getInstance(order.getType());
return handler.handle(order);
}
}
/**
* @ClassName IOrderService
* @Description TODO
* @Author wlk
* @Date 2020/11/29 14:17
*/
public interface IOrderService {
/**
* 根据订单的不同类型做出不同的处理
* @param order 订单实体
* @return 简单处理 返回字符串
*/
String handle(OrderDTO order);
}
创建上下文对象HandlerContext
/**
* @ClassName HandlerContext
* @Description 处理器上下文,用来保存不同的业务处理器
* @Author wlk
* @Date 2020/11/29 14:25
*/
public class HandlerContext {
private Map<String,Class> handlerMap;
public HandlerContext(Map<String, Class> handlerMap) {
this.handlerMap = handlerMap;
}
/**
* 根据订单类型获取不同的处理器实例
* 方法根据类型获取对应的class,然后根据class类型获取注册到spring中的bean
* @param type
* @return
*/
public AbstractHandler getInstance(String type) {
Class clazz = handlerMap.get(type);
if (clazz == null){
throw new IllegalArgumentException("not found handler for type:"+type);
}
return (AbstractHandler) BeanTool.getBean(clazz);
}
}
创建上下文对象,在springboot启动的时候,将带有@HandlerType注解的类放入上下文对象中
注意:此处不使用@Component注解,是因为在后续的Processor中会将此Context交给spring容器,如果此处使用了@Component注解,启动的时候会报错
自定义注解和抽象处理器都很简单,那么如何将处理器注册到spring容器中呢?
具体思路是:
1、扫描指定包中标有@HandlerType的类;
2、将注解中的类型值作为key,对应的类作为value,保存在Map中;
3、以上面的map作为构造函数参数,初始化HandlerContext,将其注册到spring容器中;
我们将核心的功能封装在HandlerProcessor类中,完成上面的功能。
HandlerProcessor:
HandlerProcessor实现BeanFactoryPostProcessor
/**
* @ClassName HandlerProcessor
* @Description HandlerProcessor需要实现BeanFactoryPostProcessor,在spring处理bean前,将自定义的bean注册到容器中
* @Author wlk
* @Date 2020/11/29 14:39
*/
@Component
public class HandlerProcessor implements BeanFactoryPostProcessor {
private static final String HANDLER_PACKAGE = "com.javaboy.securitydb.strategy.handler.impl";
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
HashMap<String, Class> handlerMap = Maps.newHashMapWithExpectedSize(3);
//扫描包路径下的处理器类
List<Class> classList = ClassScanner.getClasssFromPackage(HANDLER_PACKAGE);
classList.forEach(clazz -> {
String type = ((HandlerType)clazz.getAnnotation(HandlerType.class)).value();
//以type为key,以具体处理器类为value存在上下问的map中
handlerMap.put(type,clazz);
});
HandlerContext handlerContext = new HandlerContext(handlerMap);
beanFactory.registerSingleton(HandlerContext.class.getName(),handlerContext);
}
}
在这个Processor中,主要是在spring启动的时候,扫描指定包路径下的具体处理器类,然后将带有自定义注解的类和自定义注解的value映射成map,然后将map存到context上下文中。
此处想说下BeanFactoryPostProcessor这个接口和registerSingleton方法
自定义注解
/**
* @ClassName HandlerType
* @Description 自定义注解@HandlerType,用于标识该处理器对应哪个订单类型
* @Author wlk
* @Date 2020/11/29 14:31
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface HandlerType {
String value();
}
工具类
/**
* @ClassName BeanTool
* @Description BeanTool:获取bean工具类
* @Author wlk
* @Date 2020/11/29 14:38
*/
@Component
public class BeanTool {
private static ApplicationContext applicationContext;
//获取上下文
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
//设置上下文
public static void setApplicationContext(ApplicationContext applicationContext) {
BeanTool.applicationContext = applicationContext;
}
//通过名字获取上下文中的bean
public static Object getBean(String name){
return applicationContext.getBean(name);
}
//通过类型获取上下文中的bean
public static Object getBean(Class<?> requiredType){
return applicationContext.getBean(requiredType);
}
}
/**
* @ClassName ClassScanner
* @Description 根据路径扫描类
* @Author wlk
* @Date 2020/11/29 14:43
*/
public class ClassScanner {
public static void main(String[] args) {
List<Class> classList = getClasssFromPackage("com.javaboy.securitydb.strategy.handler");
classList.forEach(System.out::println);
}
/**
* 获得包下面的所有的class
*
* @param pack
* package完整名称
* @return List包含所有class的实例
*/
public static List<Class> getClasssFromPackage(String pack) {
List<Class> clazzs = new ArrayList<Class>();
// 是否循环搜索子包
boolean recursive = true;
// 包名字
String packageName = pack;
// 包名对应的路径名称
String packageDirName = packageName.replace('.', '/');
Enumeration<URL> dirs;
try {
dirs = Thread.currentThread().getContextClassLoader().getResources(packageDirName);
while (dirs.hasMoreElements()) {
URL url = dirs.nextElement();
String protocol = url.getProtocol();
if ("file".equals(protocol)) {
System.out.println("file类型的扫描");
String filePath = URLDecoder.decode(url.getFile(), "UTF-8");
findClassInPackageByFile(packageName, filePath, recursive, clazzs);
} else if ("jar".equals(protocol)) {
System.out.println("jar类型的扫描");
}
}
} catch (Exception e) {
e.printStackTrace();
}
return clazzs;
}
/**
* 在package对应的路径下找到所有的class
* @param packageName package名称
* @param filePath package对应的路径
* @param recursive 是否查找子package
* @param clazzs 找到class以后存放的集合
*/
public static void findClassInPackageByFile(String packageName, String filePath, final boolean recursive, List<Class> clazzs) {
File dir = new File(filePath);
if (!dir.exists() || !dir.isDirectory()) {
return;
}
// 在给定的目录下找到所有的文件,并且进行条件过滤
File[] dirFiles = dir.listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
boolean acceptDir = recursive && file.isDirectory();// 接受dir目录
boolean acceptClass = file.getName().endsWith("class");// 接受class文件
return acceptDir || acceptClass;
}
});
for (File file : dirFiles) {
if (file.isDirectory()) {
findClassInPackageByFile(packageName + "." + file.getName(), file.getAbsolutePath(), recursive, clazzs);
} else {
String className = file.getName().substring(0, file.getName().length() - 6);
try {
clazzs.add(Thread.currentThread().getContextClassLoader().loadClass(packageName + "." + className));
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
在启动类中,将context加载到工具类,以便获取bean实例
@SpringBootApplication
public class SecurityDbApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SecurityDbApplication.class, args);
BeanTool.setApplicationContext(context);
}
}
测试一波
@GetMapping("/hello")
public String hello(@PathParam("type") String type){
OrderDTO order = new OrderDTO();
order.setType(type);
String handle = orderService.handle(order);
System.out.println(handle);
return "hello javaboy"+user.getUsername()+handle;
}
http://localhost:8080/hello?type=3
总结
- HandlerProcessor需要实现BeanFactoryPostProcessor,在spring处理bean前,将自定义的bean注册到容器中。
- 注意一点,HandlerProcessor和BeanTool必须能被扫描到,或者通过@Bean的方式显式的注册,才能在项目启动时发挥作用。
- 每个处理器都必须添加到spring容器中,因此需要加上@Component注解,其次需要加上一个自定义注解@HandlerType,用于标识该处理器对应哪个订单类型,最后就是继承AbstractHandler,实现自己的业务逻辑。
- 自定义注解和抽象处理器都很简单,那么如何将处理器注册到spring容器中呢?
具体思路是:
1、扫描指定包中标有@HandlerType的类;
2、将注解中的类型值作为key,对应的类作为value,保存在Map中;
3、以上面的map作为构造函数参数,初始化HandlerContext,将其注册到spring容器中;
我们将核心的功能封装在HandlerProcessor类中,完成上面的功能。
利用策略模式可以简化繁杂的if else代码,方便维护,而利用自定义注解和自注册的方式,可以方便应对需求的变更。本文只是提供一个大致的思路,还有很多细节可以灵活变化,例如使用枚举类型、或者静态常量,作为订单的类型,相信你能想到更多更好的方法。
参考文章:https://zhuanlan.zhihu.com/p/153530147