⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 c1n.cn/LXJFJ 「冬天里的懒猫」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

问题提出

对于很多码农而言,if-else 可能是最高频的代码关键字,毕竟,这也比较符合人们二维思考问题的方式,试想大部分问题的答案都是只有两个维度,要么 true,要么 false,那么通过 if-else 的方式是再好不过了。

当然,if-else 固然好,但是在代码中过多的使用,或者反复的嵌套使用,那样就不好了。

前几天看到了下面这张图,固然这张图比较夸张,但是也说明了,多重嵌套的 if-else 的不可取之处。

今天本文就来聊聊,在 Java 中,面对已经出现了的多重 if-else 嵌套的情况,我们应该怎么去优化。

考虑到要优化 if,else 的方案,那么现在正好手头上有一个具体的实例代码,在 netty 的自定义协议栈中,在 netty 收到消息之后的 ByteToMessageDecoder 中,将收到的二进制消息,转换为所需要的实体对象。

if(in.readInt() == 1) {
//转换为TankJoinMsg对象

}else if(in.readInt() == 2) {
//转换为TankStartMovingMsg对象

}else if(in.readInt() == 3) {
//转换为TankStopMsg对象

}else if(in.readInt() == 4) {
//转换为TankDirChangedMsg对象

}else if(in.readInt() == 5) {
//转换为BulletNew对象

}

代码结构如上所示,现在需要在 channel 中对传入的第一个 int 字段进行判断,根据这个字段的值,来确定传入的数据类型,之后将后续的字节流转换为所需要的实体对象。这个过程非常 low。

用 switch-case 优化

鉴于 if-else 的控制逻辑的冗余性,如果 if-else 的分支间不存在关联性,那么首先想到的解决方案是通过 switch-case。对于本文的问题,可以定义一个枚举类 MsgType,然后用 switch-case 来解决。

如下是定义的枚举类:

public enum  MsgType {
TankJoin,TankDirChanged,TankStop,TankStartMoving,BulletNew,TankDie,TankExit
}

之前的 if-else 代码被优化为:

MsgType msgType = MsgType.values()[in.readInt()];
int length = in.readInt();
if (in.readableBytes() < length) {
in.resetReaderIndex();
return;
}
byte[] bytes = new byte[length];
in.readBytes(bytes);

Msg msg = null;
switch (msgType) {
case TankJoin:
msg = new TankJoinMsg();
msg.parse(bytes);
out.add(msg);
break;
case TankStartMoving:
msg = new TankStartMovingMsg();
msg.parse(bytes);
out.add(msg);
break;
case TankStop:
msg = new TankStopMsg();
msg.parse(bytes);
out.add(msg);
break;
case TankDirChanged:
msg = new TankDirChangedMsg();
msg.parse(bytes);
out.add(msg);
break;
case BulletNew:
msg = new BulletNewMsg();
msg.parse(bytes);
out.add(msg);
break;
case TankDie:
msg = new TankDieMsg();
msg.parse(bytes);
out.add(msg);
break;
case TankExit:
msg = new TankExitMsg();
msg.parse(bytes);
out.add(msg);
break;
default:
break;
}

这样就能很好的将 if 转换为了 switch-case 的方式。但是需要注意的是,并不是全部的 if-esle 的复杂逻辑都能转换为 switch-case,只有 if 的分支处于并列关系,且分支逻辑间没有什么关联性的情况才适用这种情况。

此外,上述 switch-case 方法还存在一个问题就是,一旦增加了新的消息类型,那么就需要不断的修改这个类的代码进行扩展。这在设计上来说不是一个很好的设计。这就不满足开闭原则。

用反射替换 switch-case

对于上述 switch-case 逻辑,我们可以看到,是存在一定的规律的,我们定义的消息类型,如 TankJoin,则会处理为 TankJoinMsg 对象来进行处理。

那么只要我们后续添加的类型都始终满足这个逻辑的话,我们就可以使用反射的方式来优化这部分代码,使其符合开闭原则。

Msg msg = null;
//此处可用反射替换
Class localClass = Class.forName("com.dhb.tank.mode."+msgType.toString()+"Msg");
msg = (Msg)localClass.newInstance();
msg.parse(bytes);
out.add(msg);

这样就将上述复杂的 switch 代码通过反射几行代码就能搞定。

但是需要注意的是,反射代码存在的问题是,在写代码的时候需要满足一些通用的规则。

如上述代码中,我们根据 type 的 toString 加上 Msg 字符串就能够反射出这个实体类,我们在增加新的业务类型的时候,就带来了局限性。

所以通过反射方式来实现的逻辑的话,必须要将这写潜在的业务规则写明白,以便后续的开发者忽略了这些规则而造成 bug。

策略模式进一步优化

如果要对反射的实现反射进一步优化的话,那么还可以使用策略模式来实现。

代码实现如下:首先需要定义一个 HashMap,将对应关系存在这个 hashMap 中。

private static final Map<MsgType,Msg> msgMap = new HashMap<>();
static{
msgMap.put(MsgType.TankJoin,new TankJoinMsg());
msgMap.put(MsgType.TankStartMoving,new TankStartMovingMsg());
msgMap.put(MsgType.TankStop,new TankStopMsg());
msgMap.put(MsgType.TankDirChanged,new TankDirChangedMsg());
msgMap.put(MsgType.BulletNew,new BulletNewMsg());
msgMap.put(MsgType.TankDie,new TankDieMsg());
msgMap.put(MsgType.TankExit,new TankExitMsg());
}

之后使用这个代码就非常容易了:

Msg msg = null;
//此处可用反射替换
msg = msgMap.get(msgType);
msg.parse(bytes);
out.add(msg);

可以直接采用 get 的方式就能轻松或者之前定义的 msg 类型进行处理。如果在 spring 中,这个 map 完全可以在配置文件中进行配置,然后再此处使用的时候进行注入。

那么就能完美实现减少代码的目的。不过需要注意的是,上述方式仍然只能解决并列的分支判断问题。

用责任链模式处理复杂的嵌套关系

考虑到策略模式只能解决并列分支的问题,对解决分支嵌套的问题还是没有任何帮助。因此,我们考虑另外一种设计模式,责任链模式。

责任链模式的链实际上是一个 list 对象,如果需要进入下一个嵌套,那么此处就不是写一个新的 if-else,而是将这个新的 if-else 封装为一个对象,写在代码里面。

如果假定我们上述的 if-else 嵌套为如下的话:

if(in.readInt() == 1) {
//转换为TankJoinMsg对象
if(in.readInt() == 2) {
//转换为TankStartMovingMsg对象
if(in.readInt() == 3) {
//转换为TankStopMsg对象
if(in.readInt() == 4) {
//转换为TankDirChangedMsg对象
if(in.readInt() == 5) {
//转换为BulletNew对象

//复杂嵌套式的处理逻辑

} else {
//处理逻辑5
}
}else {
//else处理逻辑4
}
}else {
//else处理逻辑3
}
}else {
//else处理逻辑2
}
}else {
//else处理逻辑1

}

这就与开篇那张图非常类似了,对于这样的嵌套逻辑,那么可以采用责任链模式进行优化。

上述代码修改为责任链如下,构建了一个处理链:

public class ProcessChain {
int index = 0;
List<MsgProcesser> processers = new ArrayList<>();

public ProcessChain add(MsgProcesser p) {
processers.add(p);
return this;
}

public void process(Msg msg) {
if(index == processers.size()) {
return;
}
MsgProcesser proccess = processers.get(index);
index ++;
proccess.process(msg,this);
}
}

之后将每层的 if-else 都定义为一个 msgProcesser:

public interface MsgProcesser {

public void process(Msg msg,ProcessChain chain);


}

然后具体的类实现这个 processer:

public class TankJoinMsgProcesser implements MsgProcesser{

@Override
public void process(Msg msg, ProcessChain chain) {
if(in.readInt() == 1) {
//if处理逻辑,之后继续执行责任链中的后续逻辑
chain.process(msg);
}else {
//else处理逻辑,并退出
}
}
}

可以看到,我们在处理具体的 proccesser 的实现类的时候,如果if逻辑满足,则继续对链中的后续逻辑进行调用。

那么在调用的时候,只需要将已经构造好的处理器增加到 chain 中,之后就能完成整个流程。

ProcessChain chain = new ProcessChain();
chain.add(new TankJoinMsgProcesser()).add(new TankStartMovingMsgProcesser());
...
chain.process(msg);

其本质就是将每一层的 if-else 都转换为了一个具体的类,如果某个类里面如果需要继续向下嵌套,那么继续调用这个 chain 的 process 方法。

需要注意的是,这是一种单一的责任链,如果条件复杂的情况下,可能会构成多个链。

反正不难看出,对于 if-else 的处理,实际上有很多方式,但是我们需要注意的是避免对程序的过度设计,这样会造成代码的可读性变差。

文章目录
  1. 1. 问题提出
  2. 2. 用 switch-case 优化
  3. 3. 用反射替换 switch-case
  4. 4. 策略模式进一步优化
  5. 5. 用责任链模式处理复杂的嵌套关系