全面分析如何減少if-else條件判斷語句

一、概述

timg.jpg

如上圖,你選擇Life,還是選擇Work?這是個問題,這也是判斷語句,落實到代碼就是if-else語句。一句if-else看起來是那么的簡單和美好。

然而,實際項目開發(fā)過程中,由于業(yè)務(wù)邏輯復(fù)雜、條件判斷多、需求更新迭代、容錯處理等等,常常會使用if-else來判斷;時間久了,if-else 越疊越多,讓后續(xù)維護(hù)的人眼花繚亂。

if-else本身并不是錯,也不會有導(dǎo)致bug/crash的問題。但是從 軟件架構(gòu)思維 出發(fā),這樣的代碼不利于擴(kuò)展、不易維護(hù)、容易出錯、后續(xù)開發(fā)效率降低等問題。

所以,請保持程序猿的 代碼潔癖 ,優(yōu)化它。

注意:當(dāng)然switch-case也存在這樣的情況,本質(zhì)上switch-caseif-else同屬于條件判斷語句。下面以if-else為例來說明。

我們可以將 if-else 分為兩個方向來探索,分別為「廣度」和「深度」?!笍V度」指的是有很多else if分支,「深度」指的是if-else嵌套if-else的行為。

二、「廣度if-else」的方案探索

2.1、通用場景1:“空間” 換 “整潔”

算法領(lǐng)域有個經(jīng)典論斷:空間換時間!說的時候,使用更多內(nèi)存空間 提高代碼執(zhí)行速度 使用更少的時間。 同樣的,在優(yōu)化if-else 語句方面,也有類似的解決方案。

使用 map/array 等數(shù)據(jù)結(jié)構(gòu),保存條件分支「入口」地址,這地址往往是函數(shù)指針或者方法名。然后呢?在代碼運行的時候動態(tài)注冊,執(zhí)行的時候找到對應(yīng)的「入口」執(zhí)行調(diào)用。 這種方法也被稱為「表驅(qū)動」。

  • 下面將用一個本人實際開發(fā)中使用的場景來說明下原理。
    例子:移動開發(fā)中的「統(tǒng)跳」邏輯,即根據(jù)某個條件判斷,跳轉(zhuǎn)到對應(yīng)ViewController。
// if-else 做法
if (condition1) {
    // jump to ViewController1
} else if (condition2) {
    // jump to ViewController2
} else if (condition3) {
    // jump to ViewController3
} else if (condition4) {
    // jump to ViewController4
} else {
    // jump to ViewController#
}

當(dāng)業(yè)務(wù)不斷迭代N個版本之后,這個if-else 會變得很多很多,達(dá)到幾十上百分支判斷。

  • 如何使用「“空間” 換 “整潔”」來優(yōu)化呢?
// 數(shù)據(jù)model
typedef void(^JumpActionBlock)(id info);

// 管理類
@interface GlobalJumpCenter : NSObject
@property (nonatomic, strong) NSMutableDictionary *routerMapper;
@end
@implementation GlobalJumpCenter
- (void)registerJumpWithKey:(NSString *)key actionBlock:(ZTGJumpActionBlock)actionBlock {
    if (key && key.length && actionBlock) {
          [_routerMapper setObject:actionBlock forKey:key];
    }
}

- (void)runJumpActionWithKey:(NSString *)key data:(id)data {
    if (key && key.length) {
         ZTGJumpActionBlock blk = [_routerMapper objectForKey:key];
         blk(data);
    }
}
@end

/* 1、注冊 */
ZTGGlobalJumpCenter *center = [ZTGGlobalJumpCenter defaultService];
[center registerJumpWithKey:@"condition1" actionBlock:^(id info) {
    // jump to ViewController1
}];
[center registerJumpWithKey:@"condition2" actionBlock:^(id info) {
    // jump to ViewController2
}];
[center registerJumpWithKey:@"condition3" actionBlock:^(id info) {
    // jump to ViewController3
}];
...

/* 2、調(diào)用 */
[center runJumpActionWithKey:@"condition2" data:data];
  • 「統(tǒng)跳」的場景優(yōu)化的好處有以下:
    1、統(tǒng)一的入口注冊,方便擴(kuò)展和維護(hù);
    2、調(diào)用邏輯簡單,一句話搞定,上層使用簡單;
    3、key的命名,如果做到“知名達(dá)意”,那么就可以很簡單知道這個分支是干嘛,好維護(hù)。

2.2、通用場景2:策略模式

在策略模式定義中,一個類的行為或其算法可以在運行時,根據(jù)不同的環(huán)境使用不同的策略。
這本質(zhì)就是if-else,因此可以使用策略模式,利用面向?qū)ο蟮摹岸鄳B(tài)“ 特性,減少if-else。

關(guān)于策略模式,詳見之前一篇文章。 行為型設(shè)計模式.策略模式

  • 實例代碼如下,
/* 策略接口 */ 
@protocol SZStrategyInterface <NSObject>
- (void)strategyMethod:(id)info;
@end
@interface SZStrategy : NSObject <SZStrategyInterface>
@end

/* 策略實現(xiàn)類 */
@interface SZStrategyImplOne : SZStrategy
@end
@interface SZStrategyImplTwo : SZStrategy
@end

@implementation SZStrategyImplOne
- (void)strategyMethod:(id)info {
    NSLog(@"SZStrategyImplOne:call method~");
}
@end
@implementation SZStrategyImplTwo
- (void)strategyMethod:(id)info {
    NSLog(@"SZStrategyImplTwo method~");
}
@end

/* 上下文 */
@interface SZStrategyContext ()
@property (nonatomic, strong) NSMutableDictionary *strategyMap;
@end

@implementation SZStrategyContext
- (void)registerStrategyWithKey:(NSString *)key impl:(id<SZStrategyInterface>)impl {
    if (key && key.length && impl && [impl conformsToProtocol:@protocol(SZStrategyInterface)]) {
        [self.strategyMap setValue:impl forKey:key];
    }
}

- (id<SZStrategyInterface>)selectStrategyWithKey:(NSString *)key {
    return (!key || !key.length) ? nil : [self.strategyMap objectForKey:key];
}
@end

/* 使用場景 */
SZStrategyContext *context = [[SZStrategyContext alloc] init];
[context registerStrategyWithKey:@"conditon_1" impl:[[SZStrategyImplOne alloc] init]];
[context registerStrategyWithKey:@"conditon_2" impl:[[SZStrategyImplTwo alloc] init]];
// 獲取并且執(zhí)行
id impl = [context selectStrategyWithKey:@"conditon_1"];

我們發(fā)現(xiàn)策略模式代碼和上面的 通用場景1:“空間” 換 “整潔” 非常的相近。策略模式,將action等封裝到類中,利用多態(tài)特性實現(xiàn)。

2.3、通用場景3:使用三元表達(dá)式

三元表示只能降低一兩層的if-else判斷,更多用于賦值表達(dá)式中。

name = condition ? nickName : trueName;

當(dāng)然也可以用于語句執(zhí)行判斷,如下

result = condition  ? case_func() : case_func();
result = condition  ?  : case_func();

2.4、特殊場景之「判空和數(shù)據(jù)合法性」判斷

「判空和數(shù)據(jù)合法性」判斷,在實際開發(fā)場景中,可以說是家常便飯,很煩但又是不得不做的事情,它也會增加if-else,特別是數(shù)據(jù)結(jié)構(gòu)負(fù)責(zé)、參數(shù)多的情況下。

  • 解決方案:利用「分層思想」,從「代碼層次結(jié)構(gòu)」上解耦
    針對參數(shù)或者數(shù)據(jù)的「判空和數(shù)據(jù)合法性」問題,可以使用「分層思想」。

    將使用「判空和數(shù)據(jù)合法性」的判斷上,劃分到單獨一層或者統(tǒng)一函數(shù)處理,在「代碼層次結(jié)構(gòu)」上劃分開來;從而減少「判空和數(shù)據(jù)合法性」導(dǎo)致的if-else多的問題。

    這有點類似于 面向切面編程 AOP的思想。

2.5、特殊場景之「多狀態(tài)」遷移問題

實際業(yè)務(wù)場景中,常常遇到因為管理某個事物的「狀態(tài)」,不同的「狀態(tài)」走不同的流程,這不可避免會有很多if-else或者switch-case來判斷。
這樣的場景讓后續(xù)維護(hù)的時候,不容易理解原有狀態(tài)遷移過程,往往會把自己繞暈了,特別是沒有前人之路和設(shè)計文檔加持的情況下。

  • 解決方案:「狀態(tài)機(jī)設(shè)計模式」解耦,具體百度查詢。

2.6、其他

還有很多業(yè)務(wù)場景,我們或多或少的可以使用設(shè)計模式中的責(zé)任鏈模式命令模式等來適當(dāng)?shù)臏p少if-else,不過設(shè)計模式本身不僅僅是用來減少if-else判斷語句,更多是體現(xiàn)一種架構(gòu)思維想,是在面向?qū)ο缶幋a中,對象與對象在結(jié)構(gòu)、行為上一種設(shè)計,達(dá)到更好解耦,更好迭代業(yè)務(wù),更好維護(hù)代碼等目的。

三、「嵌套的if-else」的方案探索

上面討論的問題很多是if-else在「廣度上的多雜問題」,那么嵌套的if-else,就屬于if-else的深「深度上的深多雜問題」。

對于嵌套的if-else場景,上面的方案確不見得好。map方案,需要構(gòu)建「樹」的數(shù)據(jù)結(jié)構(gòu),面臨著尋址等問題,增加復(fù)雜度。策略模式,類的數(shù)量成倍上漲,這也不是我們所希望的。

3.1、「衛(wèi)語句」解決

什么是「衛(wèi)語句」呢?借用張圖,來說明什么是「衛(wèi)語句」。

ifelse-depth.png

「嵌套的if-else」在代碼層次來看,其最大的問題在于深度過于深。那么解決這個問題,最直接的方式就是減少深度。那么解決的方式呢?就是「衛(wèi)語句」,這就是「衛(wèi)語句」的最大作用所在。上圖很好說明這個問題。

「衛(wèi)語句」的實質(zhì)就是它將深層的if-else扁平化,強(qiáng)制使用return結(jié)束判斷流程。

  • 舉個例子說說
function getPayAmount() {
  let result;
  if (isDead)
    result = deadAmount();
  else {
    if (isSeparated)
      result = separatedAmount();
    else {
      if (isRetired)
        result = retiredAmount();
      else
        result = normalPayAmount();
    }
  }
  return result;
}

使用「衛(wèi)語句」解決。

function getPayAmount() {
  if (isDead) return deadAmount();
  if (isSeparated) return separatedAmount();
  if (isRetired) return retiredAmount();
  return normalPayAmount();
}

關(guān)于「衛(wèi)語句」參考Replace Nested Conditional with Guard Clauses 代碼和圖均來自于此,在于說明思想,顧直接Copy不做額外編碼。

  • 回頭看swiftguard語句
func guardTest(x: Int?) {
    guard let x = x where x > 0 else {
        // 變量不符合條件判斷時,執(zhí)行下面代碼
        return
    }
    
    ...
}

從某種意義來說,guard也是「衛(wèi)語句」,就像門衛(wèi)一樣,守護(hù)在函數(shù)入口前,摒棄一切非法。

  • assert語句
    assert不符合直接crash,屬于極端報錯方法,這只能算作是輔助,讓你debug的時候發(fā)現(xiàn)更多的問題。其只會在debug會,在release不會crash。

3.2、「分層思想」

方案就是利用「分層思想」從「代碼層次結(jié)構(gòu)」把不同if-else分為不同的層去做判斷。
詳見上面的 「2.4、特殊場景之「判空和數(shù)據(jù)合法性」判斷」 章節(jié)。

四、總結(jié)

針對單層if-else的優(yōu)化,采用上面第二章的各種方案,可以有效降低多if-else帶來的問題。

但是呢?辯證哲學(xué)思想,告訴我們,事物都是有利必有弊。這些方案往往是犧牲空間,或者增加類數(shù)量,沒有十全十美。

那么怎么用呢?一切以業(yè)務(wù)場景觸發(fā),選取最優(yōu)、最能解決你痛點的方案。

針對嵌套的if-else場景,上面的方案其實是從「代碼編寫約束」上去尋求解決出路。

有更加合適的方案,希望大牛在評論區(qū)告知,萬分感謝!

其他:

消除if-else的十種方法
《重構(gòu)與模式》
《重構(gòu):改善既有代碼的設(shè)計》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

友情鏈接更多精彩內(nèi)容