- 技巧一:提取方法,拆分逻辑
- 技巧二:分支逻辑提前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 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能。
技巧二:分支逻辑提前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 。
技巧三:枚举
经过“技巧一”和“技巧二”的优化,文章开头的这段代码被优化成如下所示:
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。与君共勉,共同进步!