资讯专栏INFORMATION COLUMN

谈谈代码——如何避免写出糟糕if...else语句

huhud / 2223人阅读

摘要:源码剖析之设计模式鉴赏策略模式小结在这篇文章中笔者和大家分享几个减少的小由于这些都会有一定的限制因此还向大家介绍了几个能够避免写出糟糕的的设计模式并使用观察者模式简单的改进了仲裁者模式的例子

本文首发于数据浮云:https://mp.weixin.qq.com/s?__...

在写代码的日常中,if...else语句是极为常见的.正因其常见性,很多同学在写代码的时候并不会去思考其在目前代码中的用法是否妥当.而随着项目的日渐发展,糟糕的if...else语句将会充斥在各处,让项目的可维护性急剧下降.故在这篇文章中,笔者想和大家谈谈如何避免写出糟糕if...else语句.

由于脱密等原因.文章中的示例代码将会用一些开源软件的代码或者抽象过的生产代码作为示范.
问题代码

当我们看到一组if...else时,一般是不会有什么阅读负担的.但当我们看到这样的代码时:

    private void validate(APICreateSchedulerMessage msg) {
        if (msg.getType().equals("simple")) {
            if (msg.getInterval() == null) {
                if (msg.getRepeatCount() != null) {
                    if (msg.getRepeatCount() != 1) {
                        throw new ApiMessageInterceptionException(argerr("interval must be set when use simple scheduler when repeat more than once"));
                    }
                } else {
                    throw new ApiMessageInterceptionException(argerr("interval must be set when use simple scheduler when repeat forever"));
                }
            } else if (msg.getInterval() != null) {
                if (msg.getRepeatCount() != null) {
                    if (msg.getInterval() <= 0) {
                        throw new ApiMessageInterceptionException(argerr("interval must be positive integer"));
                    } else if ((long) msg.getInterval() * (long) msg.getRepeatCount() * 1000L + msg.getStartTime() < 0 ) {
                        throw new ApiMessageInterceptionException(argerr("duration time out of range"));
                    } else if ((long) msg.getInterval() * (long) msg.getRepeatCount() * 1000L + msg.getStartTime() > 2147454847000L) {
                        throw new ApiMessageInterceptionException(argerr("stopTime out of mysql timestamp range"));
                    }
                }
            }

            if (msg.getStartTime() == null) {
                throw new ApiMessageInterceptionException(argerr("startTime must be set when use simple scheduler"));
            } else if (msg.getStartTime() != null && msg.getStartTime() < 0) {
                throw new ApiMessageInterceptionException(argerr("startTime must be positive integer or 0"));
            } else if (msg.getStartTime() != null && msg.getStartTime() > 2147454847 ){
                //  mysql timestamp range is "1970-01-01 00:00:01" UTC to "2038-01-19 03:14:07" UTC.
                //  we accept 0 as startDate means start from current time
                throw new ApiMessageInterceptionException(argerr("startTime out of range"));
            }

            if (msg.getRepeatCount() != null && msg.getRepeatCount() <= 0) {
                throw new ApiMessageInterceptionException(argerr("repeatCount must be positive integer"));
            }
        }

        if (msg.getType().equals("cron")) {
            if (msg.getCron() == null || ( msg.getCron() != null && msg.getCron().isEmpty())) {
                throw new ApiMessageInterceptionException(argerr("cron must be set when use cron scheduler"));
            }
            if ( (! msg.getCron().contains("?")) || msg.getCron().split(" ").length != 6) {
                throw new ApiMessageInterceptionException(argerr("cron task must follow format like this : "0 0/3 17-23 * * ?" "));
            }
            if (msg.getInterval() != null || msg.getRepeatCount() != null || msg.getStartTime() != null) {
                throw new ApiMessageInterceptionException(argerr("cron scheduler only need to specify cron task"));
            }
        }
    }

亦或是这样的代码:

try {
   for (int j = myConfig.getContentStartNum(); j <= rowNum; j++) {
        row = sheet.getRow(j);
        T obj = target.newInstance();
        for (int i = 0; i < colNum; i++) {

            Field colField = ExcelUtil.getOneByTitle(metaList, titleList[i]);
            colField.setAccessible(true);
            String fieldType = colField.getType().getSimpleName();
            HSSFCell cell = row.getCell(i);
            int cellType = cell.getCellType();
            System.out.println(colField.getName()+"|"+fieldType+" | "+cellType);

            if(HSSFCell.CELL_TYPE_STRING == cellType){
                if("Date".equals(fieldType)){
                    colField.set(obj, DateUtil.parse(cell.getStringCellValue()));
                }else {
                    colField.set(obj, cell.getStringCellValue());
                }
            }else if(HSSFCell.CELL_TYPE_BLANK == cellType){
                System.out.println("fieldName"+colField.getName());
                if("Boolean".equals(fieldType)){
                    colField.set(obj, cell.getBooleanCellValue());
                }else{
                    colField.set(obj, "");
                }
            }else if(HSSFCell.CELL_TYPE_NUMERIC == cellType){
                if("Integer".equals(fieldType) || "int".equals(fieldType)){
                    colField.set(obj, (int)cell.getNumericCellValue());
                }else {
                    colField.set(obj, cell.getNumericCellValue());
                }
            }else if(HSSFCell.CELL_TYPE_BOOLEAN == cellType){
                colField.set(obj, cell.getBooleanCellValue());
            }
        }
        result.add(obj);
    }
} catch (InstantiationException | IllegalAccessException | ParseException e) {
    e.printStackTrace();
}

看完这两段代码,相信大家和我的心情是一样的:

阅读它们的负担实在是太大了——我们要记住好几个逻辑判断分支,才能知道到底什么情况下才能得到那个结果.更别说维护的成本有多高了,每次维护时都要读一遍,然后再基于此来改.长此以往,我们的代码就变成"箭头式代码"了.

    //...............
        //...............
             //...............
                 //...............
                     //...............
                     //...............
                 //...............
             //...............
       //...............
 //...............
目标和关键指标

前面说过,我们的目标减少糟糕的if...else代码.那么什么是糟糕的if...else代码呢?我们可以简单的总结一下:

两重以上的嵌套

一个逻辑分支的判断条件有多个,如:A && B || C这种.其实这也可以看作变种的嵌套

这样就可以看出来,我们的关键指标就是减少嵌套.

常见Tips 1. 三元表达式

三元表达式在代码中也是较为常见的,它可以简化一些if...else,如:

    public Object getFromOpaque(String key) {
        return opaque == null ? null : opaque.get(key);
    }

为什么说是一些呢?因此三元表达式必须要有一个返回值.

这种情况下就没法使用三元表达式

    public void putToOpaque(String key, Object value) {
        if (opaque == null) {
            opaque = new LinkedHashMap();
        }
        opaque.put(key, value);
    }
2. switch case

在Java中,switch可以关注一个变量( byte short int 或者 char,从Java7开始支持String),然后在每个case中比对是否匹配,是的话则进入这个分支.

在通常情况下,switch case的可读性比起if...else会好一点.因为if中可以放复杂的表达式,而switch则不行.话虽如此,嵌套起来还是会很恶心.

因此,如果仅仅是对 byte,short,int和char以String简单的值判断,可以考虑优先使用switch.

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

针对这种情况,我们应该及时抛出异常(或者说return),保证正常流程在外层,如:

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

除了上面的几个tips,我们还可以通过设计模式来避免写出糟糕的if...else语句.在这一节,我们将会提到下面几个设计模式:

State模式

Mediator模式

Observer模式

Strategy模式

1. State模式

在代码中,我们经常会判断一些业务对象的状态来决定在当前的调用下它该怎么做.我们举个例子,现在我们有一个银行的接口:

public interface Bank {
    /**
     * 银行上锁
     * */
    void lock();
    /**
     * 银行解锁
     * */
    void unlock();
    /**
     * 报警
     * */
    void doAlarm();
}

让我们来看一下它的实现类

public class BankImpl implements Bank {
    @Override
    public void lock() {
        //保存这条记录
    }

    @Override
    public void unlock() {
        if ((BankState.Day == getCurrentState())) {
            //白天解锁正常
            //仅仅保存这条记录
        } else if (BankState.Night == getCurrentState()) {
            //晚上解锁,可能有问题
            //保存这条记录,并报警
            doAlarm();
        }
    }

    @Override
    public void doAlarm() {
        if ((BankState.Day == getCurrentState())) {
            //白天报警,联系当地警方,并保留这条记录
        } else if (BankState.Night == getCurrentState()) {
            //晚上报警,可能有事故,不仅联系当地警方,还需要协调附近的安保人员,并保留这条记录
        }
    }


    private BankState getCurrentState() {
        return BankState.Day;
    }
}

显然,我们涉及到了一个状态:

public enum BankState {
    Day,
    Night
}

在不同的状态下,同一件事银行可能会作出不同的反应.这样显然很挫,因为在真实业务场景下,业务的状态可能不仅仅只有两种.每多一种,就要多写一个if...else.所以,如果按照状态模式,可以这样来重构:

public class BankDayImpl implements Bank {
    @Override
    public void lock() {
        //保存这条记录
    }

    @Override
    public void unlock() {
        //白天解锁正常
        //仅仅保存这条记录

    }

    @Override
    public void doAlarm() {
        //白天报警,联系当地警方,并保留这条记录
    }
}
public class BankNightImpl implements Bank {
    @Override
    public void lock() {
        //保存这条记录
    }

    @Override
    public void unlock() {
        //晚上解锁,可能有问题
        //保存这条记录,并报警
        doAlarm();
    }

    @Override
    public void doAlarm() {
        //晚上报警,可能有事故,不仅联系当地警方,还需要协调附近的安保人员,并保留这条记录
    }
}
2. Mediator模式

在本文的第一段的代码中,其实是ZStack 2.0.5版本中某处的代码,它用来防止用户使用Cli时传入不当的参数,导致后面的逻辑运行不正常.为了方便理解,我们可以对其规则做一个简化,并画成图的样子来供大家理解.

假设这是一个提交定时重启VM计划任务的“上古级”界面(因为好的交互设计师一定不会把界面设计成这样吧...).规则大概如下:

2.1 Simple类型的Scheduler

Simple类型的Scheduler,可以根据Interval,RepeatCount,StartTime来定制一个任务.

2.1.1 当选择Simple类型的任务时,Interval,StartTime这两个参数必填

2.1.2 当填好Interval,和StartTime,这个时候已经可以提交定时任务了

2.1.3 RepeatCount是个可选参数

2.2 Cron类型的Scheduler

Cron类型的Scheduler,可以根据cron表达式来提交任务.

2.2.1 当填入cron表达式后,这个时候已经可以提交定时任务了

在这里请大家思考一个问题,如果要写这样的一个界面,该怎么写?——在一个windows类里,先判断上面的可选栏是哪种类型,然后根据文本框里的值是否被填好决定提交按钮属否亮起...这算是基本逻辑.上面还没有提到边界值的校验——这些边界值的校验往往会散落在各个组件的实例里,并通过互相通信的方式来判断自己应该做出什么样的变化,相信大家已经意识到了直接无脑堆if...else代码的恐怖之处了吧.

2.3 使用仲裁者改善它

接下来,我们将会贴上来一些伪代码,方便读者更好的理解这个设计模式

/**
 * 仲裁者的成员接口
 * */
public interface Colleague {
    /**
     * 设置成员的仲裁者
     * */
    void setMediator(Mediator mediator);

    /**
     * 设置成员是否被启用
     * */
    void setColleagueEnabled(boolean enabled);
}
/**
 * 仲裁者接口
 * */
public interface Mediator {
    /**
     * 当一个组员发生状态变化时,调用此方法
     * */
    void colllectValueChanged(String value);
}
/**
 * 含有textField的组件应当实现接口
 */
public interface TextField {
    String getText();
}
/**
 * 当一个组件的值发生变化时,ValueListener会收到相应通知
 * */
public interface ValueListener {
    /**
     * 当组员的值变化时,这个接口会被调用
     * */
    void valueChanged(String str);
}

定义了几个接口之后,我们开始编写具体的类:

用于表示SimpleCron的checkBox

public class CheckBox {
    private boolean state;

    public boolean isState() {
        return state;
    }

    public void setState(boolean state) {
        this.state = state;
    }
}

Button

public class ColleagueButtonField implements Colleague, ValueListener {
    private Mediator mediator;

    @Override
    public void setMediator(Mediator mediator) {
        this.mediator = mediator;
    }

    @Override
    public void setColleagueEnabled(boolean enabled) {
        setEnable(enabled);
    }

    private void setEnable(boolean enable) {
        //当true时去掉下划线,并允许被按下
    }

    @Override
    public void valueChanged(String str) {
        mediator.colllectValueChanged(str);
    }
}

以及几个Text

public class ColleagueTextField implements Colleague, ValueListener, TextField {
    private Mediator mediator;
    private String text;

    @Override
    public void setMediator(Mediator mediator) {
        this.mediator = mediator;
    }

    @Override
    public void setColleagueEnabled(boolean enabled) {
        setEnable(enabled);
    }

    private void setEnable(boolean enable) {
        //当true时去掉下划线,并允许值输入
    }

    @Override
    public void valueChanged(String str) {
        mediator.colllectValueChanged(str);
    }

    @Override
    public String getText() {
        return text;
    }
}

SchedulerValidator的具体实现SchedulerValidatorImpl就不贴上来了,里面仅仅是一些校验逻辑.

接着是我们的主类,也就是知道全局状态的窗口类

public class MainWindows implements Mediator {
    private SchedulerValidator validator = new SchedulerValidatorImpl();
    ColleagueButtonField submitButton, cancelButton;
    ColleagueTextField intervalText, repeatCountText, startTimeText, cronText;
    CheckBox simpleCheckBox, cronCheckBox;


    public void main() {
        createColleagues();
    }

    /**
     * 当一个组员发生状态变化时,调用此方法
     * 组件初始化时都为true
     */
    @Override
    public void colllectValueChanged(String str) {
        if (simpleCheckBox.isState()) {
            cronText.setColleagueEnabled(false);
            simpleChanged();
        } else if (cronCheckBox.isState()) {
            intervalText.setColleagueEnabled(false);
            repeatCountText.setColleagueEnabled(false);
            startTimeText.setColleagueEnabled(false);
            cronChanged();
        } else {
            submitButton.setColleagueEnabled(false);
            intervalText.setColleagueEnabled(false);
            repeatCountText.setColleagueEnabled(false);
            startTimeText.setColleagueEnabled(false);
            cronText.setColleagueEnabled(false);
        }
    }


    private void cronChanged() {
        if (!validator.validateCronExpress(cronText.getText())) {
            submitButton.setColleagueEnabled(false);
        }
    }

    private void simpleChanged() {
        if (!validator.validateIntervalBoundary(intervalText.getText())
                || !validator.validateRepeatCountBoundary(repeatCountText.getText())
                || !validator.validateStartTime(startTimeText.getText())) {
            submitButton.setColleagueEnabled(false);
        }
    }

    private void createColleagues() {
        submitButton = new ColleagueButtonField();
        submitButton.setMediator(this);
        cancelButton = new ColleagueButtonField();
        cancelButton.setMediator(this);

        intervalText = new ColleagueTextField();
        intervalText.setMediator(this);
        repeatCountText = new ColleagueTextField();
        repeatCountText.setMediator(this);
        startTimeText = new ColleagueTextField();
        startTimeText.setMediator(this);
        cronText = new ColleagueTextField();
        cronText.setMediator(this);

        simpleCheckBox = new CheckBox();
        cronCheckBox = new CheckBox();
    }
}


在这个设计模式中,所有实例状态的判断全部都交给了仲裁者这个实例来判断,而不是互相去通信.在目前的场景来看,其实涉及的实例还不是特别多,但在一个复杂的系统中,涉及的实例将会变得非常多.假设现在有A,B两个实例,那么会有两条通信线路:

而有A,B,C时,则有6条线路

当有4个实例时,将会有12个通信线路

当有5个实例时,会有20个通信线路

以此类推...

这个时候,仲裁者模式的优点就发挥出来了——这些逻辑如果分散在各个角色中,代码将会变得难以维护.

3. Observer模式
ZStack源码剖析之设计模式鉴赏——三驾马车

结合本文的主题,其实观察者模式做的更多的是将if...else拆分到属于其自己的模块中.以ZStack的为例,当主存储重连时,主存储模块可能要让模块A和模块B去做一些事,如果不使用观察者模式,那么代码就会都耦合在主存储模块下,拆开if...else也就不太可能了.

改进之前的仲裁者例子

观察者模式一般是通过事件驱动的方式来通信的,因此Observer和Subject一般都是松耦合的——Subject发出通知时并不会指定消费者.而在之前仲裁者模式的例子中,仲裁者和成员之间紧耦合的(即他们必须互相感知),因此可以考虑通过观察者模式来改进它.

4. Strategy模式

通常在编程时,算法(策略)会被写在具体方法中,这样会导致具体方法中充斥着条件判断语句。但是Strategy却特意将算法与其他部分剥离开来,仅仅定义了接口,然后再以委托的方式来使用算法。然而这种做法正是让程序更加的松耦合(因为使用委托可以方便的整体替换算法),使得整个项目更加茁壮。

ZStack源码剖析之设计模式鉴赏——策略模式
小结

在这篇文章中,笔者和大家分享几个减少if...else的小tips,由于这些tips都会有一定的限制,因此还向大家介绍了几个能够避免写出糟糕的if...else的设计模式,并使用观察者模式简单的改进了仲裁者模式的例子.

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/73065.html

相关文章

  • Python 工匠:编写条件分支代码的技巧

    摘要:系列文章工匠善用变量改善代码质量序言编写条件分支代码是编码过程中不可或缺的一部分。而进行条件分支判断时用到的也是这个值重点来了,虽然所有用户类实例的布尔值都是真。 欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 本文由鹅厂优文发表于云+社区专栏 作者:朱雷 | 腾讯IEG高级工程师 『Python 工匠』是什么? 我一直觉得编程某种意义是一门『手艺』,因为优雅而高效的代码...

    KaltZK 评论0 收藏0
  • 论 CSS 中的逻辑

    摘要:天生缺乏逻辑性的问题导致了预处理器的出现。这会导致圈复杂度问题。圈复杂度对于来说可能是一种比较高阶的原则,但如果我们通过它来考量那些蕴含在我们写的选择器中的逻辑性,那我们也许就能写出更加优秀的代码。 本文在征得原作者 @csswizardry 同意的情况下,翻译自他博客中的文章:Cyclomatic Complexity: Logic in CSS。最初发布于我的个人博客:咀嚼之...

    PiscesYE 评论0 收藏0
  • 代码整洁之道》读书笔记

    摘要:看完代码整洁之道之后我受益匪浅,但等到自己实践时却很难按照书中给的建议编写出整洁的代码。意味着新人除了了解代码逻辑之外,还需要学习这种编码语言。代码在演化,注释却不总是随之变动。区隔与靠近空格强调左右两边的分割。 看完《代码整洁之道》之后我受益匪浅,但等到自己实践时却很难按照书中给的建议编写出整洁的代码。一方面是规则太多,记不住,另一方面书上引用了大量示例代码对这些规则进行佐证,在我记...

    liangzai_cool 评论0 收藏0
  • 十个你需要在 PHP 7 中避免的坑

    摘要:不要使用类函数终于,你不用再看到建议不要使用函数的提示了。因为从核心上完全移除了它们,这意味着请你移步至更好的类函数,或者更灵活的层。将从数据库获取一个元数据,如果您正在循环访问特定文章的元数据,则可以在循环中使用它。 showImg(https://segmentfault.com/img/bV75FM?w=1024&h=534); 1. 不要使用 mysql_ 类函数 终于,你不用...

    leanote 评论0 收藏0
  • 谈谈JavaScript异步代码优化

    摘要:异步问题回调地狱首先,我们来看下异步编程中最常见的一种问题,便是回调地狱。同时使用也是异步编程最基础和核心的一种解决思路。基于,目前也被广泛运用,其是异步编程的一种解决方案,比传统的回调函数解决方案更合理和强大。 关于 微信公众号:前端呼啦圈(Love-FED) 我的博客:劳卜的博客 知乎专栏:前端呼啦圈 前言 在实际编码中,我们经常会遇到Javascript代码异步执行的场景...

    chnmagnus 评论0 收藏0

发表评论

0条评论

huhud

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<