上期我們分享了關於Java中equals與hashCode的理解
本期我們將分享Java中if/else複雜邏輯的處理
在github上曾看到一些issue,國外的程式設計師比較忌諱寫else,看到了很多這樣的評論
else is horrible
,那麼對於邏輯很複雜的程式碼段,如果用太多的if/else的話,那麼會導致程式碼的閱讀難度變大,同時會增加程式碼的圈複雜度
,理論上,如果一個函式的圈複雜度超過8,那麼這個函式就還有可優化的地方,那麼如何優化這種多分支的複雜邏輯的函式呢?手冊中給出了三種方法:衛語句
、策略模式
、狀態模式
,通過閱讀《重構:改善既有程式碼的設計》
發現,解決這個問題其實有很多種,下面我們就一一道來。
第一大類:重新組織函式
1、Extract Method(提煉函式)
這種方法應該是最常用的方法之一,當函式過長或者分支太多的話,就可以考慮將其中的一段程式碼提煉成一個獨立的函式。
- 原始程式碼:
public void today() {
if (isBusy()) {
System.out.println("change time.");
} else if (isFree()) {
System.out.println("go to travel.");
} else {
System.out.println("stay at home to learn Alibaba Java Coding Guidelines.");
}
}
複製程式碼
- 改後程式碼:
public void today() {
if (isBusy()) {
changeTime();
} else if (isFree()) {
goToTravel();
} else {
stayAtHomeToLearn();
}
}
private void changeTime() {
System.out.println("change time.");
}
private void goToTravel() {
System.out.println("go to travel.");
}
private void stayAtHomeToLearn() {
System.out.println("stay at home to learn Alibaba Java Coding Guidelines.");
}
複製程式碼
用法:
- 提煉新函式,根據這個函式的意圖來命名,以它做什麼來命名,而不是以他怎麼做來命名
- 仔細檢查提煉出的程式碼是否引用了作用域限於源函式的變數,包括區域性變數和源函式引數
- 適用場景:函式過長或者需要註釋才能讓人理解用途的程式碼
2、Substitute Algorithm(替換演算法)
把某個演算法替換成另一個更清晰的演算法,或將函式本體替換為另一個演算法。
- 原始程式碼:
public String foundPerson(String[] people) {
for (int i = 0; i < people.length; i++) {
if (people[i].equals("Don")) {
return "Don";
}
if (people[i].equals("John")) {
return "John";
}
if (people[i].equals("Kent")) {
return "Kent";
}
}
return "";
}
複製程式碼
- 改後程式碼:
public String foundPerson(String[] people) {
List<String> candidates = Arrays.asList(new String[] { "Don", "John", "Kent" });
for (int i = 0; i < people.length; i++) {
if (candidates.contains(people[i])) {
return people[i];
}
}
return "";
}
複製程式碼
用法:
- 準備好另一個替換用的演算法
- 新演算法,要與原本的演算法結果相同
- 適用場景:把某個演算法替換為更清晰的演算法,或者把函式替換為一個演算法
第二大類:簡化條件表示式
1、Eecompose Conditional(分解條件表示式)
如果有複雜的條件(if-then-else)語句,從if、then、else三個段落中分別提煉出獨立函式。
- 原始程式碼:
public void today() {
if (isBusy() || isNotWeekend()) {
System.out.println("change time.");
return;
} else {
System.out.println("go to travel.");
}
}
複製程式碼
- 改後程式碼:
public void today() {
if (notFree()) {
changeTime();
} else {
goToTravel();
}
}
private boolean notFree() {
return isBusy() || isNotWeekend();
}
private void changeTime() {
System.out.println("change time.");
}
private void goToTravel() {
System.out.println("go to travel.");
}
複製程式碼
用法:
- 將if段落提煉出來,構成一個獨立函式
- 將then段落和else段落都提煉出來,各自構成一個獨立函式
- 適用場景:複雜的條件語句。如果發現巢狀的條件邏輯,先觀察是否可以使用衛語句,如果不行,再開始分解其中的每個條件
2、Consolidate Conditioinal Expression(合併條件表示式)
如果有一系列條件測試,都得到相同結果,將這些測試合併為一個條件表示式,並將這個條件表示式提煉成為一個獨立函式。
- 原始程式碼:
public void today() {
if (isWeekend()) {
System.out.println("go to travel.");
}
if (isHoliday()) {
System.out.println("go to travel.");
}
if (noWork()) {
System.out.println("go to travel.");
}
}
複製程式碼
- 改後程式碼:
public void today() {
if (isFree()) {
System.out.println("go to travel.");
}
}
private boolean isFree() {
return isWeekend() || isHoliday() || noWork();
}
複製程式碼
用法:
- 確定這些條件語句都沒有副作用
- 使用適當的邏輯操作符,將一系列相關條件表示式合併為一個,並對合並後的表示式提煉函式
- 適用場景:一系列條件測試,都得到相同的結果
3、Consolidate Duplicate Conditional Clauses(合併重複的條件判斷)
在條件表示式的每個分支上有相同的一段程式碼,將這段程式碼搬移到條件表示式之外。
- 原始程式碼:
public void today() {
if (isBusy()) {
System.out.println("change time.");
sleep();
} else if (isFree()) {
System.out.println("go to travel.");
sleep();
} else {
System.out.println("stay at home to learn Alibaba Java Coding Guidelines.");
sleep();
}
}
複製程式碼
- 改後程式碼:
public void today() {
if (isBusy()) {
System.out.println("change time.");
} else if (isFree()) {
System.out.println("go to travel.");
} else {
System.out.println("stay at home to learn Alibaba Java Coding Guidelines.");
}
sleep();
}
複製程式碼
用法:
- 鑑別出執行方式不隨條件變化而變化的的程式碼
- 如果這些共同程式碼位於條件表示式起始處,就將它移到條件表示式之前,如果在尾端,移到條件表示式之後
- 適用場景:在條件表示式的每個分支上有相同的一段程式碼
4、Remove Control Flag(移除控制標記)
在一系列布林表示式中,某個變數帶有“控制標記(Flag)”的作用,以break語句或者return語句取代控制標記。
5、Replace Nested Confitional with Guard Clauses(以衛語句取代巢狀條件表示式)
如果多個分支都屬於正常行為,就應該使用if...else...的條件表示式,如果某個條件極其罕見,就應該單獨檢查該條件,並在該條件為真時立刻從函式中返回,這樣的單獨檢查常常被稱為衛語句。
- 原始程式碼:
public void today() {
if (isBusy()) {
System.out.println("change time.");
} else if (isFree()) {
System.out.println("go to travel.");
} else {
System.out.println("stay at home to learn Alibaba Java Coding Guidelines.");
}
}
複製程式碼
- 改後程式碼:
public void today() {
if (isBusy()) {
System.out.println("change time.");
return;
}
if (isFree()) {
System.out.println("go to travel.");
return;
}
System.out.println("stay at home to learn Alibaba Java Coding Guidelines.");
return;
}
複製程式碼
用法:
- 對於每個檢查,放進一個衛語句,衛語句要麼就從函式中返回,要麼就丟擲一個異常
- 每次將條件檢查替換成衛語句後,編譯並測試:如果所有的衛語句都導致同樣的結果,請使用
合併條件表示式
- 適用場景:使用衛語句返回所有特殊情況
6、Replace Conditional with Polymorphism(以多型取代條件表示式)
條件表示式,根據物件的型別選擇不同的行為,將這個條件表示式的每個分支放進一個子內的覆寫函式中,然後將原始函式宣告為抽象函式,這一項就是手冊中說的策略模式以及狀態模式。 正因為有了多型,所以“型別碼的switch語句”以及“基於型別名稱的if-then-else”語句在物件導向程式中很少出現。
第三大類:簡化函式呼叫
1、Separate Query from Modifier(將查詢函式和修改函式分離)
某個函式既返回物件狀態值,又修改了狀態,建立兩個不同的函式,其中一個負責查詢,另一個負責修改。
- 併發的情況:需要保留第三個函式來同時做這兩件事
2、Parameterize Method(令函式攜帶引數)
若干函式做了類似的工作,但在函式本體中卻包含了不同的值,建立單一函式,以參數列達那些不同的值。
- 原始程式碼:
public void tenPercentRaise() {
salary *= 1.1;
}
public void fivePercentRaise() {
salary *= 1.05;
}
複製程式碼
- 改後程式碼:
public void raise(double factor) {
salary *= (1 + factor);
}
複製程式碼
- 要點在於:以可將少量數值視為引數為依據,找出帶有重複性的程式碼
3、Replace Parameter with Explicit Methods(以明確函式取代引數)
有一個函式,其中完全取決於引數值而採取不同行為,針對該引數的每一個可能值,建立一個獨立的函式。
- 原始程式碼:
public void setValue(String name, int value) {
if (name.equals("height")) {
height = value;
}
if (name.equals("width")) {
width = value;
}
}
複製程式碼
- 改後程式碼:
public void setHeight(int arg) {
height = arg;
}
public void setWidth(int arg) {
width = arg;
}
複製程式碼
用法:
- 針對引數的每一種可能值,新建一個明確的函式
- 修改條件表示式的每個分支,使其呼叫合適的新函式
- 適用場景:函式完全取決於引數值而採取不同行為