美文网首页
从「广度」和「深度」分析减少if-else等条件判断语句

从「广度」和「深度」分析减少if-else等条件判断语句

作者: 双鱼子曰1987 | 来源:发表于2020-11-13 11:21 被阅读0次

    一、概述

    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」的方案探索

    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的十种方法
    《重构与模式》
    《重构:改善既有代码的设计》

    相关文章

      网友评论

          本文标题:从「广度」和「深度」分析减少if-else等条件判断语句

          本文链接:https://www.haomeiwen.com/subject/iyxvbktx.html