談談代碼——如何避免寫出糟糕if...else語句

本文首發於 數據浮雲: https://mp.weixin.qq.com/s?__...

在寫代碼的平常中,if...else語句是極爲常見的.正因其常見性,不少同窗在寫代碼的時候並不會去思考其在目前代碼中的用法是否穩當.而隨着項目的日漸發展,糟糕的if...else語句將會充斥在各處,讓項目的可維護性急劇降低.故在這篇文章中,筆者想和你們談談如何避免寫出糟糕if...else語句.java

因爲脫密等緣由.文章中的示例代碼將會用一些開源軟件的代碼或者抽象過的生產代碼做爲示範.

問題代碼

當咱們看到一組if...else時,通常是不會有什麼閱讀負擔的.但當咱們看到這樣的代碼時:mysql

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();
}

看完這兩段代碼,相信你們和個人心情是同樣的:
sql

閱讀它們的負擔實在是太大了——咱們要記住好幾個邏輯判斷分支,才能知道到底什麼狀況下才能獲得那個結果.更別說維護的成本有多高了,每次維護時都要讀一遍,而後再基於此來改.久而久之,咱們的代碼就變成"箭頭式代碼"了.編程

//...............
        //...............
             //...............
                 //...............
                     //...............
                     //...............
                 //...............
             //...............
       //...............
 //...............

目標和關鍵指標

前面說過,咱們的目標減小糟糕的if...else代碼.那麼什麼是糟糕的if...else代碼呢?咱們能夠簡單的總結一下:segmentfault

  • 兩重以上的嵌套
  • 一個邏輯分支的判斷條件有多個,如:A && B || C這種.其實這也能夠看做變種的嵌套

這樣就能夠看出來,咱們的關鍵指標就是減小嵌套.windows

常見Tips

1. 三元表達式

三元表達式在代碼中也是較爲常見的,它能夠簡化一些if...else,如:設計模式

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

爲何說是一些呢?所以三元表達式必需要有一個返回值.ide

這種狀況下就無法使用三元表達式ui

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<Student> getStudents(int uid){
        ArrayList<Student> result = new ArrayList<Student>();
        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 {
                    throw new MyException("獲取學生列表失敗");
                }
            }else {
                throw new MyException("獲取老師信息失敗");
            }
        } else {
            throw new MyException("獲取學生信息失敗");
        }
        return result;
    }

針對這種狀況,咱們應該及時拋出異常(或者說return),保證正常流程在外層,如:

/* 查找年齡大於18歲且爲男性的學生列表 */
    public ArrayList<Student> getStudents(int uid){
        ArrayList<Student> result = new ArrayList<Student>();
        Student stu = getStudentByUid(uid);
        if (stu == null) {
             throw new MyException("獲取學生信息失敗");
        }
 
        Teacher teacher = stu.getTeacher();
        if(teacher == null){
             throw new MyException("獲取老師信息失敗");
        }
 
        ArrayList<Student> 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語句.在這一節,咱們將會提到下面幾個設計模式:

  1. State模式
  2. Mediator模式
  3. Observer模式
  4. 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的設計模式,並使用觀察者模式簡單的改進了仲裁者模式的例子.

相關文章
相關標籤/搜索