全面分析如何减少if-else条件判断语句

一、概述

timg.jpg

如上图,你选择Life,还是选择Work?这是个问题,这也是判断语句,落实到代码就是if-else语句。一句if-else看起来是那么的简单和美好。

然而,实际项目开发过程中,由于业务逻辑复杂、条件判断多、需求更新迭代、容错处理等等,常常会使用if-else来判断;时间久了,if-else 越叠越多,让后续维护的人眼花缭乱。

if-else本身并不是错,也不会有导致bug/crash的问题。但是从 软件架构思维 出发,这样的代码不利于扩展、不易维护、容易出错、后续开发效率降低等问题。

所以,请保持程序猿的 代码洁癖 ,优化它。

注意:当然switch-case也存在这样的情况,本质上switch-caseif-else同属于条件判断语句。下面以if-else为例来说明。

我们可以将 if-else 分为两个方向来探索,分别为「广度」和「深度」。「广度」指的是有很多else if分支,「深度」指的是if-else嵌套if-else的行为。

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

2.1、通用场景1:“空间” 换 “整洁”

算法领域有个经典论断:空间换时间!说的时候,使用更多内存空间 提高代码执行速度 使用更少的时间。 同样的,在优化if-else 语句方面,也有类似的解决方案。

使用 map/array 等数据结构,保存条件分支「入口」地址,这地址往往是函数指针或者方法名。然后呢?在代码运行的时候动态注册,执行的时候找到对应的「入口」执行调用。 这种方法也被称为「表驱动」。

  • 下面将用一个本人实际开发中使用的场景来说明下原理。
    例子:移动开发中的「统跳」逻辑,即根据某个条件判断,跳转到对应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#
}

当业务不断迭代N个版本之后,这个if-else 会变得很多很多,达到几十上百分支判断。

  • 如何使用「“空间” 换 “整洁”」来优化呢?
// 数据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、调用 */
[center runJumpActionWithKey:@"condition2" data:data];
  • 「统跳」的场景优化的好处有以下:
    1、统一的入口注册,方便扩展和维护;
    2、调用逻辑简单,一句话搞定,上层使用简单;
    3、key的命名,如果做到“知名达意”,那么就可以很简单知道这个分支是干嘛,好维护。

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

在策略模式定义中,一个类的行为或其算法可以在运行时,根据不同的环境使用不同的策略。
这本质就是if-else,因此可以使用策略模式,利用面向对象的“多态“ 特性,减少if-else

关于策略模式,详见之前一篇文章。 行为型设计模式.策略模式

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

/* 策略实现类 */
@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]];
// 获取并且执行
id impl = [context selectStrategyWithKey:@"conditon_1"];

我们发现策略模式代码和上面的 通用场景1:“空间” 换 “整洁” 非常的相近。策略模式,将action等封装到类中,利用多态特性实现。

2.3、通用场景3:使用三元表达式

三元表示只能降低一两层的if-else判断,更多用于赋值表达式中。

name = condition ? nickName : trueName;

当然也可以用于语句执行判断,如下

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

2.4、特殊场景之「判空和数据合法性」判断

「判空和数据合法性」判断,在实际开发场景中,可以说是家常便饭,很烦但又是不得不做的事情,它也会增加if-else,特别是数据结构负责、参数多的情况下。

  • 解决方案:利用「分层思想」,从「代码层次结构」上解耦
    针对参数或者数据的「判空和数据合法性」问题,可以使用「分层思想」。

    将使用「判空和数据合法性」的判断上,划分到单独一层或者统一函数处理,在「代码层次结构」上划分开来;从而减少「判空和数据合法性」导致的if-else多的问题。

    这有点类似于 面向切面编程 AOP的思想。

2.5、特殊场景之「多状态」迁移问题

实际业务场景中,常常遇到因为管理某个事物的「状态」,不同的「状态」走不同的流程,这不可避免会有很多if-else或者switch-case来判断。
这样的场景让后续维护的时候,不容易理解原有状态迁移过程,往往会把自己绕晕了,特别是没有前人之路和设计文档加持的情况下。

  • 解决方案:「状态机设计模式」解耦,具体百度查询。

2.6、其他

还有很多业务场景,我们或多或少的可以使用设计模式中的责任链模式命令模式等来适当的减少if-else,不过设计模式本身不仅仅是用来减少if-else判断语句,更多是体现一种架构思维想,是在面向对象编码中,对象与对象在结构、行为上一种设计,达到更好解耦,更好迭代业务,更好维护代码等目的。

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

上面讨论的问题很多是if-else在「广度上的多杂问题」,那么嵌套的if-else,就属于if-else的深「深度上的深多杂问题」。

对于嵌套的if-else场景,上面的方案确不见得好。map方案,需要构建「树」的数据结构,面临着寻址等问题,增加复杂度。策略模式,类的数量成倍上涨,这也不是我们所希望的。

3.1、「卫语句」解决

什么是「卫语句」呢?借用张图,来说明什么是「卫语句」。

ifelse-depth.png

「嵌套的if-else」在代码层次来看,其最大的问题在于深度过于深。那么解决这个问题,最直接的方式就是减少深度。那么解决的方式呢?就是「卫语句」,这就是「卫语句」的最大作用所在。上图很好说明这个问题。

「卫语句」的实质就是它将深层的if-else扁平化,强制使用return结束判断流程。

  • 举个例子说说
function getPayAmount() {
  let result;
  if (isDead)
    result = deadAmount();
  else {
    if (isSeparated)
      result = separatedAmount();
    else {
      if (isRetired)
        result = retiredAmount();
      else
        result = normalPayAmount();
    }
  }
  return result;
}

使用「卫语句」解决。

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

关于「卫语句」参考Replace Nested Conditional with Guard Clauses 代码和图均来自于此,在于说明思想,顾直接Copy不做额外编码。

  • 回头看swiftguard语句
func guardTest(x: Int?) {
    guard let x = x where x > 0 else {
        // 变量不符合条件判断时,执行下面代码
        return
    }
    
    ...
}

从某种意义来说,guard也是「卫语句」,就像门卫一样,守护在函数入口前,摒弃一切非法。

  • assert语句
    assert不符合直接crash,属于极端报错方法,这只能算作是辅助,让你debug的时候发现更多的问题。其只会在debug会,在release不会crash。

3.2、「分层思想」

方案就是利用「分层思想」从「代码层次结构」把不同if-else分为不同的层去做判断。
详见上面的 「2.4、特殊场景之「判空和数据合法性」判断」 章节。

四、总结

针对单层if-else的优化,采用上面第二章的各种方案,可以有效降低多if-else带来的问题。

但是呢?辩证哲学思想,告诉我们,事物都是有利必有弊。这些方案往往是牺牲空间,或者增加类数量,没有十全十美。

那么怎么用呢?一切以业务场景触发,选取最优、最能解决你痛点的方案。

针对嵌套的if-else场景,上面的方案其实是从「代码编写约束」上去寻求解决出路。

有更加合适的方案,希望大牛在评论区告知,万分感谢!

其他:

消除if-else的十种方法
《重构与模式》
《重构:改善既有代码的设计》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,012评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,628评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,653评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,485评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,574评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,590评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,596评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,340评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,794评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,102评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,276评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,940评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,583评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,201评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,441评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,173评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,136评论 2 352