一、为什么会出现大量的if-else代码
当你使用if-else的时候,无非是以下的两种情况:
1.1 异常情况判断的需要
在很多情况下,我们在对一个对象进行操作之前,需要对其进行非空的判断,不然很容易出现NPE异常,代码示例如下:
Fruit fruit = new Fruit();
if(fruit != null){
// 正常的处理逻辑
} else {
// 异常的处理逻辑
}
1.2 不同处理逻辑的需要
还有一些情况,我们需要根据对象中某个属性的值不同,来决定走哪条具体的业务逻辑,代码示例如下:
Fruit fruit = new Fruit();
if(fruit == null){
return;
}
if(fruit.getType == 1){
// 处理逻辑1
}else if(fruit.getType == 2) {
// 处理逻辑2
}else{
// 处理逻辑3
}
大量的if-else代码容易让逻辑变得异常复杂,维护性和可靠性极差。
二、几种推荐的优化方法
幸运的是,前辈们已经总结出了很多有效改进if-else代码的方法,一起来看看哪一款最适合你。
2.1 合并条件表达式
如果你发现一段代码中,有多个不同的if-else里面处理的逻辑是相同的,那么就意味着这些分支其实是可以合并的,如下代码给出了示例:
if(age < 18){
return false;
}
...
if(height < 165){
return false;
}
...
if(weight > 100){
return false;
}
...
return true;
在如上的代码示例中,我们看到前三个if里面的逻辑是相同的,那么我们就可以将这三个条件判断进行合并:
if(age < 18 || height < 165 || weight > 100){
return false;
}
...
return true;
2.2 异常条件先退出
比如下面的代码示例,我们想要找同事讨论一个问题,那么需要首先判断是不是工作日,是的话还要判断他是不是请假了没来,如果来的话,还要判断他是不是正在忙,如果不忙的话,才能找他讨论。代码示例如下:
if(isWorkDay()){
if(isAbsent()){
return false;
}else{
if(isBusy()){
return false;
}else{
return true;
}
}
}else{
return false;
}
这样一段代码,是不是很难一下子弄清楚整个逻辑究竟是想干啥。现在,我们尝试把所有的异常条件都放在最前面,优先退出,然后优化后的结果如下:
if(!isWorkDay()){
return false;
}
if(isAbsent()){
return false;
}
if(isBusy()){
return false;
}
return true;
这时候,我们发现,这段代码还可以使用合并条件表达式再次进行优化:
if(!isWorkDay() || isAbsent()() || isBusy()){
return false;
}
return true;
如此,代码逻辑瞬间感觉清晰多了。
2.3 正常流程外移
如果主流程放在if-else里面,甚至放在好几层的if-else里面,估计也很难看清楚什么条件下才会执行它吧,我们先来看一个例子:
double cost = 0.0;
if(price > 100){
if(weight > 50){
cost = getAccount() - price * weight;
}
}
return cost;
在这段代码中,计算cost的逻辑是主流程,我们应该尽量保持其在if代码块之外,优化后的结果如下:
if(price <= 100 || weight <= 50){
return 0.0
}
return getAccount() - price * weight;
我们不但将主体代码外移了,还去除了临时变量,如此,就能很清楚地看到什么条件下返回什么结果。
2.4 逻辑封装成方法
当if-else中有大量的语句代码的时候,可以考虑将这些代码单独封装成一个方法,然后调用方法。
Fruit fruit = new Fruit();
double cost = 0.0;
if(fruit == null){
return cost;
}
if(fruit.getType == 1){
// 一大段代码1
}else if(fruit.getType == 2) {
// 一大段代码2
}else{
// 一大段代码3
}
优化后:
Fruit fruit = new Fruit();
double cost = 0.0;
if(fruit == null){
return cost;
}
if(fruit.getType == 1){
return getOneCost();
}else if(fruit.getType == 2) {
return getTwoCost();
}else{
return getThreeCost();
}
private double getOneCost(){
// 一大段代码1
}
private double getTwoCost(){
// 一大段代码2
}
private double getThreeCost(){
// 一大段代码3
}
2.5 使用卫语句
卫语句是为了消除else,将所有的条件分支以平行的方式进行展示。
if(isWorkDay()){
...
}
if(isAbsent()){
...
}
if(isBusy()){
...
}
...
三、重构的建议
上述优化的建议都是比较简单的,且对代码的改动比较少。如果你期待更高层次,更加优雅的代码优化,就需要重构成如下两种设计模式的形式。
3.1 策略模式
一个典型的名单创建场景原代码逻辑如下:
if(customer == null) {
return false;
}
if("TEL".equals(customer.getSource())){
// 执行电话来源的名单创建过程
} else if("SMS".equals(customer.getSource())){
// 执行短信来源的名单创建过程
} else if("WECHAT".equals(customer.getSource())){
// 执行微信来源的名单创建过程
} else {
// 执行默认来源的名单创建过程
}
return true;
我们可以看到,代码对不同的名单来源进行了不同的处理过程,但是随着名单来源的增加和每种名单来源处理逻辑的复杂化,代码的可读性和维护性就变得很差。
现在我们使用策略模式对其进行优化:
public abstract class CustomerCreateStrategy{
public abstract void createCustomer();
// 公共方法,不管那种来源的名单创建流程都需要调用
public void recordCustomer(){
// 执行记录名单创建的记录
}
}
public class TelCustomerCreateStrategy extends CustomerCreateStrategy {
@Override
public void createCustomer(){
// 执行电话来源的名单创建逻辑
}
}
其它三种名单创建代码省略,都是大同小异的,然后还要创建一个环境类。
public class customerCreatorContext{
// 需要保持对策略类的引用
private CustomerCreateStrategy customerCreateStrategy;
public customerCreatorContext(CustomerCreateStrategy customerCreateStrategy){
this.customerCreateStrategy = customerCreateStrategy;
}
public void beginCreateCustomer(){
this.customerCreateStrategy.createCustomer();
}
}
最后,我们在使用的时候代码如下:
CustomerCreatorContext context =
new CustomerCreatorContext(new TelCustomerCreateStrategy());
context.beginCreateCustomer();
要点是,用户必须知道有哪些策略,而具体使用哪一种策略,是由用户自己决定的。
3.2 状态模式
提醒有不同的状态,在不同状态下允许做不同的操作,下面是原来的处理逻辑:
if(remind.getDays() < 1) {
// 提醒生成时间小于1天的逻辑
} else if(remind.getDays() < 3) {
// 提醒生成时间小于3天的逻辑
} else {
// 提醒生成时间不小于3天的逻辑
}
这样的代码和上面策略模式演示的名单创建逻辑有同样的问题,下面我们使用状态模式来对其进行重构:
public abstract class RemindState{
public abstract void notifyHandler();
// 公共方法
public void recordRemind(){
// 执行记录逻辑
}
}
public class OneDayRemindState extends RemindState {
@Override
public void notifyHandler(){
// 一日内提醒通知处理人的逻辑
}
}
其余的状态类省略,下面我们定义一个环境类。
public class RemindNotifier{
// 需要保持对状态类的引用
private RemindState remindState;
public beginNotifyHandler(){
// 环境类自己维护状态的变更
Integer days = getPassedDays();
if(days < 1){
this.remindState = new OneDayRemindState();
}else if(days < 3){
this.remindState = new ThreeDayRemindState();
}else{
this.remindState = new ManyDayRemindState();
}
remindState.notifyHandler();
}
}
最后,我们使用的时候是这样的:
// 每天通过定时任务启动如下逻辑
RemindNotifier rm = new RemindNotifier();
rm.beginNotifyHandler();
3.3 两种模式的区别
我们可以看到,策略模式和状态模式非常地相近,都是一个抽象类(接口)和多个具体类,再加上一个环境类。但是在使用过程中还是有以下区别:
策略模式需要使用者知道总共有哪些策略可以使用,状态模式则不需要;
策略模式需要使用者手动指定需要使用哪一种策略,状态模式则不需要;
策略模式适用于对象在整个生命周期内没有状态变化的情况,而状态模式则适用于对象多状态之间频繁变化的情况;
策略模式一旦选定策略开始执行,中间无法变更策略,而状态模式是可以的;
-
状态模式生命周期中变更状态可以由环境类决定,也可以由各个状态类之间相互改变,比如A状态在执行完操作后,自动将状态对象转换为下一个状态返回给环境类,环境类并不知晓状态发生变化,只是继续调用即可。
public class OpenStatus implements Status{ @Override public Status doSomeThing(){ // 此处省略open状态的逻辑 // 将状态翻转 return new CloseStatus(); } }
public class CloseStatus implements Status{ @Override public Status doSomeThing(){ // 此处省略close状态的逻辑 // 将状态翻转 return new OpenStatus(); } }
public class SwitchContext{ private Status status = new OpenStatus(); public void start(){ for(int i = 0; i < 10; i++){ status = status.doSomeThing(); } } }
全文完。