满屏的if-else,看我怎么消灭你!

Posted Hollis Chuang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了满屏的if-else,看我怎么消灭你!相关的知识,希望对你有一定的参考价值。

来源:u6.gg/k376d

  • 技巧一:提取方法,拆分逻辑

  • 技巧二:分支逻辑提前return

  • 技巧三:枚举

  • 技巧四:函数式接口

  • 技巧五:设计模式

  • 其他技巧

  • 写在最后

在实际的业务开发当中,经常会遇到复杂的业务逻辑,可能部分同学实现出来的代码并没有什么问题,但是代码的可读性很差。

本篇文章主要总结一下自己在实际开发中如何避免大面积的 if-else 代码块的问题。补充说明一点,不是说 if-else 不好,而是多层嵌套的 if-else 导致代码可读性差、维护成本高等问题。

现有如下一段示例代码,部分优化技巧是根据这段代码进行的:

public class BadCodeDemo 

    private void getBadCodeBiz(Integer city, List<TestCodeData> newDataList, List<TestCodeData> oldDataList) 
        if (city != null) 

            if (newDataList != null && newDataList.size() > 0) 
                TestCodeData newData = newDataList.stream().filter(p -> 
                    if (p.getIsHoliday() == 1) 
                        return true;
                    
                    return false;
                ).findFirst().orElse(null);
                if (newData != null) 
                    newData.setCity(city);
                
            

         else 

            if (oldDataList != null && newDataList != null) 
                List<TestCodeData> oldCollect = oldDataList.stream().filter(p -> 
                    if (p.getIsHoliday() == 1) 
                        return true;
                    
                    return false;

                ).collect(Collectors.toList());
                List<TestCodeData> newCollect = newDataList.stream().filter(p -> 
                    if (p.getIsHoliday() == 1) 
                        return true;
                    
                    return false;
                ).collect(Collectors.toList());

                if (newCollect != null && newCollect.size() > 0 && oldCollect != null && oldCollect.size() > 0) 
                    for (TestCodeData newPO : newCollect) 
                        if (newPO.getStartTime() == 0 && newPO.getEndTime() == 12) 
                            TestCodeData po = oldCollect.stream().filter(p -> p.getStartTime() == 0
                                    && (p.getEndTime() == 12 || p.getEndTime() == 24)).findFirst().orElse(null);
                            if (po != null) 
                                newPO.setCity(po.getCity());
                            
                         else if (newPO.getStartTime() == 12 && newPO.getEndTime() == 24) 
                            TestCodeData po = oldCollect.stream().filter(
                                    p -> (p.getStartTime() == 12 || p.getStartTime() == 0)
                                            && p.getEndTime() == 24).findFirst().orElse(null);
                            if (po != null) 
                                newPO.setCity(po.getCity());
                            
                         else if (newPO.getStartTime() == 0 && newPO.getEndTime() == 24) 
                            TestCodeData po = oldCollect.stream().filter(
                                    p -> p.getStartTime() == 0 && p.getEndTime() == 24).findFirst().orElse(null);
                            if (po == null) 
                                po = oldCollect.stream().filter(
                                        p -> p.getStartTime() == 0 && p.getEndTime() == 12).findFirst().orElse(null);
                            
                            if (po == null) 
                                po = oldCollect.stream().filter(
                                        p -> p.getStartTime() == 12 && p.getEndTime() == 24).findFirst().orElse(null);
                            
                            if (po != null) 
                                newPO.setCity(po.getCity());
                            
                         else if (newPO.getTimeUnit().equals(Integer.valueOf(1))) 
                            TestCodeData po = oldCollect.stream().filter(
                                    e -> e.getTimeUnit().equals(Integer.valueOf(1))).findFirst().orElse(null);
                            if (po != null) 
                                newPO.setCity(po.getCity());
                            
                        
                    
                

            
        
    

技巧一:提取方法,拆分逻辑

比如上面这段代码中:

if(null != city) 

 else 

这里可以拆分成两段逻辑,核心思想就是逻辑单元最小化,然后合并逻辑单元。

private void getCityNotNull(Integer city, List<TestCodeData> newDataList) 
    if (newDataList != null && newDataList.size() > 0) 
        TestCodeData newData = newDataList.stream().filter(p -> 
            if (p.getIsHoliday() == 1) 
                return true;
            
            return false;
        ).findFirst().orElse(null);
        if (newData != null) 
            newData.setCity(city);
        
    


// 合并逻辑流程
private void getBadCodeBiz(Integer city, List<TestCodeData> newDataList, List<TestCodeData> oldDataList) 
    if (city != null) 
        this.getCityNull(city, newDataList);
     else 
        //此处代码省略
    

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能。

项目地址:https://github.com/YunaiV/ruoyi-vue-pro

技巧二:分支逻辑提前return

比如“技巧一”中的 getCityNull 方法,我们可以这样写:

public void getCityNotNull(Integer city, List<TestCodeData> newDataList) 
    if (CollectionUtils.isEmpty(newDataList)) 
        // 提前判断,返回业务逻辑
        return;
    
    TestCodeData newData = newDataList.stream().filter(p -> 
        if (p.getIsHoliday() == 1) 
            return true;
        
        return false;
    ).findFirst().orElse(null);

    if (null != newData) 
        newData.setCity(city);
    

基于微服务的思想,构建在 B2C 电商场景下的项目实战。核心技术栈,是 Spring Boot + Dubbo 。未来,会重构成 Spring Cloud Alibaba 。

项目地址:https://github.com/YunaiV/onemall

技巧三:枚举

经过“技巧一”和“技巧二”的优化,文章开头的这段代码被优化成如下所示:

public class BadCodeDemo 

    public void getBadCodeBiz(Integer city, List<TestCodeData> newDataList, List<TestCodeData> oldDataList) 
        if (city != null) 
            this.getCityNotNull(city, newDataList);
         else 
            this.getCityNull(newDataList, oldDataList);
        
    

    private void getCityNotNull(Integer city, List<TestCodeData> newDataList) 
        if (CollectionUtils.isEmpty(newDataList)) 
            // 提前判断,返回业务逻辑
            return;
        
        TestCodeData newData = newDataList.stream().filter(p -> 
            if (p.getIsHoliday() == 1) 
                return true;
            
            return false;
        ).findFirst().orElse(null);

        if (null != newData) 
            newData.setCity(city);
        
    

    private void getCityNull(List<TestCodeData> newDataList, List<TestCodeData> oldDataList) 
        // 提前判断,返回业务逻辑
        if (CollectionUtils.isEmpty(oldDataList) && CollectionUtils.isEmpty(newDataList)) 
            return;
        

        List<TestCodeData> oldCollect = oldDataList.stream().filter(p -> 
            if (p.getIsHoliday() == 1) 
                return true;
            
            return false;
        ).collect(Collectors.toList());

        List<TestCodeData> newCollect = newDataList.stream().filter(p -> 
            if (p.getIsHoliday() == 1) 
                return true;
            
            return false;
        ).collect(Collectors.toList());

        // 提前判断,返回业务逻辑
        if (CollectionUtils.isEmpty(newCollect) && CollectionUtils.isEmpty(oldCollect)) 
            return;
        

        for (TestCodeData newPO : newCollect) 
            if (newPO.getStartTime() == 0 && newPO.getEndTime() == 12) 
                TestCodeData po = oldCollect.stream().filter(p -> p.getStartTime() == 0
                        && (p.getEndTime() == 12 || p.getEndTime() == 24)).findFirst().orElse(null);
                if (po != null) 
                    newPO.setCity(po.getCity());
                
             else if (newPO.getStartTime() == 12 && newPO.getEndTime() == 24) 
                TestCodeData po = oldCollect.stream().filter(
                        p -> (p.getStartTime() == 12 || p.getStartTime() == 0)
                                && p.getEndTime() == 24).findFirst().orElse(null);
                if (po != null) 
                    newPO.setCity(po.getCity());
                
             else if (newPO.getStartTime() == 0 && newPO.getEndTime() == 24) 
                TestCodeData po = oldCollect.stream().filter(
                        p -> p.getStartTime() == 0 && p.getEndTime() == 24).findFirst().orElse(null);
                if (po == null) 
                    po = oldCollect.stream().filter(
                            p -> p.getStartTime() == 0 && p.getEndTime() == 12).findFirst().orElse(null);
                
                if (po == null) 
                    po = oldCollect.stream().filter(
                            p -> p.getStartTime() == 12 && p.getEndTime() == 24).findFirst().orElse(null);
                
                if (po != null) 
                    newPO.setCity(po.getCity());
                
             else if (newPO.getTimeUnit().equals(Integer.valueOf(1))) 
                TestCodeData po = oldCollect.stream().filter(
                        e -> e.getTimeUnit().equals(Integer.valueOf(1))).findFirst().orElse(null);
                if (po != null) 
                    newPO.setCity(po.getCity());
                
            
        
    

现在利用“枚举”来优化 getCityNull 方法中的 for 循环部分代码,我们可以看到这段代码中有 4 段逻辑,总体形式如下:

if (newPO.getStartTime() == 0 && newPO.getEndTime() == 12) 
    //第一段逻辑
 else if (newPO.getStartTime() == 12 && newPO.getEndTime() == 24) 
    //第二段逻辑
 else if (newPO.getStartTime() == 0 && newPO.getEndTime() == 24) 
    //第三段逻辑
 else if (newPO.getTimeUnit().equals(Integer.valueOf(1))) 
    //第四段逻辑

按照这个思路利用枚举进行二次优化,将其中的逻辑封装到枚举类中:

public enum TimeEnum 

    AM("am", "上午") 
        @Override
        public void setCity(TestCodeData data, List<TestCodeData> oldDataList) 
            TestCodeData po = oldDataList.stream().filter(p -> p.getStartTime() == 0
                    && (p.getEndTime() == 12 || p.getEndTime() == 24)).findFirst().orElse(null);
            if (null != po) 
                data.setCity(po.getCity());
            
        
    ,
    PM("pm", "下午") 
        @Override
        public void setCity(TestCodeData data, List<TestCodeData> oldCollect) 
            TestCodeData po = oldCollect.stream().filter(
                    p -> (p.getStartTime() == 12 || p.getStartTime() == 0)
                            && p.getEndTime() == 24).findFirst().orElse(null);
            if (po != null) 
                data.setCity(po.getCity());
            
        
    ,
    DAY("day", "全天") 
        @Override
        public void setCity(TestCodeData data, List<TestCodeData> oldCollect) 
            TestCodeData po = oldCollect.stream().filter(
                    p -> p.getStartTime() == 0 && p.getEndTime() == 24).findFirst().orElse(null);
            if (po == null) 
                po = oldCollect.stream().filter(
                        p -> p.getStartTime() == 0 && p.getEndTime() == 12).findFirst().orElse(null);
            
            if (po == null) 
                po = oldCollect.stream().filter(
                        p -> p.getStartTime() == 12 && p.getEndTime() == 24).findFirst().orElse(null);
            
            if (po != null) 
                data.setCity(po.getCity());
            
        
    ,
    HOUR("hour", "小时") 
        @Override
        public void setCity(TestCodeData data, List<TestCodeData> oldCollect) 
            TestCodeData po = oldCollect.stream().filter(
                    e -> e.getTimeUnit().equals(Integer.valueOf(1))).findFirst().orElse(null);
            if (po != null) 
                data.setCity(po.getCity());
            
        
    ;

    public abstract void setCity(TestCodeData data, List<TestCodeData> oldCollect);

    private String code;
    private String desc;

    TimeEnum(String code, String desc) 
        this.code = code;
        this.desc = desc;
    

    public String getCode() 
        return code;
    

    public void setCode(String code) 
        this.code = code;
    

    public String getDesc() 
        return desc;
    

    public void setDesc(String desc) 
        this.desc = desc;
    

然后 getCityNull 方法中 for 循环部分逻辑如下:

for (TestCodeData data : newCollect) 
    if (data.getStartTime() == 0 && data.getEndTime() == 12) 
        TimeEnum.AM.setCity(data, oldCollect);
     else if (data.getStartTime() == 12 && data.getEndTime() == 24) 
        TimeEnum.PM.setCity(data, oldCollect);
     else if (data.getStartTime() == 0 && data.getEndTime() == 24) 
        TimeEnum.DAY.setCity(data, oldCollect);
     else if (data.getTimeUnit().equals(Integer.valueOf(1))) 
        TimeEnum.HOUR.setCity(data, oldCollect);
    

其实在这个业务场景中使用枚举并不是特别合适,如果在遍历对象时,我们就知道要执行哪个枚举类型,此时最合适,伪代码如下:

for (TestCodeData data : newCollect) 
      String code = "am";  // 这里假设 code 变量是从 data 中获取的
      TimeEnum.valueOf(code).setCity(data, oldCollect);

技巧四:函数式接口

业务场景描述:比如让你做一个简单的营销拉新活动,这个活动投放到不同的渠道,不同渠道过来的用户奖励不一样。

现假设在头条、微信等渠道都投放了该活动。此时你的代码可能会写出如下形式:

@RestController
@RequestMapping("/activity")
public class ActivityController 
    @Resource
    private AwardService awardService;

    @PostMapping("/reward")
    public void reward(String userId, String source) 
        if ("toutiao".equals(source)) 
            awardService.toutiaoReward(userId);
         else if ("wx".equals(source)) 
            awardService.wxReward(userId);
        
    


@Service
public class AwardService 
    private static final Logger log = LoggerFactory.getLogger(AwardService.class);

    public Boolean toutiaoReward(String userId) 
        log.info("头条渠道用户奖励50元红包!", userId);
        return Boolean.TRUE;
    

    public Boolean wxReward(String userId) 
        log.info("微信渠道用户奖励100元红包!", userId);
        return Boolean.TRUE;
    

看完这段代码,逻辑上是没有什么问题的。但它有一个隐藏的缺陷,如果后期又增加很多渠道的时候,你该怎么办?继续 else if 吗?

其实我们可以利用函数式接口优化,当然设计模式也可以优化。这里我只是举例使用一下函数式接口的使用方式。

@RestController
@RequestMapping("/activity")
public class ActivityController 
    @Resource
    private AwardService awardService;

    @PostMapping("/reward")
    public void reward(String userId, String source) 
        awardService.getRewardResult(userId, source);
    


@Service
public class AwardService 
    private static final Logger log = LoggerFactory.getLogger(AwardService.class);
    private Map<String, BiFunction<String, String, Boolean>> sourceMap = new HashMap<>();

    @PostConstruct
    private void dispatcher() 
        sourceMap.put("wx", (userId, source) -> this.wxReward(userId));
        sourceMap.put("toutiao", (userId, source) -> this.toutiaoReward(userId));
    

    public Boolean getRewardResult(String userId, String source) 
        BiFunction<String, String, Boolean> result = sourceMap.get(source);
        if (null != result) 
            return result.apply(userId, source);
        
        return Boolean.FALSE;
    

    private Boolean toutiaoReward(String userId) 
        log.info("头条渠道用户奖励50元红包!", userId);
        return Boolean.TRUE;
    

    private Boolean wxReward(String userId) 
        log.info("微信渠道用户奖励100元红包!", userId);
        return Boolean.TRUE;
    

针对一些复杂的业务场景,业务参数很多时,可以利用 @FunctionalInterface 自定义函数式接口来满足你的业务需求,使用原理和本例并无差别。

技巧五:设计模式

设计模式对于 if-else 的优化,我个人觉得有些重,但是也是一种优化方式。设计模式适合使用在大的业务流程和场景中使用,针对代码块中的 if-else 逻辑优化不推荐使用。

常用的设计模式有:

  • 策略模式

  • 模板方法

  • 工厂模式

  • 单例模式

还是以上面的营销拉新活动为例来说明如何使用。

使用技巧一:工厂模式+抽象类

定义抽象业务接口:

public abstract class AwardAbstract 
    public abstract Boolean award(String userId);

定义具体业务实现类:

// 头条渠道发放奖励业务
public class TouTiaoAwardService extends AwardAbstract 
    @Override
    public Boolean award(String userId) 
        log.info("头条渠道用户奖励50元红包!", userId);
        return Boolean.TRUE;
    


// 微信渠道发放奖励业务
public class WeChatAwardService extends AwardAbstract 
    @Override
    public Boolean award(String userId) 
        log.info("微信渠道用户奖励100元红包!", userId);
        return Boolean.TRUE;
    

利用工厂模式获取实例对象:

public class AwardFactory 
    public static AwardAbstract getAwardInstance(String source) 
        if ("toutiao".equals(source)) 
            return new TouTiaoAwardService();
         else if ("wx".equals(source)) 
            return new WeChatAwardService();
        
        return null;
    

业务入口处根据不同渠道执行不同的发放逻辑:

@PostMapping("/reward2")
public void reward2(String userId, String source) 
    AwardAbstract instance = AwardFactory.getAwardInstance(source);
    if (null != instance) 
        instance.award(userId);
    
使用技巧二:策略模式+模板方法+工厂模式+单例模式

还是以营销拉新为业务场景来说明,这个业务流程再增加一些复杂度,比如发放奖励之前要进行身份验证、风控验证等一些列的校验,此时你的业务流程该如何实现更清晰简洁呢?

定义业务策略接口:

/** 策略业务接口 */
public interface AwardStrategy 
    /**
     * 奖励发放接口
     */
    Map<String, Boolean> awardStrategy(String userId);

    /**
     * 获取策略标识,即不同渠道的来源标识
     */
    String getSource();

定义奖励发放模板流程:

public abstract class BaseAwardTemplate 
    private static final Logger log = LoggerFactory.getLogger(BaseAwardTemplate.class);

    //奖励发放模板方法
    public Boolean awardTemplate(String userId) 
        this.authentication(userId);
        this.risk(userId);
        return this.awardRecord(userId);
    

    //身份验证
    protected void authentication(String userId) 
        log.info(" 执行身份验证!", userId);
    

    //风控
    protected void risk(String userId) 
        log.info(" 执行风控校验!", userId);
    

    //执行奖励发放
    protected abstract Boolean awardRecord(String userId);

实现不同渠道的奖励业务:

@Slf4j
@Service
public class ToutiaoAwardStrategyService extends BaseAwardTemplate implements AwardStrategy 
    /**
     * 奖励发放接口
     */
    @Override
    public Boolean awardStrategy(String userId) 
        return super.awardTemplate(userId);
    

    @Override
    public String getSource() 
        return "toutiao";
    

    /**
     * 具体的业务奖励发放实现
     */
    @Override
    protected Boolean awardRecord(String userId) 
        log.info("头条渠道用户奖励50元红包!", userId);
        return Boolean.TRUE;
    



@Slf4j
@Service
public class WeChatAwardStrategyService extends BaseAwardTemplate implements AwardStrategy 
    /**
     * 奖励发放接口
     */
    @Override
    public Boolean awardStrategy(String userId) 
        return super.awardTemplate(userId);
    

    @Override
    public String getSource() 
        return "wx";
    

    /***
     * 具体的业务奖励发放实现
     */
    @Override
    protected Boolean awardRecord(String userId) 
        log.info("微信渠道用户奖励100元红包!", userId);
        return Boolean.TRUE;
    

定义工厂方法,对外统一暴露业务调用入口:

@Component
public class AwardStrategyFactory implements ApplicationContextAware 
    private final static Map<String, AwardStrategy> MAP = new HashMap<>();

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException 
        Map<String, AwardStrategy> beanTypeMap = applicationContext.getBeansOfType(AwardStrategy.class);
        beanTypeMap.values().forEach(strategyObj -> MAP.put(strategyObj.getSource(), strategyObj));
    

    /**
     * 对外统一暴露的工厂方法
     */
    public Boolean getAwardResult(String userId, String source) 
        AwardStrategy strategy = MAP.get(source);
        if (Objects.isNull(strategy)) 
            throw new RuntimeException("渠道异常!");
        
        return strategy.awardStrategy(userId);
    

    /**
     * 静态内部类创建单例工厂对象
     */
    private static class CreateFactorySingleton 
        private static AwardStrategyFactory factory = new AwardStrategyFactory();
    

    public static AwardStrategyFactory getInstance() 
        return CreateFactorySingleton.factory;
    

业务入口方法:

@RestController
@RequestMapping("/activity")
public class ActivityController 

    @PostMapping("/reward3")
    public void reward3(String userId, String source) 
        AwardStrategyFactory.getInstance().getAwardResult(userId, source);
    

假如发起请求:POST http://localhost:8080/activity/reward3?userId=fei&source=wx

2022-02-20 12:23:27.716  INFO 20769 --- [nio-8080-exec-1] c.a.c.e.o.c.p.s.BaseAwardTemplate        : fei 执行身份验证!
2022-02-20 12:23:27.719  INFO 20769 --- [nio-8080-exec-1] c.a.c.e.o.c.p.s.BaseAwardTemplate        : fei 执行风控校验!
2022-02-20 12:23:27.719  INFO 20769 --- [nio-8080-exec-1] a.c.e.o.c.p.s.WeChatAwardStrategyService : 微信渠道用户fei奖励100元红包!

其他技巧

  • 使用三目运算符

  • 相同业务逻辑提取复用

写在最后

不论使用那种技巧,首先是我们在业务代码开发过程中一定要多思考,将复杂的业务逻辑能通过简洁的代码表现出来,这才是你的核心能力之一,而不是一个 curd boy。与君共勉,共同进步!

有道无术,术可成;有术无道,止于术

欢迎大家关注Java之道公众号

好文章,我在看❤️

以上是关于满屏的if-else,看我怎么消灭你!的主要内容,如果未能解决你的问题,请参考以下文章

满屏的if-else,看我怎么消灭你!

满屏的if-else,看我怎么消灭你!

求求你们了,别再写满屏的 if/ else 了!

参数校验别再写满屏的 if/else 了,差点被劝退……

别再写满屏的爆爆爆炸类了,试试装饰器模式,这才是优雅的方式!!

别再写满屏的爆爆爆炸类了,试试装饰器模式,这才是优雅的方式!!