当前位置:网站首页>别再用if-else了,分享一下我使用“策略模式”的项目经验...
别再用if-else了,分享一下我使用“策略模式”的项目经验...
2022-08-03 12:00:00 【Java Punk】

“代码优化”是每个程序员都会经历的一个课题,提到优化,我最先想做的事就是干掉项目里的“if-else”。代码中,如果“if-else”比较多,不仅阅读起来比较困难,而且可维护性也会变差,如果后期在此基础上增加新的业务逻辑,会很容易产生出bug。
网上有很多名为:“别再if-else走天下了”,“教你干掉if-else”之类的文章,基本上都会提到策略模式的应用。我最开始也是跟着文章学习,用多了自然有了自己的理解。本讲,我就把使用“策略模式”优化“if-else”的经验历程分享一下。
正文
“策略模式”核心就在于“策略”,实现的方式也大同小异:服务端定义统一行为(接口或抽象类),并实现不同策略下的处理逻辑(对应实现类)。更进一步的话,可以利用工厂类来统一处理客户端逻辑。
“策略模式”的UML类图如下:

接下来,我用实践中常遇到的业务场景,深入浅出的讲解一下:
假如,对于不同来源(pc端、mobile端)的支付订单需要不同的逻辑处理。通常,项目中会有一个实体类 Order 承载变量,还有一个专用的 OrderService 来处理该类订单业务,方法里面有一坨 if-else 的逻辑,目的是根据订单的来源的做不同的处理,如下:
/**
* 支付订单的实体类
**/
@Data
public class Order {
// 订单来源
private String source;
// 支付方式
private String payWay;
// 订单编号
private String orderCode;
// 订单金额
private BigDecimal amount;
// ... 其他字段
}
/**
* 处理支付订单的服务
* 为了简化代码,处理订单的服务没有使用接口设计
**/
@Service
public class OrderService {
public void orderMethod(Order order) {
if(order.getSource().equals("pc")){
// 处理pc端订单的逻辑
}else if(order.getSource().equals("mobile")){
// 处理移动端订单的逻辑
}else {
// ...其他逻辑
}
}
}“策略模式”就是要干掉上面的一坨if-else,使得代码看起来优雅且高大上,分析下以下几种方案:
一、策略+工厂
这种方案是 “策略模式”最好的诠释,完全按照UML图来,先往下看,一会再说缺点。
1. 定义OrderHandler接口
首先定义一个OrderHandler接口,此接口规定了处理订单的方法。
public interface OrderHandler {
void handle(Order order);
}2. 定义PC和Mobile实现类
接下来,就是实现pc端和移动端订单处理各自的handler,重写各自的处理逻辑,互补干扰。
@Service
public class MobileOrderHandler implements OrderHandler {
@Override
public void handle(Order order) {
System.out.println("处理移动端订单");
}
}
@Service
public class PCOrderHandler implements OrderHandler {
@Override
public void handle(Order order) {
System.out.println("处理PC端订单");
}
}3. 定义工厂类
Order工厂类的取代了“if-else”的操作:根据不同的参数,匹配不同的实体类,以便执行不同的业务逻辑。
public class OrderServiceFactory {
private static final Map<String, IOrderHandler> map = new HashMap<>();
static {
map.put("pc", new PCOrderHandler());
map.put("mobile", new MobileOrderHandler());
}
// 对外提供的服务
public static IOrderHandler getOrderService(String source) {
return map.get(source);
}
}4. Junit测试
用 Junit 测试类,简单模拟一下Controllor层的业务逻辑。
@Test
void payWayTest() {
Order order = new Order();
order.setSource("pc");
OrderHandler orderHandler = OrderServiceFactory.getOrderService(order.getSource());
orderHandler.handle(order);
}
// 输出:处理PC端订单二、策略+Enum
有没有发现一个问题:开发人员不仅要关注策略实现类的业务,还要在 OrderServiceFactory 工厂类中把用到的对象先配置出来,就好像是在维护一个配置文件。
static {
map.put("pc", new PCOrderHandler());
map.put("mobile", new MobileOrderHandler());
}这种操作实际上是有悖于提升代码可读性、可维护性的。所以,我优先想到的就是用Enum取代工厂类,毕竟Enum的可读性更好。
1. 定义Enum
@Getter
public enum OrderServiceEnum {
PC("pc", new PCOrderHandler()),
MOBILE("mobile", new MobileOrderHandler());
private String name;
private OrderHandler handler;
OrderServiceEnum(String name, OrderHandler handler) {
this.name = name;
this.handler = handler;
}
// 定义public的获取方式
public static OrderHandler getEnum(String name) {
for (OrderServiceEnum e : OrderServiceEnum.values()) {
if (e.getName().equals(name)) {
return e.getHandler();
}
}
return null;
}
}2. Junit测试
用 Junit 测试类,简单模拟一下Controllor层的业务逻辑。
@Test
void enumTest() {
Order order = new Order();
order.setSource("pc");
OrderHandler orderHandler = OrderServiceEnum.getEnum(order.getSource());
orderHandler.handle(order);
}
// 输出:处理PC端订单三、注解实现
虽然使用Enum对可读性有所改进,但是还没有达到预期,我在考虑有没有一种可能:工厂类自动加载所有的策略实现,不需要外界干预。日后增加产品族时,只需要关注实现类本身,不需要修改工厂类代码。
我想到了“反射”,想到了“注解”,想到了利用“反射”自动加载所有实现类的“注解”,说干就干:
1. 定义注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Service
public @interface OrderHandlerType {
String source();
}2. 实现类上加注解
@Service
@OrderHandlerType(source = "mobile")
public class MobileOrderHandler implements OrderHandler {
@Override
public void handle(Order order) {
System.out.println("处理移动端订单");
}
}
@Service
@OrderHandlerType(source = "pc")
public class PCOrderHandler implements OrderHandler {
@Override
public void handle(Order order) {
System.out.println("处理PC端订单");
}
}3. 定义工厂类
利用反射,自动获取 OrderHandler 接口下所有实现类,并且识别实现类注解上的 value 值,完成 map 自动装配,从此避免认为干涉。
@Component
public class OrderServiceFactory {
private static Map<String, OrderHandler> map = new HashMap<>();
static {
// 1.利用反射,获取IMedalService接口下所有实现类的class
List<Class> clazzs = getAllInterfaceAchieveClass(OrderHandler.class);
for(Class clazz : clazzs){
// 2.获取class的MedalType注解的value值
String type = AnnotationUtils.findAnnotation(clazz, OrderHandlerType.class).source();
try {
// 3.保存产品族为map对象
map.put(type, (OrderHandler) clazz.newInstance());
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
/**
* 对外提供的服务
* @return
*/
public static OrderHandler getOrderService(String medalType) {
return map.get(medalType);
}
/**
* 获取所有接口的实现类
*/
public static List<Class> getAllInterfaceAchieveClass(Class clazz){
ArrayList<Class> list = new ArrayList<>();
// 判断是否是接口
if (clazz.isInterface()) {
try {
ArrayList<Class> allClass = getAllClassByPath(clazz.getPackage().getName());
/**
* 循环判断路径下的所有类是否实现了指定的接口
* 并且排除接口类自己
*/
for (int i = 0; i < allClass.size(); i++) {
// 排除抽象类
if(Modifier.isAbstract(allClass.get(i).getModifiers())){
continue;
}
// 判断是不是同一个接口
if (clazz.isAssignableFrom(allClass.get(i))) {
if (!clazz.equals(allClass.get(i))) {
list.add(allClass.get(i));
}
}
}
} catch (Exception e) {
System.out.println("出现异常");
}
}
return list;
}
/**
* 从指定路径下获取所有类
*/
public static ArrayList<Class> getAllClassByPath(String packagename){
ArrayList<Class> list = new ArrayList<>();
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
String path = packagename.replace('.', '/');
try {
ArrayList<File> fileList = new ArrayList<>();
Enumeration<URL> enumeration = classLoader.getResources(path);
while (enumeration.hasMoreElements()) {
URL url = enumeration.nextElement();
fileList.add(new File(url.getFile()));
}
for (int i = 0; i < fileList.size(); i++) {
list.addAll(findClass(fileList.get(i),packagename));
}
} catch (IOException e) {
e.printStackTrace();
}
return list;
}
/**
* 如果file是文件夹,则递归调用findClass方法,或者文件夹下的类
* 如果file本身是类文件,则加入list中进行保存,并返回
*/
private static ArrayList<Class> findClass(File file,String packagename) {
ArrayList<Class> list = new ArrayList<>();
if (!file.exists()) {
return list;
}
File[] files = file.listFiles();
for (File file2 : files) {
if (file2.isDirectory()) {
// 添加断言用于判断
assert !file2.getName().contains(".");
ArrayList<Class> arrayList = findClass(file2, packagename+"."+file2.getName());
list.addAll(arrayList);
}else if(file2.getName().endsWith(".class")){
try {
// 保存的类文件不需要后缀.class
list.add(Class.forName(packagename + '.' + file2.getName().substring(0,
file2.getName().length()-6)));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
return list;
}
}4. 测试
@Test
void orderTest() {
Order order = new Order();
order.setSource("pc");
OrderHandler orderHandler = OrderServiceFactory.getOrderService(order.getSource());
orderHandler.handle(order);
}
// 输出:处理PC端订单总结
使用“注解”的策略模式,可以让代码维护变的更简洁,工厂类可以封装起来,开发过程中,只需要关注策略实现就可以了。
说了这么多,难道使用“策略模式”就没有缺点吗?其实,缺点也很明显:
- 首先,“策略模式”虽然节省了“if-else”部分的代码,但是会产生很多的策略类,每个具体策略类都会产生一个新类,这无疑是变相的增加了代码的维护成本。
- 其次,如果“策略模式”封装的是多种算法逻辑,那就意味着客户端必须知道所有的策略类,理解这些算法的区别,然后自行决定使用哪一个策略类。
边栏推荐
猜你喜欢

第4章 搭建网络库&Room缓存框架

《数字经济全景白皮书》金融数字用户篇 重磅发布!

基于Sikuli GUI图像识别框架的PC客户端自动化测试实践

87.(cesium之家)cesium热力图(贴地形)

劝退背后。

mysql advanced (twenty-four) method summary of defense against SQL injection

第四周学习 HybridSN,MobileNet V1,V2,V3,SENet

What knowledge points do you need to master to learn software testing?

"Digital Economy Panorama White Paper" Financial Digital User Chapter released!

3年软件测试经验,不懂自动化基础...不知道我这种测试人员是不是要被淘汰了?
随机推荐
《数字经济全景白皮书》金融数字用户篇 重磅发布!
JUC(三):锁核心类AQS ing
4500 words sum up, a software test engineer need to master the skill books
Traceback (most recent call last): File
Take you understand the principle of CDN technology
【JS 逆向百例】某网站加速乐 Cookie 混淆逆向详解
dataset数据集有哪些_数据集类型
87.(cesium之家)cesium热力图(贴地形)
LyScript implements memory stack scanning
基于Sikuli GUI图像识别框架的PC客户端自动化测试实践
5个超好用手机开源自动化工具,哪个适合你?
What knowledge points do you need to master to learn software testing?
Explain the virtual machine in detail!JD.com produced HotSpot VM source code analysis notes (with complete source code)
LyScript 实现对内存堆栈扫描
长城简漫·暑期安全篇⑤ 这个强,不能逞
零拷贝、MMAP、堆外内存,傻傻搞不明白...
直播弱网优化
数据库系统原理与应用教程(073)—— MySQL 练习题:操作题 131-140(十七):综合练习
PC client automation testing practice based on Sikuli GUI image recognition framework
记住用户名案例(js)