修炼码德:简化条件表达式

Silently9527
2021-08-14 / 0 评论 / 31 阅读
温馨提示:
本文最后更新于2021-08-14,若内容或图片失效,请留言反馈。

前言

与面向过程编程相比,面向对象编程的条件表达式相对来说已经比少了,因为很多的条件行为都可以被多态的机制处理掉;但是有时候我们还是会遇到一些小伙伴写出来的条件表达式和面向过程编程没什么差别,比如我遇到过这段代码:

样例

整段代码有三层,每一层还有if-else,本身的这段代码的逻辑就够难以理解了,更加恶心的是这个方法的调用方以及调用的其他方法,同样也是这样的if-else嵌套几层;
加之这段代码还有一个很大的问题是传入的参数对象,在内部以及调用的其他方法中被修改多次修改,这样就更难懂了;靠普通人的单核CPU想看懂太难了,维护这段代码我感觉身体被掏空

身体被掏空

有时候我们可能会遇到比较复杂的条件逻辑,需要我们想办法把分成若干个小块,让分支逻辑和操作细节分离;看一个程序员的码德如何,先看他的条件表达式是否够简洁易懂;今天我们来分享一下简化条件表达式的常用方法,修炼自己的码德;本文中大部分的例子来源于《重构改善既有代码设计》


分解条件表达式

复杂的条件逻辑是最常导致复杂度上升的地方之一,另外如果分支内部逻辑也很多,最终我们会得到一个很大的函数,一个长的方法可读性本身就会下降,所以我们需要把大的方法才分的多个的方法,为每个方法取一个容易清楚表达实现内部逻辑的方法名,这样可读性就会上大大提高。

举例:

if (date.before (SUMMER_START) || date.after(SUMMER_END)) {
    charge = quantity * _winterRate + _winterServiceCharge;
} else {
    charge = quantity * _summerRate
}

这种代码很多人可能都觉得没必要去提取方法,但是如果我们想要看懂这段代码,还是必须的去想想才知道在做什么;接下来我们修改一下

if (notSummer(date)) {
    charge = winterCharge(quantity);
} else {
    charge = summerCharge(quantity);
}

private boolean notSummer(Date date){
    date.before (SUMMER_START) || date.after(SUMMER_END)
}

private double summerCharge(int quantity) {
    return quantity * _summerRate;
}

private double winterCharge(int quantity) {
    return quantity * _winterRate + _winterServiceCharge;
}

这样修改之后是不是很清楚,好的代码本身不需要写注释(代码具有自说明性),更不需要在方法内部写任何注释,有时候我们会看到有同学会在方法内部隔几行就会写一点注释,这说明本身代码的自说明性不够好,可以通过刚才这个例子的方式提高代码的可读性


合并条件表达式

当遇到一段代码多个if条件判断,但是条件内部的逻辑缺类似,我们可以把条件合并在一起,然后抽取方法。

举例1:

double disabilityAmount () {
    if(_seniortiy <2 ) 
        return 0;
    if(_monthsDisabled > 12)
        return 0;
    if(_isPartTime)
        return 0;
    // 省略...
}

这里的条件返回的结果都是一样的,那么我们先把条件合并起来

double disabilityAmount () {
    if(_seniortiy <2 || _monthsDisabled > 12 || _isPartTime) {
        return 0;
    }
    // 省略...
}

接下来我们再来把判断条件判断条件抽取成方法提高可读性

double disabilityAmount () {
    if(isNotEligibleForDisableility()) {
        return 0;
    }
    // 省略...
}

boolean isNotEligibleForDisableility() {
    return _seniortiy <2 || _monthsDisabled > 12 || _isPartTime;
}

举例2:

if(onVacation()) {
    if(lengthOfService() > 10) {
        return 2;
    }
}
return 1;

合并之后的代码

if(onVacation() && lengthOfService() > 10){
    return 2
}
return 1;

接着我们可以使用三元操作符更加简化,修改后的代码:

return onVacation() && lengthOfService() > 10 ? 2 : 1;

通过这两个例子我们可以看出,先把条件逻辑与分支逻辑抽离成不同的方法分离开,然后我们会发现提高代码的可读性是如此的简单,得心应手;所以抽离好的方法是关键;我觉得此处应该有掌声

我膨胀了


合并重复的条件片段

我们先来看一个例子,10岁以下的小朋友票价打5折

if(ageLt10(age)) {
    price = price * 0.5;
    doSomething();
} else {
    price = price * 1;
    doSomething();
}

我们发现不同的分支都执行了相同的末段代码逻辑,这时候我们可以把这段代码提取到条件判断之外,这里举得例子较为简单,通常工作中遇到的可能不是这样一个简单的方法,而是很多行复杂的逻辑条件,我们可以先把这个代码提取成一个方法,然后把这个方法的调用放在条件判断之前或之后

修改之后的代码

if(ageLt10(age)) {
    price = price * 0.5;
} else {
    price = price * 1;
}
doSomething();

当我们遇到try-catch中有相同的逻辑代码,我们也可以使用这种方式处理


卫语句取代嵌套条件表达式

方法中一旦出现很深的嵌套逻辑让人很难看懂执行的主线。当使用了if-else表示两个分支都是同等的重要,都是主线流程;向下图表达的一样,
if-else

但是大多数时候我们会遇到只有一条主线流程,其他的都是个别的异常情况,在这种情况下使用if-else就不太合适,应该用卫语句取代嵌套表达式。
if-reture

举例1:

在薪酬系统中,以特殊的规则处理死亡员工,驻外员工,退休员工的薪资,这些情况都是很少出现,不属于正常逻辑;

double getPayAmount() {
    double result;
    if(isDead){
        result = deadAmount();
    } else {
        if(isSeparated) {
            result = separatedAmount();
        } else {
            if(isRetired) {
                result = retiredAmount();
            } else {
                result = normalPayAmount();
            }
        }
    }
    return result;
}

在这段代码中,我们完全看不出正常流程是什么,这些偶尔发生的情况把正常流程给掩盖了,一旦发生了偶然情况,就应该直接返回,引导代码的维护者去看一个没用意义的else只会妨碍理解;让我们用return来改造一下

double getPayAmount() {
    if(isDead){
        return deadAmount();
    }
    if(isSeparated) {
        return separatedAmount():
    }
    if(isRetired) {
        return retiredAmount();
    }
    return normalPayAmount();
}

多态取代条件表达式

有时候我们会遇到if-else-if或者switch-case这种结构,这样的代码不仅不够整洁,遇到复杂逻辑也同样难以理解。这种情况我们可以利用面向对象的多态来改造。

举例:
假如你正在开发一款游戏,需要写一个获取箭塔(Bartizan)、弓箭手(Archer)、坦克(Tank)攻击力的方法;经过两个小时的努力终于完成了这个功能;开发完成后的代码如下:

int attackPower(Attacker attacker) {
    switch (attacker.getType()) {
        case "Bartizan":
            return 100;
        case "Archer":
            return 50;
        case "Tank":
            return 800;
    }
    throw new RuntimeException("can not support the method");
}

经过自测后没有任何问题,此时你的心情很爽

心情很爽

当你提交代码交由领导review的时候,领导(心里想着这点东西搞两个小时,上班摸鱼太明显了吧)直接回复代码实现不够优雅,重写

1. 枚举多态

你看到这个回复虽然心里很不爽,但是你也没办法,毕竟还是要在这里混饭吃的;嘴上还是的回答OK

你思考了一会想到了使用枚举的多态来实现不就行了,说干就干,于是你写了下一个版本

int attackPower(Attacker attacker) {
   return AttackerType.valueOf(attacker.getType()).getAttackPower();
}

enum AttackerType {
    Bartizan("箭塔") {
        @Override
        public int getAttackPower() {
            return 100;
        }
    },
    Archer("弓箭手") {
        @Override
        public int getAttackPower() {
            return 50;
        }
    },
    Tank("坦克") {
        @Override
        public int getAttackPower() {
            return 800;
        }
    };

    private String label;

    Attacker(String label) {
        this.label = label;
    }

    public String getLabel() {
        return label;
    }

    public int getAttackPower() {
        throw new RuntimeException("Can not support the method");
    }
}

这次再提交领导review,顺利通过了,你心想总于get到了领导的点了

2. 类多态

没想到你没高兴几天,又接到个新的需求,这个获取攻击力的方法需要修改,根据攻击者的等级不同而攻击力也不同;你考虑到上次的版本攻击力是固定的值,使用枚举还比较合适,而这次的修改要根据攻击者的本身等级来计算攻击了,如果再使用枚举估计是不合适的;同时你也想着上次简单实现被领导怼了,这次如果还是在上次的枚举版本上来实现,估计也不会有好结果;最后你决定使用类的多态来完成

int attackPower(Attacker attacker) {
    return attacker.getAttackPower();
}

interface Attacker {
    default int getAttackPower() {
        throw new RuntimeException("Can not support the method");
    }
}

class Bartizan implements Attacker {
    public int getAttackPower() {
        return 100 * getLevel();
    }
}

class Archer implements Attacker {
    public int getAttackPower() {
        return 50 * getLevel();
    }
}

class Tank implements Attacker {
    public int getAttackPower() {
        return 800 * getLevel();
    }
}

完成之后提交给领导review,领导笑了笑通过了代码评审;

3. 策略模式

你本以为这样就结束了,结果计划赶不上变化,游戏上线后,效果不是太好,你又接到了一个需求变更,攻击力的计算不能这么粗暴,我们需要后台配置规则,让部分参加活动玩家的攻击力根据规则提升。

你很生气,心里想着:没听说过杀死程序员不需要用枪吗,改三次需求就可以了,MD这是想我死吗。

改需求

生气归生气,但是不敢表露出来,谁让你是领导呢,那就开搞吧

考虑到本次的逻辑加入了规则,规则本身可以设计的简单,也可以设计的很复杂,如果后期规则变得更加复杂,那么整个攻击对象类中会显得特别的臃肿,扩展性也不好,所以你这次不再使用类的多态来实现,考虑使用策略模式,完成后代码如下:

//定义计算类的接口
interface AttackPowerCalculator {
    boolean support(Attacker attacker);

    int calculate(Attacker attacker);
}

//箭塔攻击力计算类
class BartizanAttackPowerCalculator implements AttackPowerCalculator {

    @Override
    public boolean support(Attacker attacker) {
        return "Bartizan".equals(attacker.getType());
    }

    @Override
    public int calculate(Attacker attacker) {
        //根据规则计算攻击力
        return doCalculate(getRule());
    }
}

//弓箭手攻击力计算类
class ArcherAttackPowerCalculator implements AttackPowerCalculator {

    @Override
    public boolean support(Attacker attacker) {
        return "Archer".equals(attacker.getType());
    }

    @Override
    public int calculate(Attacker attacker) {
        //根据规则计算攻击力
        return doCalculate(getRule());
    }
}

//坦克攻击力计算类
class TankAttackPowerCalculator implements AttackPowerCalculator {

    @Override
    public boolean support(Attacker attacker) {
        return "Tank".equals(attacker.getType());
    }

    @Override
    public int calculate(Attacker attacker) {
        //根据规则计算攻击力
        return doCalculate(getRule());
    }
}

//聚合所有计算类
class AttackPowerCalculatorComposite implements AttackPowerCalculator {
    List<AttackPowerCalculator> calculators = new ArrayList<>();

    public AttackPowerCalculatorComposite() {
        this.calculators.add(new TankAttackPowerCalculator());
        this.calculators.add(new ArcherAttackPowerCalculator());
        this.calculators.add(new BartizanAttackPowerCalculator());
    }

    @Override
    public boolean support(Attacker attacker) {
        return true;
    }

    @Override
    public int calculate(Attacker attacker) {
        for (AttackPowerCalculator calculator : calculators) {
            if (calculator.support(attacker)) {
                calculator.calculate(attacker);
            }
        }
        throw new RuntimeException("Can not support the method"); 
    }
}

//入口处通过调用聚合类来完成计算
int attackPower(Attacker attacker) {
    AttackPowerCalculator calculator = new AttackPowerCalculatorComposite();
    return calculator.calculate(attacker);
}

你再次提交代码给领导review,领导看了很满意,表扬你说:小伙子,不错,进步很快嘛,给你点赞哦;你回答:感谢领导认可(心里想那是当然,毕竟我已经摸清了你的点在哪里了)

觉得本次你的这个功能完成的还比较满意的,请点赞关注评论走起哦

骄傲

引入断言

最后一个简化条件表达式的操作是引入断言,这部分比较简单,并且Spring框架本身也提供了断言的工具类,比如下面这段代码:

public void getProjectLimit(String project){
    if(project == null){
        throw new RuntimeException("project can not null");
    }
    doSomething();
}

加入Spring的断言后的代码

public void getProjectLimit(String project){
    Assert.notNull(project,"project can not null");
    doSomething();
}

写在最后
感谢大家可以耐心地读到这里。
当然,文中或许会存在或多或少的不足、错误之处,有建议或者意见也非常欢迎大家在评论交流。
最后,希望朋友们可以点赞评论关注三连,因为这些就是我分享的全部动力来源🙏

往期Java8系列文章: