談談代碼——如何避免寫出糟糕if...else語句
在寫代碼的日常中,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<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語句。在這一節,我們將會提到下面幾個設計模式:
- 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);
}
定義了幾個介面之後,我們開始編寫具體的類:
用於表示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的具體實現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的設計模式,並使用觀察者模式簡單的改進了仲裁者模式的例子。
| 作者簡介
泊浮目·沃趣科技研發工程師
ZStack愛好者,曾就職於ZStack.目前主要負責公司的Iaas平臺開發,對於代碼整潔之道和自動化測試有著孜孜不倦的追求。
推薦閱讀: