真香!我终于干掉了该死的 if-else...
点击“开发者技术前线”,选择“星标?”
在看|星标|留言, 真爱
if else 是所有高级编程语言都有的必备功能。但现实中的代码往往存在着过多的 if else。
作者:艾瑞克·邵 图片来自 Pexels
编辑:陶家龙
出处:https://www.cnblogs.com/eric-shao/p/10115577.html
虽然 if else 是必须的,但滥用 if else 会对代码的可读性、可维护性造成很大伤害,进而危害到整个软件系统。
现在软件开发领域出现了很多新技术、新概念,但 if...else 这种基本的程序形式并没有发生太大变化。
使用好 if else 不仅对于现在,而且对于将来,都是十分有意义的。今天我们就来看看如何“干掉”代码中的 if else,还代码以清爽。
问题一:if else 过多
问题表现
if else 过多的代码可以抽象为下面这段代码。其中只列出 5 个逻辑分支,但实际工作中,能见到一个方法包含 10 个、20 个甚至更多的逻辑分支的情况。
另外,if else 过多通常会伴随着另两个问题:逻辑表达式复杂和 if else 嵌套过深。
if (condition1) {
} else if (condition2) {
} else if (condition3) {
} else if (condition4) {
} else {
}
通常,if else 过多的方法,通常可读性和可扩展性都不好。
从软件设计角度讲,代码中存在过多的 if else 往往意味着这段代码违反了违反单一职责原则和开闭原则。
因为在实际的项目中,需求往往是不断变化的,新需求也层出不穷。所以,软件系统的扩展性是非常重要的。
而解决 if else 过多问题的最大意义,往往就在于提高代码的可扩展性。
如何解决
接下来我们来看如何解决 ifelse 过多的问题,下面我列出了一些解决方法:
表驱动
职责链模式
注解驱动
事件驱动
有限状态机
Optional
Assert
多态
方法一:表驱动
适用场景:逻辑表达模式固定的 if else。
实现与示例:
if (param.equals(value1)) {
doAction1(someParams);
} else if (param.equals(value2)) {
doAction2(someParams);
} else if (param.equals(value3)) {
doAction3(someParams);
}
// ...
可重构为:
Map, Function> action> actionMappings = new HashMap<>(); // 这里泛型 ? 是为方便演示,实际可替换为你需要的类型
// When init
actionMappings.put(value1, (someParams) -> { doAction1(someParams)});
actionMappings.put(value2, (someParams) -> { doAction2(someParams)});
actionMappings.put(value3, (someParams) -> { doAction3(someParams)});
// 省略 null 判断
actionMappings.get(param).apply(someParams);
下面借用《编程珠玑》中的一个税金计算的例子:
if income <= 2200
tax = 0
else if income <= 2700
tax = 0.14 * (income - 2200)
else if income <= 3200
tax = 70 + 0.15 * (income - 2700)
else if income <= 3700
tax = 145 + 0.16 * (income - 3200)
......
else
tax = 53090 + 0.7 * (income - 102200)
方法二:职责链模式
适用场景:条件表达式灵活多变,没有统一的形式。
实现与示例:职责链的模式在开源框架的 Filter、Interceptor 功能的实现中可以见到很多。下面看一下通用的使用模式。
重构前:
public void handle(request) {
if (handlerA.canHandle(request)) {
handlerA.handleRequest(request);
} else if (handlerB.canHandle(request)) {
handlerB.handleRequest(request);
} else if (handlerC.canHandle(request)) {
handlerC.handleRequest(request);
}
}
重构后:
public void handle(request) {
handlerA.handleRequest(request);
}
public abstract class Handler {
protected Handler next;
public abstract void handleRequest(Request request);
public void setNext(Handler next) { this.next = next; }
}
public class HandlerA extends Handler {
public void handleRequest(Request request) {
if (canHandle(request)) doHandle(request);
else if (next != null) next.handleRequest(request);
}
}
注:职责链的控制模式,职责链模式在具体实现过程中,会有一些不同的形式。从链的调用控制角度看,可分为外部控制和内部控制两种。
方法三:注解驱动
适用场景:适合条件分支很多多,对程序扩展性和易用性均有较高要求的场景。通常是某个系统中经常遇到新需求的核心功能。
实现与示例:很多框架中都能看到这种模式的使用,比如常见的 Spring MVC。
因为这些框架很常用,Demo 随处可见,所以这里不再上具体的演示代码了。
方法四:事件驱动
适用场景:从理论角度讲,事件驱动可以看做是表驱动的一种,但从实践角度讲,事件驱动和前面提到的表驱动有多处不同。
具体来说:
表驱动通常是一对一的关系;事件驱动通常是一对多。
表驱动中,触发和执行通常是强依赖;事件驱动中,触发和执行是弱依赖。
实现与示例:实现方式上,单机的实践驱动可以使用 Guava、Spring 等框架实现。分布式的则一般通过各种消息队列方式实现。
但是因为这里主要讨论的是消除 if else,所以主要是面向单机问题域。因为涉及具体技术,所以此模式代码不做演示。
方法五:有限状态机
有限状态机通常被称为状态机(无限状态机这个概念可以忽略)。先引用维基百科上的定义:
有限状态机(英语:finite-state machine,缩写:FSM),简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。
适用场景:虽然现在互联网后端服务都在强调无状态,但这并不意味着不能使用状态机这种设计。
其实,在很多场景中,如协议栈、订单处理等功能中,状态机有这其天然的优势。因为这些场景中天然存在着状态和状态的流转。
实现与示例:实现状态机设计首先需要有相应的框架,这个框架需要实现至少一种状态机定义功能,以及对于的调用路由功能。
状态机定义可以使用 DSL 或者注解的方式。原理不复杂,掌握了注解、反射等功能的同学应该可以很容易实现。
①Apache Mina State Machine
https://mina.apache.org/mina-project/userguide/ch14-state-machine/ch14-state-machine.html
②Spring State Machine
Spring 子项目众多,其中有个不显山不露水的状态机框架,可以通过 DSL 和注解两种方式定义。
https://projects.spring.io/spring-statemachine/
方法六:Optional
使用场景:有较多用于非空判断的 if else。
实现与示例如下,传统写法:
String str = "Hello World!";
if (str != null) {
System.out.println(str);
} else {
System.out.println("Null");
}
使用 Optional 之后:
Optional strOptional = Optional.of("Hello World!");
strOptional.ifPresentOrElse(System.out::println, () -> System.out.println("Null"));
扩展:Kotlin Null Safety
Kotlin 带有一个被称为 Null Safety 的特性:
bob?.department?.head?.name
方法七:Assert 模式
Apache Commons Lang 中的 Validate 类:
https://commons.apache.org/proper/commons-lang/javadocs/api-3.1/org/apache/commons/lang3/Validate.html
Spring 的 Assert 类:
https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/util/Assert.html
使用场景:通常用于各种参数校验。
扩展:Bean Validation
方法八:多态
https://refactoring.com/catalog/replaceConditionalWithPolymorphism.html
使用场景:链接中给出的示例比较简单,无法体现适合使用多态消除 if else 的具体场景。
一般来说,当一个类中的多个方法都有类似于示例中的 if else 判断,且条件相同,那就可以考虑使用多态的方式消除 if else。
小结:上面这节介绍了 if else 过多所带来的问题,以及相应的解决方法。除了本节介绍的方法,还有一些其他的方法。
比如,在《重构与模式》一书中就介绍了“用 Strategy 替换条件逻辑”、“用 State 替换状态改变条件语句”和“用 Command 替换条件调度程序”这三个方法。
其中的“Command 模式”,其思想同本文的“表驱动”方法大体一致。另两种方法,因为在《重构与模式》一书中已做详细讲解,这里就不再重复。
问题二:if else 嵌套过深
问题表现
if else 多通常并不是最严重的的问题。有的代码 if else 不仅个数多,而且 if else 之间嵌套的很深,也很复杂,导致代码可读性很差,自然也就难以维护。
if (condition1) {
action1();
if (condition2) {
action2();
if (condition3) {
action3();
if (condition4) {
action4();
}
}
}
}
如何解决
抽取方法
卫语句
方法一:抽取方法
适用场景:if else 嵌套严重的代码,通常可读性很差。故在进行大型重构前,需先进行小幅调整,提高其代码可读性。抽取方法便是最常用的一种调整手段。
实现与示例如下,重构前:
public void add(Object element) {
if (!readOnly) {
int newSize = size + 1;
if (newSize > elements.length) {
Object[] newElements = new Object[elements.length + 10];
for (int i = 0; i < size; i++) {
newElements[i] = elements[i];
}
elements = newElements
}
elements[size++] = element;
}
}
重构后:
public void add(Object element) {
if (readOnly) {
return;
}
if (overCapacity()) {
grow();
}
addElement(element);
}
方法二:卫语句
直接看代码:
double getPayAmount() {
double result;
if (_isDead) result = deadAmount();
else {
if (_isSeparated) result = separatedAmount();
else {
if (_isRetired) result = retiredAmount();
else result = normalPayAmount();
};
}
return result;
}
重构之后:
double getPayAmount() {
if (_isDead) return deadAmount();
if (_isSeparated) return separatedAmount();
if (_isRetired) return retiredAmount();
return normalPayAmount();
}
使用场景:当看到一个方法中,某一层代码块都被一个 if else 完整控制时,通常可以采用卫语句。
问题三:if else 表达式过于复杂
问题表现
当 condition 1、2、3、4 分别为 true、false,请大家排列组合一下下面表达式的结果:
if ((condition1 && condition2 ) || ((condition2 || condition3) && condition4)) {
}
如何解决
总结