一、概述
timg.jpg如上图,你选择Life,还是选择Work?这是个问题,这也是判断语句,落实到代码就是
if-else
语句。一句if-else
看起来是那么的简单和美好。
然而,实际项目开发过程中,由于业务逻辑复杂、条件判断多、需求更新迭代、容错处理等等,常常会使用if-else
来判断;时间久了,if-else
越叠越多,让后续维护的人眼花缭乱。
多if-else
本身并不是错,也不会有导致bug/crash
的问题。但是从 软件架构思维
出发,这样的代码不利于扩展、不易维护、容易出错、后续开发效率降低等问题。
所以,请保持程序猿的 代码洁癖
,优化它。
注意:当然switch-case
也存在这样的情况,本质上switch-case
和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、「卫语句」解决
什么是「卫语句」呢?借用张图,来说明什么是「卫语句」。
「嵌套的
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不做额外编码。
- 回头看
swift
的guard
语句
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的十种方法
《重构与模式》
《重构:改善既有代码的设计》
网友评论