大量if-else的金字塔代码该如何优化?秋招问答题踩的坑现在埋起来


为什么我们写的代码都是 if-else

在今年秋招时,就遇到这样一道笔试题,问:

怎么优化多层嵌套的if-else结构,为什么要去优化?

其实,自己第一能够想到的就是用switch来优化,因为相对于多个 else-if,switch的逻辑结构更加清晰。简单的逻辑可以使用三元运算符,如果是多层嵌套的 if-else 可以试着进行拆分。当时大概就是这么简短的回答的,现做一个总结。

写 if-else 不外乎两种场景: 异常逻辑处理 和 不同状态处理

两者最主要的区别是:

  • 异常逻辑处理说明只有一个分支是正常流程,另一个是异常情况的处理
  • 而不同状态处理中 所有分支都是正常流程(类比switch)

异常逻辑处理举例:

不同状态处理举例:

第一个例子 if (obj != null) 是异常处理,是代码健壮性判断,只有 if 里面才是正常的处理流程,else 分支是出错处理流程;

而第二个例子不管 type 等于 1,2,3 还是其他情况,都属于业务的正常流程。对于这两种情况重构的方法也不一样。

if-else 代码太多有什么缺点

如果是复杂的业务的逻辑或者代码量很多时,就会存在问题:

最大的问题是代码逻辑复杂,维护性、拓展性差,极容易引发 bug。如果使用 if-else,说明 if 分支和 else 分支的重视是同等的,但大多数情况并非如此,容易引起误解和理解困难。想要新加一个分支的时候,就会很难添加,极其容易影响到其他的分支。


优化的思路

  • 重构 if-else 的结构
  • 策略模式

重构优化

重构 if-else 时,心中无时无刻把握一个原则:

尽可能地维持正常流程代码在最外层。

意思是说,可以写 if-else 语句时一定要尽量保持主干代码是正常流程,避免嵌套过深。

实现的手段有:减少嵌套、移除临时变量、条件取反判断、合并条件表达式等


异常逻辑处理型重构

优化一:合并条件表达式

如果有一系列条件测试都得到相同结果,将这些结果测试合并为一个条件表达式。

这个重构方法简单易懂,带来的效果也非常明显,能有效地较少if语句,减少代码量逻辑上也更加易懂。

重构前:

public double disablityAmount() {
    if(_seniority < 2) 
        return 0;
 
    if(_monthsDisabled > 12)
        return 0;
 
    if(_isPartTime)
        return 0;
    // TODO
}

重构后:

public double disablityAmount() {
    if(_seniority < 2 || _monthsDisabled > 12 || _isPartTime)  {
	    return 0;
    }        
    // TODO
}

优化二:减少嵌套

重构前:

public double getPayAmount() {
    double result;
    if (_isDead) {
        result = deadAmount();
    } else {
        if (_isSeparated) {
            result = separatedAmount();
        } else {
            if (_isRetired) {
                result = retiredAmount();
        else{
                    result = normalPayAmount();
                }
            }
        }
        return result;
    }
}

重构后:

public double getPayAmount() {
    if (_isDead) 
        return deadAmount();
    
    if (_isSeparated) 
        return separatedAmount();
    
    if (_isRetired) 
        return retiredAmount();
    
    if (_isRetired) 
        return retiredAmount();
    
    return normalPayAmount();
}

总结重构的要点】:如果 if-else 嵌套没有关联性,直接提取到第一层,一定要避免逻辑嵌套太深。尽量减少临时变量改用 return 直接返回。

  • 重构的前提:要理清楚多层嵌套之间的关联

  • 结果:很大程度上减少了if-else的多层嵌套,逻辑更加清晰明了。

因为嵌套内的if-else和最外层并没有关联性的,它们之间是平行关系而非包含关系;取消了result临时变量,直接return返回,因为return之后后面的逻辑是执行不到的,虽然是多个if,但是实际上只执行了一个。

原来的做法先赋值给 result 最后统一 return,那么对于最后 return 的值到底是那个函数返回的结果不明确,增加了一层理解难度。

【覆写Object类的equals方法实例】

@Override
public boolean equals(Object obj) {
    if (obj == null)  return false; 
    // 判断地址是否相等,地址相同则二者指向的是用同一个引用
    if (this == obj)  return true; 
    // 判断传入的对象是否属于要比较的类的对象,不是则无比较性
    if (!(obj instanceof Person))  return false;
    //向下转型,将Object类转为Person类
    Person per = (Person) obj;
    return per.name.equals(this.name) && per.age == this.age;
}

优化三:条件反转使异常情况先退出

重构前:

public double getPayAmount() {
    double result = 0.0;
    if (_capital > 0.0) {
        if (_intRate > 0 && _duration > 0) {
            resutl = (_income / _duration) * ADJ_FACTOR;
        }
    }
    return result;
}

重构后:

首先,运用第一招,减少嵌套和移除临时变量:

public double getPayAmount() {
    if (_capital <= 0.0) return 0.0;
    if (_intRate > 0 && _duration > 0) {
        return (_income / _duration) * ADJ_FACTOR;
    }
    return 0.0;
}

这样重构后,还不够,因为主要的语句 (_income / _duration) *ADJ_FACTOR; 在 if 内部,并非在最外层,根据优化原则(尽可能地维持正常流程代码在最外层),可以再继续重构:

public double getPayAmount() {
    if (_capital <= 0.0) return 0.0;
    if (_intRate <= 0 && _duration <= 0) {
        return 0.0;
    }
    return (_income / _duration) * ADJ_FACTOR;
}

这里用到的重构方法是:将条件反转使异常情况先退出,让正常流程维持在主干流程。

这才是好的代码风格,逻辑清晰,一目了然,没有 if-else 嵌套难以理解的流程。


优化四:合并条件表达式

重构前:

典型的"箭头型"代码,最大的问题是嵌套过深,解决方法是异常条件先退出,保持主干流程是核心流程

/** 查找年龄大于18岁且为男性的学生列表 **/
public ArrayList<Student> getStudents(int uid) {
    ArrayList<Student> result = new ArrayList<>();
    Student stu = getStudentByUid(uid);
    if (stu != null) {
        Teacher teacher = stu.getTeacher();
        if (teacher != null) {
            ArrayList<Student> students = teacher.getStudents();
            if (students != null) {
                for (Student student : students) {
                    if (student.getAge() >= 18 && student.getGender() == MALE) {
                        result.add(student);
                    }
                }
            } else {
                logger.error("获取学生列表失败");
            }
        } else {
            logger.error("获取老师信息失败");
        }
    } else {
        logger.error("获取学生信息失败");
    }
    return result;
}

重构后:

public ArrayList<Student> getStudents(int uid) {
    ArrayList<Student> result = new ArrayList<>();
    Student stu = getStudentByUid(uid);
    if(stu == null) {
        logger.error("获取学生信息失败");
        return result;
    }
    Teacher teacher = stu.getTeacher();
    if (teacher != null) {
        logger.error("获取老师信息失败");
        return result;
    }
    ArrayList<Student> students = teacher.getStudents();
    if (students != null) {
        logger.error("获取学生列表失败");
        return result;
    }
    for (Student student : students) {
        if (student.getAge() >= 18 && student.getGender() == MALE) {
            result.add(student);
        }
    }
    return result;
}

状态逻辑处理型重构

优化一:封装内部函数

重构前:

public double getPayAmount() {
    Object obj = getObj();
    double money = 0;
    if (obj.getType == 1) {
        ObjectA objA = obj.getObjectA();
        money = objA.getMoney() * obj.getNormalMoneryA();
    } else if (obj.getType == 2) {
        ObjectB objB = obj.getObjectB();
        money = objB.getMoney() * obj.getNormalMoneryB() + 1000;
    }
}

重构后:

public double getPayAmount() {
    Object obj = getObj();
    if (obj.getType == 1) {
        return getType1Money(obj);
    } else if (obj.getType == 2) {
        return getType2Money(obj);
    }
}

public double getType1Money(Object obj) {
    ObjectA objA = obj.getObjectA();
    return objA.getMoney() * obj.getNormalMoneryA();
}

public double getType2Money(Object obj) {
    ObjectB objB = obj.getObjectB();
    return objB.getMoney() * obj.getNormalMoneryB() + 1000;
}

这里使用的重构方法是:把 if-else 内的代码都封装成一个公共函数。函数的好处是屏蔽内部实现,缩短 if-else 分支的代码。代码结构和逻辑上清晰,能一下看出来每一个条件内做的功能。


优化二:多态取代条件表达式

针对状态处理的代码,一种优雅的做法是用 多态 取代条件表达式(《重构》推荐做法)。

你手上有个条件表达式,它根据对象类型的不同而选择不同的行为。将这个表达式的每个分支放进一个子类内的覆写函数中,然后将原始函数声明为抽象函数。


重构前:

public double getSpeed() {
    switch (_type) {
        case EUROPEAN:
            return getBaseSpeed();
        case AFRICAN:
            return getBaseSpeed() - getLoadFactor() * _numberOfCoconuts;
        case NORWEGIAN_BLUE:
            return (_isNailed) ? 0 : getBaseSpeed(_voltage);
    }
}

重构后:

class Bird {
    abstract double getSpeed();
}

class European extends Bird {
    double getSpeed() {
        return getBaseSpeed();
    }
}

class African extends Bird {
    double getSpeed() {
        return getBaseSpeed() - getLoadFactor() * _numberOfCoconuts;
    }
}

class NorwegianBlue extends Bird {
    double getSpeed() {
        return (_isNailed) ? 0 : getBaseSpeed(_voltage);
    }
}

使用多态后直接没有了 if-else,但使用多态对原来代码修改过大,需要一番功夫才行。最好在设计之初就使用多态方式。


策略模式优化

抽象了出了接口,将业务逻辑封装成一个一个的实现类,任意地替换。在复杂场景(业务逻辑较多)时比直接 if-else 来的好维护些。

就是几个if else场景我需要用到策略模式吗?

我们所不满的其实就是传统接口实现的缺点:

  1. 策略类会很多。

  2. 业务逻辑分散到各个实现类中,而且没有一个地方可以俯览整个业务逻辑

有没有什么更好的代码结构来实现策略模式的吗?

针对传统策略模式的缺点,分享了利用Map与函数式接口来实现的思路。


优化实例

假设有这么一个需求:

一个电商系统,当用户消费满1000 金额,可以根据用户VIP等级,享受打折优惠。

根据用户VIP等级,计算出用户最终的费用。

  • 普通会员 不打折
  • 白银会员 优惠50元
  • 黄金会员 8折
  • 白金会员 优惠50元,再打7折

【策略模式优化】

我们定义来一个 Strategy 接口,并且定义 四个子类,实现接口。在对应的 compute方法 实现自身策略的计费逻辑。

public interface Strategy {
    // 计费方法
    double compute(long money);
}

接口实现类:

// 普通会员策略
public class OrdinaryStrategy implements Strategy {

    @Override
    public double compute(long money) {
        System.out.println("普通会员 不打折");
        return money;
    }
}

// 白银会员策略
public class SilverStrategy implements Strategy {

    @Override
    public double compute(long money) {

        System.out.println("白银会员 优惠50元");
        return money - 50;
    }
}

// 黄金会员策略
public class GoldStrategy implements Strategy{

    @Override
    public double compute(long money) {
        System.out.println("黄金会员 8折");
        return money * 0.8;
    }
}

// 白金会员策略
public class PlatinumStrategy implements Strategy {
    @Override
    public double compute(long money) {
        System.out.println("白金会员 优惠50元,再打7折");
        return (money - 50) * 0.7;
    }
}

然后对应 getResult 方法,根据 type 替换为对应的 用户VIP 策略。这里代码上出现了重复的调用 compute ,我们可以尝试进一步优化。

private static double getResult(long money, int type) {

    if (money < 1000) {
        return money;
    }

    Strategy strategy;

    if (type == UserType.SILVER_VIP.getCode()) {
        strategy = new SilverStrategy();
    } else if (type == UserType.GOLD_VIP.getCode()) {
        strategy = new GoldStrategy();
    } else if (type == UserType.PLATINUM_VIP.getCode()) {
        strategy = new PlatinumStrategy();
    } else {
        strategy = new OrdinaryStrategy();
    }

    return strategy.compute(money);
}

可以看到,虽然我们使用了策略模式,但是优化的效果并不是很理想,仍然出现了if-else


【工厂 + 策略】

接口不变,我们先在 Strategy 新增一个 getType 方法,用来表示 该策略的 type 值

public class OrdinaryStrategy implements Strategy {

    @Override
    public double compute(long money) {
        System.out.println("普通会员 不打折");
        return money;
    }

    // 添加 type 返回
    @Override
    public int getType() {
        return UserType.SILVER_VIP.getCode();
    }
}

public class SilverStrategy implements Strategy {

    @Override
    public double compute(long money) {

        System.out.println("白银会员 优惠50元");
        return money - 50;
    }

    // type 返回
    @Override
    public int getType() {
        return UserType.SILVER_VIP.getCode();
    }
}

....省略剩下 Strategy

我们再着手创建一个 StrategyFactory 工厂类。StrategyFactory 这里我使用的是静态内部类单例,在构造方法的时候,初始化好 需要的 Strategy,并把 list 转化为 map。

public class StrategyFactory {

    private Map<Integer, Strategy> map;

    public StrategyFactory() {

        List<Strategy> strategies = new ArrayList<>();

        strategies.add(new OrdinaryStrategy());
        strategies.add(new SilverStrategy());
        strategies.add(new GoldStrategy());
        strategies.add(new PlatinumStrategy());
        strategies.add(new PlatinumStrategy());

        // 看这里 看这里 看这里!
        map = strategies.stream().collect(Collectors.toMap(Strategy::getType, strategy -> strategy));

        /* 等同上面
        map = new HashMap<>();
        for (Strategy strategy : strategies) {
            map.put(strategy.getType(), strategy);
        }*/
    }

    public static class Holder {
        public static StrategyFactory instance = new StrategyFactory();
    }

    public static StrategyFactory getInstance() {
        return Holder.instance;
    }

    public Strategy get(Integer type) {
        return map.get(type);
    }
}

List 转化为 Map,转化就是“灵魂”所在,再次优化上面策略模式下的getResult()方法,消除其if-else

private static double getResult(long money, int type) {

    if (money < 1000) {
        return money;
    }

    Strategy strategy = StrategyFactory.getInstance().get(type);

    if (strategy == null){
        throw new IllegalArgumentException("please input right type");
    }

    return strategy.compute(money);
}

通过一个工厂类,在我们在 getResult()调用的时候,根据传入 type,即可获取到 对应 Strategy

再也没有可怕的 if-else 语句,完结撒花撒花 : )。


【List转化为Map】

通常情况下,我们遍历 List,手动put到 Map 中。

map = new HashMap<>();
for (Strategy strategy : strategies) {
    map.put(strategy.getType(), strategy);
}

来看看 Java8 语法中的小技巧

map = strategies.stream().collect(Collectors.toMap(Strategy::getType, strategy -> strategy));

toMap 第一个参数是一个Function,对应 Map 中的 key,第二个参数也是一个Function,strategy -> strategy, 左边strategy 是遍历 strategies 中的每一个strategy,右边strategy则是 Map 对应 value 值。


【原文链接】

[1] CSDN-yinnnnnnn.6个实例详解如何把if-else代码重构成高质量代码.https://blog.csdn.net/qq_35440678/article/details/77939999

[2] 公众号Hollis-上帝爱吃苹果.用策略模式干掉 if-else

[3] hyzhan43.探讨复杂的 if-else 语句“优雅处理”的思路.juejin.im/post/5def654f51882512302daeef

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值