如何避免寫出糟糕if...else語句,相信很多沒有經驗的人對此束手無策,為此本文總結了問題出現(xiàn)的原因和解決方法,通過這篇文章希望你能解決這個問題。
為天津等地區(qū)用戶提供了全套網頁設計制作服務,及天津網站建設行業(yè)解決方案。主營業(yè)務為網站制作、成都網站制作、天津網站設計,以傳統(tǒng)方式定制建設網站,并提供域名空間備案等一條龍服務,秉承以專業(yè)、用心的態(tài)度為用戶提供真誠的服務。我們深信只要達到每一位用戶的要求,就會得到認可,從而選擇與我們長期合作。這樣,我們也可以走得更遠!
在寫代碼的日常中,if...else語句是極為常見的。正因其常見性,很多同學在寫代碼的時候并不會去思考其在目前代碼中的用法是否妥當。而隨著項目的日漸發(fā)展,糟糕的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
三元表達式在代碼中也是較為常見的,它可以簡化一些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簡單的值判斷,可以考慮優(yōu)先使用switch。
/* 查找年齡大于18歲且為男性的學生列表 */ public ArrayListgetStudents(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 ArrayListgetStudents(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語句。在這一節(jié),我們將會提到下面幾個設計模式:
State模式
Mediator模式
Observer模式
Strategy模式
在代碼中,我們經常會判斷一些業(yè)務對象的狀態(tài)來決定在當前的調用下它該怎么做。我們舉個例子,現(xiàn)在我們有一個銀行的接口:
public interface Bank { /** * 銀行上鎖 * */ void lock(); /** * 銀行解鎖 * */ void unlock(); /** * 報警 * */ void doAlarm(); }
讓我們來看一下它的實現(xiàn)類
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())) { //白天報警,聯(lián)系當?shù)鼐?,并保留這條記錄 } else if (BankState.Night == getCurrentState()) { //晚上報警,可能有事故,不僅聯(lián)系當?shù)鼐?,還需要協(xié)調附近的安保人員,并保留這條記錄 } } private BankState getCurrentState() { return BankState.Day; } }
顯然,我們涉及到了一個狀態(tài):
public enum BankState { Day, Night }
在不同的狀態(tài)下,同一件事銀行可能會作出不同的反應。這樣顯然很挫,因為在真實業(yè)務場景下,業(yè)務的狀態(tài)可能不僅僅只有兩種。每多一種,就要多寫一個if...else。所以,如果按照狀態(tài)模式,可以這樣來重構:
public class BankDayImpl implements Bank { @Override public void lock() { //保存這條記錄 } @Override public void unlock() { //白天解鎖正常 //僅僅保存這條記錄 } @Override public void doAlarm() { //白天報警,聯(lián)系當?shù)鼐?,并保留這條記錄 } }
public class BankNightImpl implements Bank { @Override public void lock() { //保存這條記錄 } @Override public void unlock() { //晚上解鎖,可能有問題 //保存這條記錄,并報警 doAlarm(); } @Override public void doAlarm() { //晚上報警,可能有事故,不僅聯(lián)系當?shù)鼐?,還需要協(xié)調附近的安保人員,并保留這條記錄 } }
在本文的第一段的代碼中,其實是ZStack 2.0.5版本中某處的代碼,它用來防止用戶使用Cli時傳入不當?shù)膮?shù),導致后面的邏輯運行不正常。為了方便理解,我們可以對其規(guī)則做一個簡化,并畫成圖的樣子來供大家理解。
假設這是一個提交定時重啟VM計劃任務的“上古級”界面(因為好的交互設計師一定不會把界面設計成這樣吧...).規(guī)則大概如下:
Simple類型的Scheduler,可以根據Interval,RepeatCount,StartTime來定制一個任務。
Cron類型的Scheduler,可以根據cron表達式來提交任務。
在這里請大家思考一個問題,如果要寫這樣的一個界面,該怎么寫?——在一個windows類里,先判斷上面的可選欄是哪種類型,然后根據文本框里的值是否被填好決定提交按鈕屬否亮起...這算是基本邏輯。上面還沒有提到邊界值的校驗——這些邊界值的校驗往往會散落在各個組件的實例里,并通過互相通信的方式來判斷自己應該做出什么樣的變化,相信大家已經意識到了直接無腦堆if...else代碼的恐怖之處了吧。
接下來,我們將會貼上來一些偽代碼,方便讀者更好的理解這個設計模式
/** * 仲裁者的成員接口 * */ public interface Colleague { /** * 設置成員的仲裁者 * */ void setMediator(Mediator mediator); /** * 設置成員是否被啟用 * */ void setColleagueEnabled(boolean enabled); }
/** * 仲裁者接口 * */ public interface Mediator { /** * 當一個組員發(fā)生狀態(tài)變化時,調用此方法 * */ void colllectValueChanged(String value); }
/** * 含有textField的組件應當實現(xiàn)接口 */ public interface TextField { String getText(); }
/** * 當一個組件的值發(fā)生變化時,ValueListener會收到相應通知 * */ public interface ValueListener { /** * 當組員的值變化時,這個接口會被調用 * */ void valueChanged(String str); }
定義了幾個接口之后,我們開始編寫具體的類:
用于表示Simple和Cron的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的具體實現(xiàn)SchedulerValidatorImpl就不貼上來了,里面僅僅是一些校驗邏輯.
接著是我們的主類,也就是知道全局狀態(tài)的窗口類
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(); } /** * 當一個組員發(fā)生狀態(tài)變化時,調用此方法 * 組件初始化時都為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(); } }
在這個設計模式中,所有實例狀態(tài)的判斷全部都交給了仲裁者這個實例來判斷,而不是互相去通信。在目前的場景來看,其實涉及的實例還不是特別多,但在一個復雜的系統(tǒng)中,涉及的實例將會變得非常多。假設現(xiàn)在有A,B兩個實例,那么會有兩條通信線路:
而有A,B,C時,則有6條線路
當有4個實例時,將會有12個通信線路
當有5個實例時,會有20個通信線路
以此類推...
這個時候,仲裁者模式的優(yōu)點就發(fā)揮出來了——這些邏輯如果分散在各個角色中,代碼將會變得難以維護。
ZStack源碼剖析之設計模式鑒賞——三駕馬車
https://segmentfault.com/a/1190000012903365
結合本文的主題,其實觀察者模式做的更多的是將if...else拆分到屬于其自己的模塊中。以ZStack的為例,當主存儲重連時,主存儲模塊可能要讓模塊A和模塊B去做一些事,如果不使用觀察者模式,那么代碼就會都耦合在主存儲模塊下,拆開if...else也就不太可能了。
觀察者模式一般是通過事件驅動的方式來通信的,因此Observer和Subject一般都是松耦合的——Subject發(fā)出通知時并不會指定消費者。而在之前仲裁者模式的例子中,仲裁者和成員之間緊耦合的(即他們必須互相感知),因此可以考慮通過觀察者模式來改進它。
通常在編程時,算法(策略)會被寫在具體方法中,這樣會導致具體方法中充斥著條件判斷語句。但是Strategy卻特意將算法與其他部分剝離開來,僅僅定義了接口,然后再以委托的方式來使用算法。然而這種做法正是讓程序更加的松耦合(因為使用委托可以方便的整體替換算法),使得整個項目更加茁壯。
看完上述內容,你們掌握如何避免寫出糟糕if...else語句的方法了嗎?如果還想學到更多技能或想了解更多相關內容,歡迎關注創(chuàng)新互聯(lián)行業(yè)資訊頻道,感謝各位的閱讀!