zoukankan      html  css  js  c++  java
  • 彻底消灭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 追根溯源

    • 我们来分析下代码多分支的原因
    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
    

    五、总结

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

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

    5.1 策略模式角色

    风尘博客

    • 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
  • 相关阅读:
    hdu 5723 Abandoned country 最小生成树 期望
    OpenJ_POJ C16G Challenge Your Template 迪杰斯特拉
    OpenJ_POJ C16D Extracurricular Sports 打表找规律
    OpenJ_POJ C16B Robot Game 打表找规律
    CCCC 成都信息工程大学游记
    UVALive 6893 The Big Painting hash
    UVALive 6889 City Park 并查集
    UVALive 6888 Ricochet Robots bfs
    UVALive 6886 Golf Bot FFT
    UVALive 6885 Flowery Trails 最短路
  • 原文地址:https://www.cnblogs.com/vandusty/p/12375475.html
Copyright © 2011-2022 走看看