Switch的优化方式

背景

switch/case 是Java的一个流程控制语法,作为多if/else条件判断的替代语法,平常用的还是很多的,毕竟比起看起来就很繁琐、难以阅读的if/else,switch/case更便于阅读理解一段代码的逻辑。

虽然switch、case很好用,我们用的也很多。但是,不知道你有没有见过这种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
switch(condition) {
case Condition.1:
doMethod1();
break;
case Condition.2:
doMethod2();
break;
case Condition.3:
doMethod3();
break;
case Condition.4:
doMethod4();
break;
case Condition.5:
doMethod5();
break;
case Condition.6:
doMethod6();
break;
case Condition.7:
doMethod7();
break;
case Condition.8:
doMethod8();
break;
default:
doDefault();
}

上面这段代码是一个比较典型的switch的用法,根据不同条件走不同分支的代码,执行相关的函数代码。相比较使用if/else,这种用法无疑简便了许多,因为无需对于每个分支条件去写条件判断,这样也可以尽量避免误操作写错条件判断。但是,switch并没有改变if/else产生的问题,if/else的代码比较复杂且难以维护,违反了开闭原则,switch也同样如此,只是switch看起来稍微清晰一些,然而在需求逐渐变化,判断条件增多的情况下,每新增一个条件都需要在这一段switch的代码中添加相应的条件。所以实际上,即使使用了switch,代码依旧会慢慢变得有些难以维护,同时实现方式也很不优雅。

开闭原则:对扩展开放,对修改关闭。说白了就是:扩展功能,可以。想改源码,不行!

优化方式

那有没有办法去优化这一大段的if/else或是switch代码吗?
答案肯定是有的。很简单,简单工厂模式+策略模式,就可以比较好的解决这个问题。

策略模式代替switch中的各逻辑代码

首先,switch下各分支的逻辑代码抽象出一个接口,然后把各分支的业务逻辑各自创建该接口的实现类。

1
2
3
4
5
6
7
public interface IDoFunc {

/**
* do func
*/
void doMethod();
}
1
2
3
4
5
6
7
public class Method1Do implements IDoFunc {
@Override
public void doMethod() {
// do something
System.out.println("do method 1");
}
}

单纯只靠策略模式的话,就只能做到这一步了,但是switch所产生的问题并没有解决,并且只是单纯地增加了代码量。接下来就要靠另一个方式来简化switch的流程控制逻辑的代码了。

简单工厂去除switch逻辑

工厂模式的定义这里就不作阐述了,这里只讲实现。
如果只是使用工厂模式为各实现类添加创建实例的方法是没有意义的,这里我们需要定义一个枚举和一个注解。
枚举+注解的作用是标记这个实现类对应的业务逻辑,通过枚举来区分业务逻辑,通过注解来标记业务逻辑的实现类。

1
2
3
4
5
6
7
8
9
public enum DoTypeEnum {
DO_METHOD1,
DO_METHOD2,
DO_METHOD3,
;

DoTypeEnum() {
}
}
1
2
3
4
5
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DoType {
DoTypeEnum value();
}

然后修改实现类,为实现类加上自定义注解。

1
2
3
4
5
6
7
8
9
@DoType(DoTypeEnum.DO_METHOD1)
@Service
public class Method1Do implements IDoFunc {
@Override
public void doMethod() {
// do something
System.out.println("do method 1");
}
}

接下来就是工厂类的实现了,这里借助spring的工具类来简化获取实现类的代码。
首先我们需要获取spring的applicationContext,便于后续根据枚举来获取实现类的实例化对象。然后根据我们的自定义注解获取容器中已经实例化好的实现类对象,并且在工厂类初始化的时候去加载至定义好的map中。这样,工厂类在创建实例时,就可以根据传入的枚举来决定创建(返回)哪个实现类了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 获取applicationContext的方式有好几种,这里采用实现ApplicationContextAware接口的方式。
@Component
public class SpringContextUtil implements ApplicationContextAware {

private ApplicationContext context;

public ApplicationContext getContext() {
return context;
}

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context = applicationContext;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component
public class DoFactory {

@Autowired
private SpringContextUtil springContextUtil;

private static Map<DoTypeEnum, IDoFunc> doMap = new ConcurrentHashMap<>();

@PostConstruct
public void init() {
Map<String, Object> beansMap = springContextUtil.getContext().getBeansWithAnnotation(DoType.class);
beansMap.values()
.forEach(item -> {
DoType doTypeAnnotation = item.getClass().getAnnotation(DoType.class);
doMap.put(doTypeAnnotation.value(), (IDoFunc) item);
});
}

public static IDoFunc createDoMethod(DoTypeEnum doTypeEnum) {
return doMap.get(doTypeEnum);
}
}

小结

上述代码的方式可以比较好的完成switch的优化,但是这样的重构方式也不是没有坏处。最麻烦的点就是这种方式会需要给每个策略都有一个实现类,所以虽然优化掉了这一大段的switch代码,但是取而代之的就是类的数量增多,代码量会增加。不过这个缺点可以尝试用函数式接口去尝试解决。