对复杂if-else代码块的优化方案

点击上方“Java基基”,选择“设为星标”

做积极的人,而不是积极废人!

每天 14:00 更新文章,每天掉亿点点头发...

源码精品专栏

 

来源:c1n.cn/LXJFJ

5aeddf268b55022f20d3e26dde99e0ab.jpeg


问题提出

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

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

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

aece4be3b04d03829bd4a253872af7f3.png

今天本文就来聊聊,在 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。

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro

  • 视频教程:https://doc.iocoder.cn/video/

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

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud

  • 视频教程:https://doc.iocoder.cn/video/

用反射替换 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 的处理,实际上有很多方式,但是我们需要注意的是避免对程序的过度设计,这样会造成代码的可读性变差。



欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢

cb69dda2f41884c4b0198be608dbcc73.png

已在知识星球更新源码解析如下:

aea7d28b993d206debc4e7f4bf7c9414.jpeg

b5a88ea5463235489d3bde2acacd6e16.jpeg

3fc945ad8a01770ac13ff40b941ae117.jpeg

7895de6fea9ad7f705a9aab8e652692f.jpeg

最近更新《芋道 SpringBoot 2.X 入门》系列,已经 101 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。

提供近 3W 行代码的 SpringBoot 示例,以及超 6W 行代码的电商微服务项目。

获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值