if-else 超過三層之後,代碼的可讀性就會大大降低。可以使用衛語句、策略模式、狀態模式來改善代碼結構。
具體方案如下:
- 衛語句
參考鏈接:https://blog.csdn.net/jw903/article/details/455067771.使用衛語句取代嵌套表達式
函數中的條件邏輯使人難以看清正常的執行途徑。使用衛語句表現所有特殊情況。
動機:條件表達式通常有2種表現形式。第一:所有分支都屬於正常行爲。第二:條件表達式提供的答案中只有一種是正常行爲,其他都是不常見的情況。
這2類條件表達式有不同的用途。如果2條分支都是正常行爲,就應該使用形如if…..else…..的條件表達式;如果某個條件極其罕見,就應該單獨檢查該條件,並在該條件爲真時立刻從函數中返回。這樣的單獨檢查常常被稱爲“衛語句”。
Replace Nested Conditional with Guard Clauses (以衛語句取代嵌套條件表達式)的精髓是:給某個分支以特別的重視。它告訴閱讀者:這種情況很罕見,如果它真的發生了,請做一些必要的整理工作,然後退出。
“每個函數只能有一個入口和一個出口”的觀念,根深蒂固於某些程序員的腦海裏。現今的編程語言都會強制保證每個函數只有一個入口,至於“單一出口”規則,其實不是那麼有用。保持代碼清晰纔是最關鍵的:如果單一出口能使這個函數更清晰易讀,那麼就使用單一出口;否則就不必這麼做。
做法:1、對於每個檢查,放進一個衛語句。衛語句要不就從函數返回,要不就拋出一個異常。
2、每次將條件檢查替換成衛語句後,編譯並測試。如果所有衛語句都導致相同的結果,請使用 Consolidate Conditional Expression (合併條件表達式)。
2.衛語句就是把複雜的條件表達式拆分成多個條件表達式,比如一個很複雜的表達式,嵌套了好幾層的if - then-else語句,轉換爲多個if語句,實現它的邏輯,這多條的if語句就是衛語句.
3有時候條件式可能出現在嵌套n次才能真正執行,其他分支只是簡單報錯返回的情況,對於這種情況,應該單獨檢查報錯返回的分支,當條件爲真時立即返回,這樣的單獨檢查就是衛語句(guard clauses).衛語句可以把我們的視線從異常處理中解放出來,集中精力到正常處理的代碼中。
例如下列代碼:
void func(void)
{
if(IsWorkDay())
{
printf("Error,is work day");
}
else
{
if(IsWorkTime())
{
printf("Error ,is work time");
}
else
{
rest();
}
}
}使用衛語句替換以後
void func()
{
if(IsWorkDay())
{
printf("Error,is work day");
return;
}
if(IsWorkTime())
{
printf("Error,is work time");
return ;
}
rest();
} - 策略模式
參考鏈接:https://www.cnblogs.com/wkzhao/p/10229412.html什麼是策略模式?其思想是針對一組算法,將每一種算法都封裝到具有共同接口的獨立的類中,從而是它們可以相互替換。策略模式的最大特點是使得算法可以在不影響客戶端的情況下發生變化,從而改變不同的功能。
假如我們有一個根據不同用戶類型返回不同折扣的方法,我們的實現可能是這樣:
import org.springframework.stereotype.Service; @Service public class CashService { public double cash(String type, double money) { if ("svip".equals(type)) { return money * 0.75; } else if ("vip".equals(type)) { return money * 0.9; } else { return money; } } }
現在我們各個類型的用戶折扣耦合在一起,修改一個用戶的折扣力度有可能會對其他類型用戶造成影響。根據策略模式的思想,我們需要把折扣力度封裝成具體的方法並面向接口編程。我們首先定義公共的接口DiscountService,編寫其實現類,則我們改造後的代碼可能如下所示:
import com.study.designer.strategy.NormalDiscountStrategy; import com.study.designer.strategy.SvipDiscountStrategy; import com.study.designer.strategy.VipDiscountStrategy; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class CashService { @Autowired private SvipDiscountStrategy svipDiscountStrategy; @Autowired private VipDiscountStrategy vipDiscountStrategy; @Autowired private NormalDiscountStrategy normalDiscountStrategy; public double cash(String type, double money) { if ("svip".equals(type)) { return svipDiscountStrategy.getMoney(money); } else if ("vip".equals(type)) { return vipDiscountStrategy.getMoney(money); } else { return normalDiscountStrategy.getMoney(money); } } }
可以看到,改造後的CashService中還存在許多if判斷,我們需要消除這些if判斷。我們可以在CashService初始化時就獲取到所有的折扣策略,然後根據具體類型計算具體折扣。獲取所有策略可以交由Spring來完成,改造後的代碼如下所示:
import com.study.designer.strategy.inf.DiscountStrategy;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;@Service
public class CashService {
private Map<String, DiscountStrategy> strategyMap = new HashMap<>();
public CashService(List<DiscountStrategy> strategies) {
for (DiscountStrategy strategy : strategies) {
strategyMap.put(strategy.getType(), strategy);
}
}
public double cash(String type, double money) {
return strategyMap.get(type).getMoney(money);
}
} - 狀態模式
參考鏈接:https://www.cnblogs.com/wkzhao/p/10229412.html本次LZ給各位介紹狀態模式,之前在寫設計模式的時候,引入了一些小故事,二十章職責連模式是故事版的最後一篇,之後還剩餘四個設計模式,LZ會依照原生的方式去解釋這幾個設計模式,特別是原型模式和解釋器模式,會包含一些其它的內容。
好了,接下來,我們先來看看狀態模式的定義吧。
定義:(源於Design Pattern):當一個對象的內在狀態改變時允許改變其行爲,這個對象看起來像是改變了其類。
上述是百度百科中對狀態模式的定義,定義很簡單,只有一句話,請各位形象的去理解這句話,它說當狀態改變時,這個對象的行爲也會變,而看起來就像是這個類改變了一樣。
這正是應驗了我們那句話,有些人一旦發生過什麼事以後,就像變了個人似的,這句話其實與狀態模式有異曲同工之妙。
我們仔細體會一下定義當中的要點。
1、有一個對象,它是有狀態的。
2、這個對象在狀態不同的時候,行爲不一樣。
3、這些狀態是可以切換的,而非毫無關係。
前兩點比較好理解,第3點有時候容易給人比較迷惑的感覺,什麼叫這些狀態是可以切換的,而非毫無關係?
舉個例子,比如一個人的狀態,可以有很多,像生病和健康,這是兩個狀態,這是有關係並且可以轉換的兩個狀態。再比如,睡覺、上班、休息,這也算是一組狀態,這三個狀態也是有關係的並且可以互相轉換。
那如果把生病和休息這兩個狀態放在一起,就顯得毫無意義了。所以這些狀態應該是一組相關並且可互相切換的狀態。
下面我們來看看狀態模式的類圖。
類圖中包含三個角色。
Context:它就是那個含有狀態的對象,它可以處理一些請求,這些請求最終產生的響應會與狀態相關。
State:狀態接口,它定義了每一個狀態的行爲集合,這些行爲會在Context中得以使用。
ConcreteState:具體狀態,實現相關行爲的具體狀態類。
如果針對剛纔對於人的狀態的例子來分析,那麼人(Person)就是Context,狀態接口依然是狀態接口,而具體的狀態類,則可以是睡覺,上班,休息,這一系列狀態。
不過LZ也看過不少狀態模式的文章和帖子,包括《大話設計模式》當中,都舉的是有關人的狀態的例子,所以這裏給大家換個口味,我們換一個例子。
我們來試着寫一個DOTA的例子,最近貌似跟DOTA幹上了,不爲其他,就因爲DOTA伴隨了LZ四年的大學時光。
玩過的朋友都知道,DOTA裏的英雄有很多狀態,比如正常,眩暈,加速,減速等等。相信就算沒有玩過DOTA的朋友們,在其它遊戲裏也能見到類似的情況。那麼假設我們的DOTA沒有使用狀態模式,則我們的英雄類會非常複雜和難以維護,我們來看下,原始版的英雄類是怎樣的。
package com.state; //英雄類 public class Hero { public static final int COMMON = 1;//正常狀態 public static final int SPEED_UP = 2;//加速狀態 public static final int SPEED_DOWN = 3;//減速狀態 public static final int SWIM = 4;//眩暈狀態 private int state = COMMON;//默認是正常狀態 private Thread runThread;//跑動線程 //設置狀態 public void setState(int state) { this.state = state; } //停止跑動 public void stopRun() { if (isRunning()) runThread.interrupt(); System.out.println("--------------停止跑動---------------"); } //開始跑動 public void startRun() { if (isRunning()) { return; } final Hero hero = this; runThread = new Thread(new Runnable() { public void run() { while (!runThread.isInterrupted()) { try { hero.run(); } catch (InterruptedException e) { break; } } } }); System.out.println("--------------開始跑動---------------"); runThread.start(); } private boolean isRunning(){ return runThread != null && !runThread.isInterrupted(); } //英雄類開始奔跑 private void run() throws InterruptedException{ if (state == SPEED_UP) { System.out.println("--------------加速跑動---------------"); Thread.sleep(4000);//假設加速持續4秒 state = COMMON; System.out.println("------加速狀態結束,變爲正常狀態------"); }else if (state == SPEED_DOWN) { System.out.println("--------------減速跑動---------------"); Thread.sleep(4000);//假設減速持續4秒 state = COMMON; System.out.println("------減速狀態結束,變爲正常狀態------"); }else if (state == SWIM) { System.out.println("--------------不能跑動---------------"); Thread.sleep(2000);//假設眩暈持續2秒 state = COMMON; System.out.println("------眩暈狀態結束,變爲正常狀態------"); }else { //正常跑動則不打印內容,否則會刷屏 } } }
下面我們寫一個客戶端類,去試圖讓英雄在各種狀態下奔跑一下。
package com.state; public class Main { public static void main(String[] args) throws InterruptedException { Hero hero = new Hero(); hero.startRun(); hero.setState(Hero.SPEED_UP); Thread.sleep(5000); hero.setState(Hero.SPEED_DOWN); Thread.sleep(5000); hero.setState(Hero.SWIM); Thread.sleep(5000); hero.stopRun(); } }
可以看到,我們的英雄在跑動過程中隨着狀態的改變,會以不同的狀態進行跑動。在上面原始的例子當中,我們的英雄類當中有明顯的if else結構,我們再來看看百度百科中狀態模式所解決的問題的描述。
狀態模式解決的問題:狀態模式主要解決的是當控制一個對象狀態的條件表達式過於複雜時的情況。把狀態的判斷邏輯轉移到表示不同狀態的一系列類中,可以把複雜的判斷邏輯簡化。
不用說,狀態模式是可以解決我們上面的if else結構的,我們採用狀態模式,利用多態的特性可以消除掉if else結構。這樣所帶來的好處就是可以大大的增加程序的可維護性與擴展性。
下面我們就使用狀態模式對上面的例子進行改善,首先第一步,就是我們需要定義一個狀態接口,這個接口就只有一個方法,就是run。
package com.state; public interface RunState { void run(Hero hero); }
與狀態模式類圖不同的是,我們加入了一個參數Hero(Context),這樣做的目的是爲了具體的狀態類當達到某一個條件的時候可以切換上下文的狀態。下面列出四個具體的狀態類,其實就是把if else拆掉放到這幾個類的run方法中。
package com.state; public class CommonState implements RunState{ public void run(Hero hero) { //正常跑動則不打印內容,否則會刷屏 } }
package com.state; public class SpeedUpState implements RunState{ public void run(Hero hero) { System.out.println("--------------加速跑動---------------"); try { Thread.sleep(4000);//假設加速持續4秒 } catch (InterruptedException e) {} hero.setState(Hero.COMMON); System.out.println("------加速狀態結束,變爲正常狀態------"); } }
package com.state; public class SpeedDownState implements RunState{ public void run(Hero hero) { System.out.println("--------------減速跑動---------------"); try { Thread.sleep(4000);//假設減速持續4秒 } catch (InterruptedException e) {} hero.setState(Hero.COMMON); System.out.println("------減速狀態結束,變爲正常狀態------"); } }
package com.state; public class SwimState implements RunState{ public void run(Hero hero) { System.out.println("--------------不能跑動---------------"); try { Thread.sleep(2000);//假設眩暈持續2秒 } catch (InterruptedException e) {} hero.setState(Hero.COMMON); System.out.println("------眩暈狀態結束,變爲正常狀態------"); } }
這下我們的英雄類也要相應的改動一下,最主要的改動就是那些if else可以刪掉了,如下。
package com.state; //英雄類 public class Hero { public static final RunState COMMON = new CommonState();//正常狀態 public static final RunState SPEED_UP = new SpeedUpState();//加速狀態 public static final RunState SPEED_DOWN = new SpeedDownState();//減速狀態 public static final RunState SWIM = new SwimState();//眩暈狀態 private RunState state = COMMON;//默認是正常狀態 private Thread runThread;//跑動線程 //設置狀態 public void setState(RunState state) { this.state = state; } //停止跑動 public void stopRun() { if (isRunning()) runThread.interrupt(); System.out.println("--------------停止跑動---------------"); } //開始跑動 public void startRun() { if (isRunning()) { return; } final Hero hero = this; runThread = new Thread(new Runnable() { public void run() { while (!runThread.isInterrupted()) { state.run(hero); } } }); System.out.println("--------------開始跑動---------------"); runThread.start(); } private boolean isRunning(){ return runThread != null && !runThread.isInterrupted(); } }
可以看到,現在我們的英雄類優雅了許多,我們使用剛纔同樣的客戶端運行即可得到同樣的結果。
對比我們的原始例子,現在我們使用狀態模式之後,有幾個明顯的優點:
一、我們去掉了if else結構,使得代碼的可維護性更強,不易出錯,這個優點挺明顯,如果試圖讓你更改跑動的方法,是剛纔的一堆if else好改,還是分成了若干個具體的狀態類好改呢?答案是顯而易見的。
二、使用多態代替了條件判斷,這樣我們代碼的擴展性更強,比如要增加一些狀態,假設有加速20%,加速10%,減速10%等等等(這並不是虛構,DOTA當中是真實存在這些狀態的),會非常的容易。
三、狀態是可以被共享的,這個在上面的例子當中有體現,看下Hero類當中的四個static final變量就知道了,因爲狀態類一般是沒有自己的內部狀態的,所有它只是一個具有行爲的對象,因此是可以被共享的。
四、狀態的轉換更加簡單安全,簡單體現在狀態的分割,因爲我們把一堆if else分割成了若干個代碼段分別放在幾個具體的狀態類當中,所以轉換起來當然更簡單,而且每次轉換的時候我們只需要關注一個固定的狀態到其他狀態的轉換。安全體現在類型安全,我們設置上下文的狀態時,必須是狀態接口的實現類,而不是原本的一個整數,這可以杜絕魔數以及不正確的狀態碼。
狀態模式適用於某一個對象的行爲取決於該對象的狀態,並且該對象的狀態會在運行時轉換,又或者有很多的if else判斷,而這些判斷只是因爲狀態不同而不斷的切換行爲。
上面的適用場景是很多狀態模式的介紹中都提到的,下面我們就來看下剛纔DOTA中,英雄例子的類圖。
可以看到,這個類圖與狀態模式的標準類圖是幾乎一模一樣的,只是多了一條狀態接口到上下文的依賴線,而這個是根據實際需要添加的,而且一般情況下都是需要的。
狀態模式也有它的缺點,不過它的缺點和大多數模式相似,有兩點。
1、會增加的類的數量。
2、使系統的複雜性增加。
儘管狀態模式有着這樣的缺點,但是往往我們犧牲複雜性去換取的高可維護性和擴展性是相當值得的,除非增加了複雜性以後,對於後者的提升會乎其微。狀態模式在項目當中也算是較經常會碰到的一個設計模式,但是通常情況下,我們還是在看到if else的情況下,對項目進行重構時使用,又或者你十分確定要做的項目會朝着狀態模式發展,一般情況下,還是不建議在項目的初期使用。