美文网首页互联网科技老男孩的成长之路Java架构技术进阶
还在烦恼if-else?我来教你彻底消灭if-else嵌套!!

还在烦恼if-else?我来教你彻底消灭if-else嵌套!!

作者: java菲菲 | 来源:发表于2020-03-03 20:31 被阅读0次

    0、推荐阅读

    一、背景

    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 追根溯源

    • 我们来分析下代码多分支的原因
    1. 业务判断
    2. 空值判断
    3. 状态判断
    • 如何处理呢?
    1. 在有多种算法相似的情况下,利用策略模式,把业务判断消除,各子类实现同一个接口,只关注自己的实现(本文核心);
    2. 尽量把所有空值判断放在外部完成,内部传入的变量由外部接口保证不为空,从而减少空值判断(可参考如何从 if-else 的参数校验中解放出来?);
    3. 把分支状态信息预先缓存在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 项目依赖

    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
    

    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<Integer, FeeService> map;
    
        public ServiceFeeFactory() {
    
            // 该工厂管理所有的策略接口实现类
            List<FeeService> 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 项目依赖

    <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
    

    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<String, String> aliasMap;
    
        public HashMap<String, String> getAliasMap() {
            return aliasMap;
        }
    
        public void setAliasMap(HashMap<String, String> 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<String, FeeService> 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
    

    作者:Van_Fan
    原文链接:https://juejin.im/post/5e5dbe36f265da57455b4a67

    相关文章

      网友评论

        本文标题:还在烦恼if-else?我来教你彻底消灭if-else嵌套!!

        本文链接:https://www.haomeiwen.com/subject/yhlllhtx.html