消除代码坏味道之减少嵌套if的一些方式

分布式朝闻道

共 9696字,需浏览 20分钟

 · 2021-10-24

【最近发生了一些事情,不过是,蚂蚁缘槐夸大国,蚍蜉撼树谈何易。】


在实际的代码开发过程中,经常能够见到一个类或者方法中存在大量的if-else代码块,一个复杂的逻辑几乎没有方法的抽象和提取,只能根据if判断条件去理解业务逻辑。这是典型的代码坏味道。

更有甚者else中还继续嵌套if,甚至出现嵌套层级大于三层的情况,让维护者很是头痛,代码如下所示。

public void handle(String orderId, Double actualPrice, Double couponPrice) {
if (StringUtils.isNotBlank(orderId)) {
if (actualPrice != null) {
if (couponPrice != null) {
// 执行业务逻辑
doSomething();
}
}
} else {
// 其他逻辑
}
}

这是一个简单的订单处理逻辑,当订单号orderId、实际金额actualPrice以及券金额couponPrice均不为空时执行业务逻辑,否则执行其他逻辑。

初学者常常喜欢将判断属性非空的逻辑通过嵌套if方式编写,代码阅读体验比较差,如果逻辑复杂,则嵌套if代码块到处都是,无形中提高了理解成本。这里提供几种优化方式。

使用一层判断减少嵌套if

首先介绍的方法是通过使用一层判断减少嵌套if,具体的做法是对于嵌套if,通过条件运算符对判断条件进行连接,如果判断条件过长则重构提取为一个独立的判断方法,这样代码看起来就会比较精炼。

public void handle2(String orderId, Double actualPrice, Double couponPrice) {
if (isOrderParameterIllegal(orderId, actualPrice, couponPrice)) {
// 执行业务逻辑
doSomething();
} else {
// 其他逻辑
}
}

private boolean isOrderParameterIllegal(String orderId, Double actualPrice, Double couponPrice) {
return StringUtils.isNotBlank(orderId)
&& actualPrice != null && couponPrice != null;
}

上述代码中,将之前的嵌套if通过条件运算符“&&”(AND)进行连接,并将连接之后的复杂条件表达式单独提取为一个方法,只有全部满足才返回true,否则返回false。

这样处理之后,主流程的代码就看起来比较清晰,阅读体验比较好。这种通过合并判断条件,并将判断条件单独提取的方式在开发中是一种常见的消除嵌套if带来的坏味道的方式。

使用卫语句减少嵌套if层级

除了合并判断条件外,还可以通过使用“卫语句”的方式减少嵌套if层级。

经典软件开发著作《重构》中是这样定义“卫语句”的:

如果条件语句极其复杂,就应该将条件语句拆解开,然后逐个检查,并在条件为真时立刻从函数中返回,这样的单独检查通常被称之为“卫语句”(guard clauses)。

简单的说就是对复杂条件进行逐个检查,从反向逻辑进行考虑,一旦不满足条件就直接返回,将不满足条件的情况考虑完全之后,直接执行剩下的正常逻辑即可。还是通过章节开头的案例进行直观展示,代码如下:

public void handle3(String orderId, Double actualPrice, Double couponPrice) {
if (StringUtils.isBlank(orderId)) {
// 一些善后逻辑
otherThing();
return;
}
if (actualPrice == null) {
// 一些善后逻辑
otherThing();
return;
}
if (couponPrice == null) {
// 一些善后逻辑
otherThing();
return;
}
// 执行业务逻辑
doSomething();
}

通过代码案例可以看到,卫语句是对各种异常条件进行先行判断,参数一旦满足这些异常情况则直接结束业务逻辑,执行一些善后操作后就返回了。

当所有考虑到的异常情况被校验之后,直接执行正常的业务逻辑即可。这种代码编写风格直接消除了嵌套if,将代码层级优化为最多嵌套一层if,代码阅读难度大幅度降低,整体代码风格清新,条理,让阅读者心情畅快。这种风格也是笔者比较推崇的,希望读者朋友能够吸收并加以运用。

使用策略模式消除复杂if判断

对于复杂场景的业务判断,通过引入策略模式能够完全消除if,在实际开发场景中,这也是一种经常使用到的编码技巧。

关于策略模式暂时不展开讲解,有经验的同学自然用过,没有学过的同学可以去找资料学一下,比如说《设计模式之禅》。

假设有一个后端接口服务,需要同时对App、微信小程序、支付宝小程序、PC网页端提供服务,此时有个需求要统计来自不同端的用户行为,并针对不同来源的用户进行特定的操作,如图所示。

如果采用传统的if-else或者switch-case方式进行编码,代码看起来如下所示。

public void handleByChannelType(ChannelType channelType) {
if (channelType == ChannelType.PHONE_APP) {
System.out.println("手机App渠道逻辑");
} else if (channelType == ChannelType.PHONE_H5) {
System.out.println("H5渠道逻辑");
} else if (channelType == ChannelType.MICRO_APPLET_WECHAT) {
System.out.println("微信小程序处理逻辑");
} else if (channelType == ChannelType.MICRO_APPLET_ALIPAY) {
System.out.println("支付宝小程序处理逻辑");
} else {
System.out.println("其他逻辑");
}
}

public void handleByChannelType2(ChannelType channelType) {
switch (channelType) {
case PHONE_H5:
System.out.println("手机App渠道逻辑");
break;

case PHONE_APP:
System.out.println("H5渠道逻辑");
break;

case MICRO_APPLET_WECHAT:
System.out.println("微信小程序处理逻辑");
break;

case MICRO_APPLET_ALIPAY:
System.out.println("支付宝小程序处理逻辑");
break;
default:
System.out.println("其他逻辑");
}
}

代码中的渠道类型会一直增加,每个渠道内部的代码逻辑也会逐步修改与增加,随着代码逻辑被不断修改,这部分代码势必会变得越来越复杂,最终难以维护。这是if-else以及switch-case在应对复杂业务场景时天然的不足之处。

那么此时如果使用策略模式进行重构,则能够很好的解决这个问题。

(1)首先定义一个接口提供一个getChannelType()方法,供不同的渠道实现类进行实现,返回实际的渠道类型;同时接口提供了doSomething()方法,表示抽象的业务逻辑,不同的渠道实现类实现该方法,对外提供不同的业务逻辑实现;

public interface ChannelStrategy {
/**具体的业务逻辑*/
void doSomething();
/**获取渠道类型*/
ChannelType getChannelType();
}

(2)定义不同渠道的策略实现类,实现接口ChannelStrategy,支付宝小程序渠道代码如下:

public class MicroAppletAlipayStrategyImpl implements ChannelStrategy {
@Override
public void doSomething() {
System.out.println("支付宝小程序处理逻辑");
}
@Override
public ChannelType getChannelType() {
return ChannelType.MICRO_APPLET_ALIPAY;
}
}

微信小程序代码如下:

public class MicroAppletWechatStrategyImpl implements ChannelStrategy {
@Override
public void doSomething() {
System.out.println("微信小程序处理逻辑");
}
@Override
public ChannelType getChannelType() {
return ChannelType.MICRO_APPLET_WECHAT;
}
}

手机App渠道的策略实现如下:

public class PhoneAppChannelStrategyImpl implements ChannelStrategy {
@Override
public void doSomething() {
System.out.println("手机App渠道逻辑");
}
@Override
public ChannelType getChannelType() {
return ChannelType.PHONE_APP;
}
}

手机H5页面的渠道策略如下:

public class PhoneH5ChannelStrategyImpl implements ChannelStrategy {
@Override
public void doSomething() {
System.out.println("H5渠道逻辑");
}
@Override
public ChannelType getChannelType() {
return ChannelType.PHONE_H5;
}
}

(3)接着定义策略上下文,该上下文中包含一个Map,上下文定义为单例,在实例初始化阶段加载不同的渠道逻辑实现到Map中;

public class ChannelStrategyContext {

private static final Map CHANNEL_STRATEGY_MAP = new ConcurrentHashMap<>();

private ChannelStrategyContext() {
ChannelStrategy phoneH5 = new PhoneAppChannelStrategyImpl();
ChannelStrategy phoneApp = new PhoneAppChannelStrategyImpl();
ChannelStrategy appletAlipay = new MicroAppletAlipayStrategyImpl();
ChannelStrategy appletWechat = new MicroAppletWechatStrategyImpl();

CHANNEL_STRATEGY_MAP.put(phoneH5.getChannelType(), phoneH5);
CHANNEL_STRATEGY_MAP.put(phoneApp.getChannelType(), phoneApp);
CHANNEL_STRATEGY_MAP.put(appletAlipay.getChannelType(), appletAlipay);
CHANNEL_STRATEGY_MAP.put(appletWechat.getChannelType(), appletWechat);
}


public static ChannelStrategyContext getInstance() {
return Holder.singleton;
}

public void domeSomething(ChannelType channelType) {
CHANNEL_STRATEGY_MAP.get(channelType).doSomething();
}

/**
* 内部类,为单例提供
*/
private static class Holder {
private static ChannelStrategyContext singleton = new ChannelStrategyContext();
}
}

(4)接着在策略上下文中定义doSomething()方法,通过委托的方式根据方法参数中传入的ChannelType从Map中筛选出对应的渠道对象,并调用该渠道对象的doSomething()方法;

编写一段代码测试一下通过策略模式改写后的逻辑,假设上游传递的ChannelType类型为微信小程序MICRO_APPLET_WECHAT,则业务逻辑代码如下所示:

public class Main {
public static void main(String[] args) {
ChannelType wechatApplet = ChannelType.MICRO_APPLET_WECHAT;
ChannelStrategyContext.getInstance().domeSomething(wechatApplet);
}
}

运行测试代码,观察控制台输出如下:

微信小程序处理逻辑
Process finished with exit code 0

可以看到,相比于if-else或者switch-case,主流程的代码非常简洁,只需要一行代码就能根据ChannelType执行到对应的逻辑,代码逻辑更容易被人所理解。一旦需要增加新的渠道,只需要增加一个新的ChannelStrategy实现类,并添加到ChannelStrategyContext上下文中。这体现出了代码编写中的开发封闭原则,即对主业务流程的修改是关闭的,对渠道的新增是开放的。

相信有的读者发现,每次新增新的渠道实现都需要修改一下ChannelStrategyContext,还不够友好,「有没有一种方式能够只增加渠道的实现就可以动态的将新的渠道类型注册到ChannelStrategyContext中呢?」

Spring集合注入方式动态装载策略容器

其实是有的,可以通过基于Spring集合类型注入的方式对接口所有实现类批量注入从而避免手动编写大量的put代码,实现新增渠道实现类不需要修改ChannelStrategyContext的目的。接下来对这种方式进行讲解,读者可以根据自己的理解与喜好在实战中加以应用。

首先介绍Spring集合类型批量注入接口实现类的方式,首先将ChannelStrategy的接口实现类均标注为Spring的Bean,如使用@Service、@Component注解,代码如下(以支付宝渠道策略实现类为例)。

@Service
public class MicroAppletAlipayStrategyImpl implements ChannelStrategy {
@Override
public void doSomething() {
System.out.println("支付宝小程序处理逻辑");
}
@Override
public ChannelType getChannelType() {
return ChannelType.MICRO_APPLET_ALIPAY;
}
}

接着编写Spring配置类,通过@ComponentScan注解配置扫描包,开启注解支持。配置类代码如下:

@Configuration
@ComponentScan(basePackages = {"com.snowalker.from.distributed.to.cloudnative.section11_4.channeldemo.spring_collection_inject"})
public class BeanConfig {
}

¬接着编写ChannelStrategySpringContext策略上下文,通过@Autowired注入List< ChannelStrategy>集合,完整的ChannelStrategySpringContext代码如下:

@Service
public class ChannelStrategySpringContext {

private static final Map CHANNEL_STRATEGY_MAP = new ConcurrentHashMap<>();

@Autowired
List channelStrategies;

@PostConstruct
public void init() {
channelStrategies.stream().forEach(channelStrategy -> {
CHANNEL_STRATEGY_MAP.put(channelStrategy.getChannelType(), channelStrategy);
});
}

public void domeSomething(ChannelType channelType) {
CHANNEL_STRATEGY_MAP.get(channelType).doSomething();
}
}

对比上面未使用Spring框架的原生Java实现的ChannelStrategyContext,此处的ChannelStrategySpringContext代码量更少,逻辑更加简洁。

通过直接注入List,Spring框架会在Bean的加载阶段,将ChannelStrategy接口实现类装载到集合List中,这部分工作在Spring的DefaultListableBeanFactory.resolveDependency()中完成,感兴趣的读者可以自行查看。

通过@PostContruct注解标注的init()方法,将ChannelStrategy接口的所有实例解析出来并加载到Map中,方便在方法中根据具体的ChannelType获取对应的ChannelStrategy实现。

其他方法和原生Java实现相同,但是省略了内部类以及获取单例方法,原因在于Spring中Bean默认为单例,因此不需要再显式书写单例相关的代码。

为方便读者理解,将这部分逻辑用一张流程图展示如图所示。

最后编写测试代码进行调用,依旧指定渠道类型ChannelType为微信小程序,测试代码如下:

public class Client {
public static void main(String[] args) {
ApplicationContext applicationContext =
new AnnotationConfigApplicationContext(BeanConfig.class);
ChannelStrategySpringContext channelStrategySpringContext =
applicationContext.getBean("channelStrategySpringContext", ChannelStrategySpringContext.class);
channelStrategySpringContext
.domeSomething(ChannelType.MICRO_APPLET_WECHAT);
}
}

代码逻辑为:先定义AnnotationConfigApplicationContext上下文,加载BeanConfig配置类,开启注解支持。然后从上下文中根据bean名称获取到ChannelStrategySpringContext的实例,调用ChannelStrategySpringContext的domeSomething(ChannelType channelType)方法,指定ChannelType为微信小程序ChannelType.MICRO_APPLET_WECHAT。代码运行结果如下:

微信小程序处理逻辑
Process finished with exit code 0

到此就对如何基于策略模式消除代码中的if逻辑进行了充分的讲解,希望能够对读者提升代码质量,消除过多if带来的坏味道有所帮助。

后记:本文是新书的第11章节的节选,主要是讲解了开发中常用的消除嵌套if的一些策略,更多内容正在持续输出中,敬请期待。

浏览 146
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报