本次博客的內容,我將帶領大家一起來看看我們日常編碼工作中遇到最多的一個語法——條件表達式。他在我們的邏輯流程控制中扮演着不可或缺的重要角色,那么怎樣才能夠寫出高質量的條件表達式,高質量的條件分支呢?
備注:本次講解的例子只具備普遍性,不具有特殊性,針對一些特殊的業務邏輯,需要根據業務的實際情況靈活處理,切不可死記硬背,把這些重構方法硬套在具備某些特征的場合中。
1.分解條件表達式
首先我們來看一段代碼,如果你覺得這段代碼和你平常的寫法沒什么差別,那么就說明你的編碼習慣就有待優化咯思密達,你就需要繼續往下看咯:
1 if (date.before(SUMMER_START) && date.after(WINTER_END)) 2 { 3 charge = quantity * winterRate + winterServiceCharge; 4 } else 5 { 6 charge = quantity * summerRate; 7 }
這段代碼所包含的邏輯很簡單,但是有什么可以優化的地方呢?那么我們再看看下面這段代碼:
1 if (notSummer(date)) 2 { 3 charge = winterCharge(quantity); 4 } else 5 { 6 charge = summerCharge(quantity); 7 }
甚至可以這樣縮寫:
1 charge = notSummer(date) ? winterCharge(quantity) : summerCharge(quantity);
看到下面兩種寫法與上面的寫法以后,你心里會想到什么呢?程序之中,復雜的條件邏輯是最常見的導致復雜度上升的位置之一。雖然說這種重構相對於其他大型重構來說太渺小,但是殊不知再大的難讀的代碼都是通過這種小問題累加而成的。遇到這種情況下,我們只需要遵循一個原則:分支邏輯與操作細節分離。分支的目的在於控制程序的流向,重點不在於邏輯控制的細節,操作的細節那是方法所應該做的事情。程序要做到最小原子性,單一原則,那么你的程序的可讀性,可復用性就會提高很多。
這種重構手法主要適用的場景:條件分支中,條件判斷很復雜,一般包含兩個或以上邏輯操作,並且不通的流程分支都有不同的業務流程控制,那么這種重構手法就可以派上用場了。
遇到這種問題的一般做法就是將條件判斷部分,每一個分支的邏輯處理部分都單獨的提成一個方法,這樣就能大大提高程序的可讀性,使代碼結構更加清晰,這也是代碼重構工作中很重要的一個環節,也是面向接口編程,面向對象編程的一個重要體現。
2.合並條件表達式
看到這里很多的小伙伴們可能就有疑問了,上面是要分解條件表達式,我也看懂了博主的意思,確實是那么回事兒,那么為何下面又要合並條件表達式呢?很費解!那么這里我也就先賣個關子,照舊,先看代碼:
1 public String getStuLevel(int score) 2 { 3 if (score == 100) 4 { 5 return "A"; 6 } else if (score >= 90) 7 { 8 return "A"; 9 } else if (score >= 80) 10 { 11 return "B"; 12 } else if (score >= 70) 13 { 14 return "B"; 15 } else if (score >= 60) 16 { 17 return "C"; 18 } else 19 { 20 return "D"; 21 } 22 }
這個代碼估計應該能夠看出可以優化的地方(這里主要討論的是重構手法,部分代碼書寫可能沒有遵循編碼規范,請知),那么看看下面的優化結果是不是和你所想的一致:
1 public String getStuLevel(int score) 2 { 3 if (score >= 90) 4 { 5 return "A"; 6 } else if (score >= 70) 7 { 8 return "B"; 9 } else if (score >= 60) 10 { 11 return "C"; 12 } else 13 { 14 return "D"; 15 } 16 }
看到這里,很多小伙伴都會說,這不是小兒科嘛,用腳趾頭想都能想到要這樣優化。哈哈,確實很簡單我就不多廢話,其中的好處我相信大家都也能看出來。
這種重構手法主要適用的場景:在條件分支中,雖然檢查的條件不一致,但是最終所流向的行為是一致的,那么我就可以將這些表現行為一致的分支邏輯合並起來,結合上面的“分解條件表達式”的重構手法,合並不必要的邏輯條件判斷,從而提高代碼的可讀性。
3.合並重復的邏輯片段
在實際開發中我們可能會遇到如下這種代碼冗余的情況,遇到這種場景,你首先就應該想到能不能重構:
1 public void takeBus(double high) 2 { 3 double price = 0.0d; 4 if (high > 1.2) 5 { 6 price = 100; 7 takeBus(); 8 } else 9 { 10 price = 100 * 0.5; 11 takeBus(); 12 } 13 }
優化結果如下:
1 public void takeBus(double high) 2 { 3 double price = 0.0d; 4 if (high > 1.2) 5 { 6 price = 100; 7 } else 8 { 9 price = 100 * 0.5; 10 } 11 takeBus(); 12 }
這種和上面的例子也是一樣,都是小兒科,這種優化在於鑒別出"執行方式不隨條件變化而變化"的代碼。
這種重構手法主要適用的場景:一組條件表達式的所有分支都執行了一段相同的邏輯。如果是這樣,你就應該將這段代碼移動到條件表達式外面。這樣,代碼才能更清楚地表明哪些東西是隨着條件變化而變化的,哪些東西是永遠都不會變化的。
4.移除控制標記
當我講到這種重構手法的時候,對於那些剛從C轉到JAVA或者長期從事結構化編程的人員來說,就很重要了。這一點重構手法能夠讓你擺脫結構化編程的“單一原則(一個程序只能有一個入口和一個出口)”的束縛。
1 public static void main(String[] args) 2 { 3 boolean flag = true; 4 for (int i = 0; i < args.length; i++) 5 { 6 if (flag) 7 { 8 if ("ZhangGe".equals(args[i])) 9 { 10 flag = false; 11 } 12 } 13 } 14 }
這種編碼習慣很常見,尤其是在當前的多線程編程中。這里我們就把多線程編程當做一種特殊情況處理吧,至於為什么不在本篇博客的討論范圍之內。對於這種代碼的優化我相信大家都已經想到了辦法吧。
1 public static void main(String[] args) 2 { 3 for (int i = 0; i < args.length; i++) 4 { 5 if ("ZhangGe".equals(args[i])) 6 { 7 return; 8 } 9 } 10 }
這樣的控制標記帶來的麻煩現在已經遠遠超過於它所帶來的方便了。人們之所以會用這種方式是因為結構化編程原則告訴我們:每一個子程序只能有一個入口和一個出口。雖然說我們在編碼的過程中要遵循“單一原則”。但是“單一出口”原則會讓我們在代碼中加入這樣的boolean類型的標志,大大降低了條件表達式的可讀性,感覺有種條件表達式濫用的感覺。這也就是為什么相JAVA這樣的語言要引入return、break和continue這些關鍵字的原因。
這種重構手法主要適用的場景:在循環體以內有一個表達式或者標志用來判斷循環條件是否繼續的場景,通過修改條件表達式或變量來控制循環體是否繼續執行。這種結構的代碼都可以通過break,continue和return等方式結束循環,刪除標志控制,提高程序的可讀性。
5.以衛語句取代條件表達式
首先看看下面這段代碼:
1 public double getPayAmount() 2 { 3 double result; 4 if (idDead) 5 { 6 result = deadAmount(); 7 } else 8 { 9 if (isSeparated) 10 { 11 result = separatedAmount(); 12 } else 13 { 14 if (isRetired) 15 { 16 result = retiredAmount(); 17 } else 18 { 19 result = normalPayAmount(); 20 } 21 } 22 } 23 return result; 24 }
有人看到這段代碼后,心里面會有種怪怪的感覺,覺得這代碼感覺確實是有優化的地方,但是就是找不出來怎么優化,如果你一眼就看出來怎么優化,so you did a good job! 那我們就來看看怎么優化這段代碼吧:
1 public double getPayAmount() 2 { 3 if (idDead) 4 { 5 return deadAmount(); 6 } 7 if (isSeparated) 8 { 9 return separatedAmount(); 10 } 11 if (isRetired) 12 { 13 return retiredAmount(); 14 } 15 return normalPayAmount(); 16 }
哈哈,這里的優化是不是和你所想的一樣呢?或者說你有什么更好的見地?
下面來總結一下。一般的條件分支有兩種情況,第一種就是所有的分支都屬於正常行為;第二種則是條件表達式中提供的答案只有一個是正常的行為,其他的行為都不常見。如果多條分支都屬於正常行為,我們就直接使用if...(else if...)else...的形式;如果某個條件極其罕見,那么我們就應該單獨檢查該條件,並且在該條件返回為真時立刻從函數中返回。這樣的單獨檢查常常被稱為“衛語句(guard clauses)”。
以衛語句取代嵌套條件表達式的精髓就是:給某一條分支予以特別的重視。如果用if...else...的結構,那么每一個條件分支的重要性都是同等的,這樣的代碼結構給別人的感覺就是各個分支的重要性是一樣的。然而衛語句就不同了,他會告訴讀者:“這種情況很罕見,如果它真的發生了,請做一些重要的處理,然后退出!”。謂語句要不從函數中直接返回,要么就拋出一個異常。
這種重構手法主要適用的場景:在條件分支中,針對一些特殊的分支,為了強調其重要性,就可以使用衛語句來強調某一個分支的邏輯重要性。
6.以多態取代條件表達式
先看如下這段代碼:
1 public static void main(String[] args) 2 { 3 System.out.println(doCompute("3*2")); 4 } 5 6 public static double doCompute(String intStr) 7 { 8 if (intStr.contains("+")) 9 { 10 String[] split = intStr.split("\\+"); 11 return Integer.parseInt(split[0]) + Integer.parseInt(split[1]); 12 } else if (intStr.contains("-")) 13 { 14 String[] split = intStr.split("-"); 15 return Integer.parseInt(split[0]) - Integer.parseInt(split[1]); 16 } else if (intStr.contains("*")) 17 { 18 String[] split = intStr.split("\\*"); 19 return Integer.parseInt(split[0]) * Integer.parseInt(split[1]); 20 } else if (intStr.contains("/")) 21 { 22 String[] split = intStr.split("/"); 23 return Integer.parseInt(split[0]) / Integer.parseInt(split[1]); 24 } else 25 { 26 return 0d; 27 } 28 }
查看到上述代碼時,你的是否有種丈二的和尚摸不着頭腦啊?上述的代碼一看就知道不是什么好代碼,但是這樣的代碼可擴展性確實不咋地,以后我要是要添加點其他算數運算的方法呢?怎么辦,繼續添加else if...分支?以前我相信很多小伙伴還是想過這個問題,面對着這種東西代碼我該怎么優化是吧,只是沒有找到更好的解決辦法,那么今天我就給大家引入一種方式來看看,那么我們接着往下走:
1 public abstract class AbstractCompute 2 { 3 String computStr = null; 4 5 abstract double compute(); 6 }
1 public class AddComput extends AbstractCompute 2 { 3 public AddComput(String string) 4 { 5 this.computStr = string; 6 } 7 8 @Override 9 double compute() 10 { 11 if (computStr != null) 12 { 13 String[] split = computStr.split("\\+"); 14 return Integer.parseInt(split[0]) + Integer.parseInt(split[1]); 15 } 16 return 0; 17 } 18 }
1 public class RefactorWay 2 { 3 public static void main(String[] args) 4 { 5 AbstractCompute addComput = new AddComput("3+2"); 6 System.out.println(addComput.compute()); 7 } 8 }
看到上面的代碼后是不是有種柳暗花明的感覺呢?是的,這種重構手法就是將我們每一個操作作為一個對象進行封裝,其內部來實現分支的邏輯處理。是不是很巧妙啊!這樣的方式就是利用了java面向對象編程的三大特點之一的多態,通過多態來實現條件分支的處理,從而提升代碼的逼格,瞬間變為高富帥,呵呵。
這種重構手法主要適用的場景:當然是條件分支中,每個分支都具有同等的又不同的邏輯操作,那么我們就可以利用多來實現程序的擴展。當然這種重構手法不是用來取代條件分支的,而是當我們的條件分支在現在或者未來會達到一定數目的情況,可以采用這種手法來提升程序的可擴展性。
7.引入null對象
面向對象的程序中有一個語法叫做多態,在多態的實現中,我們不需要詢問對象“你是什么類型的?”而后再根據得到的對象來調用其行為,你只管調用其方法就行了,其他的一切多態機制都會幫你安排妥當。對於這個思想不知道大家是否都有想到用到我們的代碼中去,那么該如何運用呢?
要運用實現上述方法,我們需要引入一個null對象,這里的null對象不是直接返回一個null,而是頂一個class繼承自原來的類,然后提供一個方法isEmpty();父類返回false,子類(null對象)返回true,這樣只要在有空判斷的地方,我們都可以直接調用isEmpty()等方法解決我們程序中的為空校驗,編碼這樣的ugly代碼。
定義一個這種類型后,其他地方也可以引用,只要是設計到為空判斷的地方,子類都可以重寫一個父類的這個方法,去避免條件表達式所引入的為空判斷,提高代碼的可讀性。
8.引入斷言
直接看代碼吧:
1 public double getExpenseLimit() 2 { 3 return (expenseLimit != NULL_EXPENSE) ? expenseLimit : primaryProject; 4 }
引入斷言后代碼可以這樣寫:
1 public double getExpenseLimit() 2 { 3 Assert.isTrue(expenseLimit != NULL_EXPENSE || primaryProject != null); 4 return (expenseLimit != NULL_EXPENSE) ? expenseLimit : primaryProject; 5 }
引入斷言后,能夠幫助程序員定位問題所在;便於代碼調試;對代碼沒有代價。
好了,以上都是條件分支相關的重構技巧,其中的很多技巧我相信很多小伙伴們都知道,但是就是沒有總結過,當你有一些開發經驗以后你會發現其實你已經會了很多,只是沒有過系統的總結。學習的過程就是不斷總結的過程,多總結,你就會領悟很多。軟件思想也不外乎一個“理”字,只要領會其中的規律,你對編程有會有一個更高的認識。
如果你覺得本博文對你有所幫助,請記得點擊右下方的"推薦"哦,么么噠...
轉載請注明出處:http://www.cnblogs.com/liushaofeng89/p/4993294.html
