彻底消灭if-else嵌套

一、背景

1.1 反面教材

不知大家有没遇到过像 横放着的金字塔一样的 if-else嵌套:

if (true) {
    if (true) {
        if (true) {
            if (true) {
                if (true) {
                    if (true) {

                    }
                }
            }
        }
    }
}

if-else作为每种编程语言都不可或缺的条件语句,我们在编程时会大量的用到。

if-else一般不建议嵌套超过三层,如果一段代码存在过多的 if-else嵌套,代码的可读性就会急速下降,后期维护难度也大大提高。

2.2 亲历的重构

前阵子重构了 服务费收费规则,重构前的 if-else嵌套如下。

public Double commonMethod(Integer type, Double amount) {
    if (3 == type) {
        // 计算费用
        if (true) {
            // 此处省略200行代码,包含n个if-else,下同。。。
        }
        return 0.00;
    } else if (2 == type) {
        // 计算费用
        return 6.66;
    }else if (1 == type) {
        // 计算费用
        return 8.88;
    }else if (0 == type){
        return 9.99;
    }
    throw new IllegalArgumentException("please input right value");
}

我们都写过类似的代码,回想起被 if-else 支配的恐惧,如果有新需求:新增计费规则或者修改既定计费规则,无所下手。

2.3 追根溯源

  • 我们来分析下代码多分支的原因

  • 业务判断

  • 空值判断
  • 状态判断

  • 如何处理呢?

  • 在有多种算法相似的情况下,利用策略模式,把业务判断消除,各子类实现同一个接口,只关注自己的实现( 本文核心);

  • 尽量把所有空值判断放在外部完成,内部传入的变量由外部接口保证不为空,从而减少空值判断(可参考如何从 if-else 的参数校验中解放出来?);
  • 把分支状态信息预先缓存在 Map里,直接 get获取具体值,消除分支(本文也有体现)。

  • 来看看简化后的业务调用

CalculationUtil.getFee(type, amount)

或者

serviceFeeHolder.getFee(type, amount)

是不是超级简单,下面介绍两种实现方式(文末附示例代码)。

二、通用部分

2.1 需求概括

我们拥有很多公司会员,暂且分为普通会员、初级会员、中级会员和高级会员,会员级别不同计费规则不同。该模块负责计算会员所需的缴纳的服务费。

2.2 会员枚举

用于维护会员类型。

public enum MemberEnum {

    ORDINARY_MEMBER(0, "普通会员"),
    JUNIOR_MEMBER(1, "初级会员"),
    INTERMEDIATE_MEMBER(2, "中级会员"),
    SENIOR_MEMBER(3, "高级会员"),

    ;

    int code;
    String desc;

    MemberEnum(int code, String desc) {
        this.code = code;
        this.desc = desc;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getDesc() {
        return desc;
    }

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

}

2.3 定义一个策略接口

该接口包含两个方法:

  1. compute(Double amount):各计费规则的抽象
  2. getType():获取枚举中维护的会员级别
public interface FeeService {

    /**
     * 计费规则
     * @param amount 会员的交易金额
     * @return
     */
    Double compute(Double amount);

    /**
     * 获取会员级别
     * @return
     */
    Integer getType();
}

三、非框架实现

3.1 项目依赖


    junit
    junit
    4.12
    test

3.2 不同计费规则的实现

这里四个子类实现了策略接口,其中 compute()方法实现各个级别会员的计费逻辑, getType()指定了该类所属的会员级别。

  • 普通会员计费规则
public class OrdinaryMember implements FeeService {

    /**
     * 计算普通会员所需缴费的金额
     * @param amount 会员的交易金额
     * @return
     */
    @Override
    public Double compute(Double amount) {
        // 具体的实现根据业务需求修改
        return 9.99;
    }

    @Override
    public Integer getType() {
        return MemberEnum.ORDINARY_MEMBER.getCode();
    }
}
  • 初级会员计费规则
public class JuniorMember implements FeeService {

    /**
     * 计算初级会员所需缴费的金额
     * @param amount 会员的交易金额
     * @return
     */
    @Override
    public Double compute(Double amount) {
        // 具体的实现根据业务需求修改
        return 8.88;
    }

    @Override
    public Integer getType() {
        return MemberEnum.JUNIOR_MEMBER.getCode();
    }
}
  • 中级会员计费规则
public class IntermediateMember implements FeeService {

    /**
     * 计算中级会员所需缴费的金额
     * @param amount 会员的交易金额
     * @return
     */
    @Override
    public Double compute(Double amount) {
        // 具体的实现根据业务需求修改
        return 6.66;
    }

    @Override
    public Integer getType() {
        return MemberEnum.INTERMEDIATE_MEMBER.getCode();
    }
}
  • 高级会员计费规则
public class SeniorMember implements FeeService {

    /**
     * 计算高级会员所需缴费的金额
     * @param amount 会员的交易金额
     * @return
     */
    @Override
    public Double compute(Double amount) {
        // 具体的实现根据业务需求修改
        return 0.01;
    }

    @Override
    public Integer getType() {
        return MemberEnum.SENIOR_MEMBER.getCode();
    }
}

3.3 核心工厂

创建一个工厂类 ServiceFeeFactory.java,该工厂类管理所有的策略接口实现类。具体见代码注释。

public class ServiceFeeFactory {

    private Map map;

    public ServiceFeeFactory() {

        // 该工厂管理所有的策略接口实现类
        List feeServices = new ArrayList<>();

        feeServices.add(new OrdinaryMember());
        feeServices.add(new JuniorMember());
        feeServices.add(new IntermediateMember());
        feeServices.add(new SeniorMember());

        // 把所有策略实现的集合List转为Map
        map = new ConcurrentHashMap<>();
        for (FeeService feeService : feeServices) {
            map.put(feeService.getType(), feeService);
        }
    }

    /**
     * 静态内部类单例
     */
    public static class Holder {
        public static ServiceFeeFactory instance = new ServiceFeeFactory();
    }

    /**
     * 在构造方法的时候,初始化好 需要的 ServiceFeeFactory
     * @return
     */
    public static ServiceFeeFactory getInstance() {
        return Holder.instance;
    }

    /**
     * 根据会员的级别type 从map获取相应的策略实现类
     * @param type
     * @return
     */
    public FeeService get(Integer type) {
        return map.get(type);
    }
}

3.4 工具类

新建通过一个工具类管理计费规则的调用,并对不符合规则的公司级别输入抛 IllegalArgumentException

public class CalculationUtil {

    /**
     * 暴露给用户的的计算方法
     * @param type 会员级别标示(参见 MemberEnum)
     * @param money 当前交易金额
     * @return 该级别会员所需缴纳的费用
     * @throws IllegalArgumentException 会员级别输入错误
     */
    public static Double getFee(int type, Double money) {
        FeeService strategy = ServiceFeeFactory.getInstance().get(type);
        if (strategy == null) {
            throw new IllegalArgumentException("please input right value");
        }
        return strategy.compute(money);
    }
}

核心是 通过 Mapget() 方法,根据传入 type ,即可获取到对应会员类型计费规则的实现,从而减少了 if-else 的业务判断。

3.5 测试

public class DemoTest {

    @Test
    public void test() {
        Double fees = upMethod(1,20000.00);
        System.out.println(fees);
        // 会员级别超范围,抛 IllegalArgumentException
        Double feee = upMethod(5, 20000.00);
    }

    public Double upMethod(Integer type, Double amount) {
        // getFee()是暴露给用户的的计算方法
        return CalculationUtil.getFee(type, amount);
    }
}
  • 执行结果
8.88
java.lang.IllegalArgumentException: please input right value

四、 Spring Boot 实现

上述方法无非是借助策略模式+工厂模式+单例模式实现,但是实际场景中,我们都已经集成了 Spring Boot,这一段就看一下如何借助 Spring Boot更简单实现本次的优化。

4.1 项目依赖


        org.springframework.boot
        spring-boot-starter-test

        org.springframework.boot
        spring-boot-configuration-processor
        true

4.2 不同计费规则的实现

这部分是与上面区别在于: 把策略的实现类得是交给Spring 容器管理

  • 普通会员计费规则
@Component
public class OrdinaryMember implements FeeService {

    /**
     * 计算普通会员所需缴费的金额
     * @param amount 会员的交易金额
     * @return
     */
    @Override
    public Double compute(Double amount) {
        // 具体的实现根据业务需求修改
        return 9.99;
    }

    @Override
    public Integer getType() {
        return MemberEnum.ORDINARY_MEMBER.getCode();
    }
}
  • 初级会员计费规则
@Component
public class JuniorMember implements FeeService {

    /**
     * 计算初级会员所需缴费的金额
     * @param amount 会员的交易金额
     * @return
     */
    @Override
    public Double compute(Double amount) {
        // 具体的实现根据业务需求修改
        return 8.88;
    }

    @Override
    public Integer getType() {
        return MemberEnum.JUNIOR_MEMBER.getCode();
    }
}
  • 中级会员计费规则
@Component
public class IntermediateMember implements FeeService {

    /**
     * 计算中级会员所需缴费的金额
     * @param amount 会员的交易金额
     * @return
     */
    @Override
    public Double compute(Double amount) {
        // 具体的实现根据业务需求修改
        return 6.66;
    }

    @Override
    public Integer getType() {
        return MemberEnum.INTERMEDIATE_MEMBER.getCode();
    }
}
  • 高级会员计费规则
@Component
public class SeniorMember implements FeeService {

    /**
     * 计算高级会员所需缴费的金额
     * @param amount 会员的交易金额
     * @return
     */
    @Override
    public Double compute(Double amount) {
        // 具体的实现根据业务需求修改
        return 0.01;
    }

    @Override
    public Integer getType() {
        return MemberEnum.SENIOR_MEMBER.getCode();
    }
}

4.3 别名转换

思考:程序如何通过一个标识,怎么识别解析这个标识,找到对应的策略实现类?

我的方案是:在配置文件中制定,便于维护。

  • application.yml
alias:
  aliasMap:
    first: ordinaryMember
    second: juniorMember
    third: intermediateMember
    fourth: seniorMember
  • AliasEntity.java
@Component
@EnableConfigurationProperties
@ConfigurationProperties(prefix = "alias")
public class AliasEntity {

    private HashMap aliasMap;

    public HashMap getAliasMap() {
        return aliasMap;
    }

    public void setAliasMap(HashMap aliasMap) {
        this.aliasMap = aliasMap;
    }

    /**
     * 根据描述获取该会员对应的别名
     * @param desc
     * @return
     */
    public String getEntity(String desc) {
        return aliasMap.get(desc);
    }
}

该类为了便于读取配置,因为存入的是 Mapkey-value值, key存的是描述, value是各级别会员 Bean的别名。

4.4 策略工厂

@Component
public class ServiceFeeHolder {

    /**
     * 将 Spring 中所有实现 ServiceFee 的接口类注入到这个Map中
     */
    @Resource
    private Map serviceFeeMap;

    @Resource
    private AliasEntity aliasEntity;

    /**
     * 获取该会员应当缴纳的费用
     * @param desc 会员标志
     * @param money 交易金额
     * @return
     * @throws IllegalArgumentException 会员级别输入错误
     */
    public Double getFee(String desc, Double money) {
        return getBean(desc).compute(money);
    }

    /**
     * 获取会员标志(枚举中的数字)
     * @param desc 会员标志
     * @return
     * @throws IllegalArgumentException 会员级别输入错误
     */
    public Integer getType(String desc) {
        return getBean(desc).getType();
    }

    private FeeService getBean(String type) {
        // 根据配置中的别名获取该策略的实现类
        FeeService entStrategy = serviceFeeMap.get(aliasEntity.getEntity(type));
        if (entStrategy == null) {
            // 找不到对应的策略的实现类,抛出异常
            throw new IllegalArgumentException("please input right value");
        }
        return entStrategy;
    }
}

亮点

  1. Spring中所有 ServiceFee.java 的实现类注入到 Map中,不同策略通过其不同的 key获取其实现类;
  2. 找不到对应的策略的实现类,抛出 IllegalArgumentException异常。

4.5 测试

@SpringBootTest
@RunWith(SpringRunner.class)
public class DemoTest {

    @Resource
    ServiceFeeHolder serviceFeeHolder;

    @Test
    public void test() {
         // 计算应缴纳费用
        System.out.println(serviceFeeHolder.getFee("second", 1.333));
        // 获取会员标志
        System.out.println(serviceFeeHolder.getType("second"));
        // 会员描述错误,抛 IllegalArgumentException
        System.out.println(serviceFeeHolder.getType("zero"));
    }
}
  • 执行结果
8.88
1
java.lang.IllegalArgumentException: please input right value

五、总结

两种方案主要参考了设计模式中的 策略模式,因为策略模式刚好符合本场景:

  1. 系统中有很多类,而他们的区别仅仅在于他们的行为不同。
  2. 一个系统需要动态地在几种算法中选择一种。

5.1 策略模式角色

彻底消灭if-else嵌套
  • Context: 环境类

Context叫做上下文角色,起承上启下封装作用,屏蔽高层模块对策略、算法的直接访问,封装可能存在的变化,对应本文的 ServiceFeeFactory.java

  • Strategy: 抽象策略类

定义算法的接口,对应本文的 FeeService.java

  • ConcreteStrategy: 具体策略类

实现具体策略的接口,对应本文的 OrdinaryMember.java/ JuniorMember.java/ IntermediateMember.java/ SeniorMember.java

5.2 示例代码及参考文章

  1. 非框架版
  2. Spring Boot 框架版
  3. 如何从 if-else 的参数校验中解放出来?

5.3 技术交流

  1. 风尘博客
  2. 风尘博客-掘金
  3. 风尘博客-博客园
  4. Github

Original: https://www.cnblogs.com/VanFan/p/12375475.html
Author: 风尘博客
Title: 彻底消灭if-else嵌套

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/573570/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • Nginx中proxy_pass末尾加斜杠的区别

    事实上,标题这种描不太准确,准确来说: 当proxy_pass有URI参数时,会将用户访问路径中,location匹配到的部分,替换成proxy_pass的URI部分。 当prox…

    Java 2023年5月30日
    071
  • 单例设计模式

    单例模式: 所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例。 具体的代码实现: 饿汉式: class Bank { //&#x…

    Java 2023年6月14日
    090
  • 应用配置管理,基础原理分析

    工程可以有点小乱,但配置不能含糊; 一、配置架构 在微服务的代码工程中,配置管理是一项复杂的事情,即需要做好各个环境的配置隔离措施,还需要确保生产环境的配置安全;如果划分的微服务足…

    Java 2023年6月15日
    080
  • Java基础常识

    1.eclipse Run as application 快捷键 2.关于半角和全角 1.eclipse Run as application 快捷键 记得刚学时两个手并用按住了A…

    Java 2023年5月29日
    065
  • Java反射

    用来动态的操纵Java代码 反射机制的主要作用: 在运行时分析类的能力 在运行时查看对象 实现通用的数组操作代码 利用Method对象 Class类 在Java程序运行时,系统为所…

    Java 2023年6月13日
    076
  • 20220929-ArrayList扩容机制源码分析

    示例代码 public class ArrayListSource { public static void main(String[] args) { ArrayList arr…

    Java 2023年6月15日
    056
  • 如何痛快地写一篇博客

    如何痛快地写一篇博客 当各位踏入咱们的科技领域时,一定也有想写一篇博客的想法吧。可能在博客园,也可能在自建网站。但是,写博客要用markdown,不同于word,它的图片功能全部需…

    Java 2023年6月9日
    072
  • java延时队列 示例

    /** * @desc: java 延时队列 思路:使&#x7528…

    Java 2023年5月29日
    078
  • java 生成 zip格式 压缩文件

    ackage org.fh.util; import java.io.File; import java.io.FileInputStream; import java.io.Fi…

    Java 2023年6月8日
    086
  • Java多线程之ExecutorService使用说明

    一、简介 ExecutorService是Java中对线程池定义的一个接口,它java.util.concurrent包中,在这个接口中定义了和后台任务执行相关的方法。 二、线程池…

    Java 2023年5月29日
    062
  • LeetCode.1154-一年中的第几天(Day of the Year)

    这是小川的第 410次更新,第 442篇原创 看题和准备 今天介绍的是 LeetCode算法题中 Easy级别的第 261题(顺位题号是 1154)。给定表示格式为 YYYY-MM…

    Java 2023年6月5日
    0103
  • windows系统命令行cmd查看显卡驱动版本号CUDA

    好看请赞,养成习惯:) 本文来自博客园,作者:靠谱杨, 转载请注明原文链接:https://www.cnblogs.com/rainbow-1/p/16656547.html 关于…

    Java 2023年6月15日
    082
  • base64,网上工具编码结果不一致问题探讨

    今天我们就来聊一聊base64 相信同学们肯定接触的不少关于base64的编码和解码,平时 见到base64之后的内容 大概就能看出来这是base64出来的结果. 或者平时在对接 …

    Java 2023年6月5日
    077
  • 关于ThreadLocal最直白的解释

    ThreadLocal 底层原理如下:实线是强引用,虚线是弱引用Thread 持有 ThreadLocal 对象的引用,ThreadLocalMap 是 Thread 的成员变量,…

    Java 2023年6月9日
    071
  • SpringBoot入门教程,带你快速学会使用springboot

    Spring Boot 去除了大量的 xml 配置文件,简化了复杂的依赖管理,配合各种 starter 使用,基本上可以做到自动化配置。Spring 可以做的事情,现在用 Spri…

    Java 2023年6月7日
    074
  • 周转换日期区间,SQL函数

    CREATE DEFINER=mysqladmin@% FUNCTION convertWeekToDate(reportYear INT,reportWeek INT) RETU…

    Java 2023年6月9日
    061
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球