查看: 529|回复: 0

彻底消灭if-else嵌套

[复制链接]
发表于 2020-3-7 01:56:16 | 显示全部楼层 |阅读模式
一、配景

1.1 反面课本

不知大家有没遇到过像横放着的金字塔一样的if-else嵌套:
  1. if (true) {    if (true) {        if (true) {            if (true) {                if (true) {                    if (true) {                                            }                }            }        }    }}
复制代码
if-else作为每种编程语言都不可或缺的条件语句,我们在编程时会大量的用到。
但if-else一般不建议嵌套凌驾三层,如果一段代码存在过多的if-else嵌套,代码的可读性就会急速下降,后期维护难度也大大进步。
2.2 亲历的重构

前阵子重构了服务费收费规则,重构前的if-else嵌套如下。
  1. 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获取具体值,消除分支(本文也有体现)。


  • 来看看简化后的业务调用
  1. CalculationUtil.getFee(type, amount)
复制代码
大概
  1. serviceFeeHolder.getFee(type, amount)
复制代码
是不是超级简单,下面先容两种实现方式(文末附示例代码)。
二、通用部门

2.1 需求概括

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

用于维护会员范例。
  1. 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 setDesc(int code) {        this.code = code;    }    public String getDesc() {        return desc;    }    public void setDesc(String desc) {        this.desc = desc;    }}
复制代码
2.3 定义一个策略接口

该接口包含两个方法:

  • compute(Double amount):各计费规则的抽象
  • getType():获取罗列中维护的会员级别
  1. public interface FeeService {    /**     * 计费规则     * @param amount 会员的生意业务金额     * @return     */    Double compute(Double amount);    /**     * 获取会员级别     * @return     */    Integer getType();}
复制代码
三、非框架实现

3.1 项目依赖
  1.     junit    junit    4.12    test
复制代码
3.2 不同计费规则的实现

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

  • 普通会员计费规则
  1. 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();    }}
复制代码

  • 初级会员计费规则
  1. 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();    }}
复制代码

  • 中级会员计费规则
  1. 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();    }}
复制代码

  • 高级会员计费规则
  1. 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,该工厂类管理所有的策略接口实现类。具体见代码注释。
  1. 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。
  1. 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);    }}
复制代码
焦点是通过Map的get()方法,根据传入 type,即可获取到对应会员范例计费规则的实现,从而减少了if-else的业务判断。
3.5 测试
  1. 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);    }}
复制代码

  • 实行结果
  1. 8.88java.lang.IllegalArgumentException: please input right value
复制代码
四、Spring Boot 实现

上述方法无非是借助策略模式+工厂模式+单例模式实现,但是现实场景中,我们都已经集成了Spring Boot,这一段就看一下怎样借助Spring Boot更简单实现本次的优化
4.1 项目依赖
  1.         org.springframework.boot        spring-boot-starter-test                org.springframework.boot        spring-boot-configuration-processor        true   
复制代码
4.2 不同计费规则的实现

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

  • 普通会员计费规则
  1. @Componentpublic 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();    }}
复制代码

  • 初级会员计费规则
  1. @Componentpublic 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();    }}
复制代码

  • 中级会员计费规则
  1. @Componentpublic 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();    }}
复制代码

  • 高级会员计费规则
  1. @Componentpublic 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
  1. alias:  aliasMap:    first: ordinaryMember    second: juniorMember    third: intermediateMember    fourth: seniorMember
复制代码

  • AliasEntity.java
  1. @[email protected]@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);    }}
复制代码
该类为了便于读取配置,因为存入的是Map的key-value值,key存的是描述,value是各级别会员Bean的别名。
4.4 策略工厂
  1. @Componentpublic 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;    }}
复制代码
亮点

  • 将 Spring中所有 ServiceFee.java 的实现类注入到Map中,不同策略通过其不同的key获取实在现类;
  • 找不到对应的策略的实现类,抛出IllegalArgumentException非常。
4.5 测试
  1. @[email protected](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"));    }}
复制代码

  • 实行结果
  1. 8.881java.lang.IllegalArgumentException: please input right value
复制代码
五、总结

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

  • 系统中有许多类,而他们的区别仅仅在于他们的行为不同。
  • 一个系统需要动态地在几种算法中选择一种。
5.1 策略模式角色



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


  • Strategy: 抽象策略类
定义算法的接口,对应本文的FeeService.java。


  • ConcreteStrategy: 具体策略类
实现具体策略的接口,对应本文的OrdinaryMember.java/JuniorMember.java/IntermediateMember.java/SeniorMember.java。
5.2 示例代码及参考文章

5.3 技术交流

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?用户注册

x

天涯海角也要找到Ni:彻底消灭if-else嵌套

中发现Ni: 彻底消灭if-else嵌套
中发现Ni: 彻底消灭if-else嵌套
中发现Ni: 彻底消灭if-else嵌套
中发现Ni: 彻底消灭if-else嵌套
中发现Ni: 彻底消灭if-else嵌套
中发现Ni: 彻底消灭if-else嵌套
相关技术服务需求,请联系管理员和客服QQ:2753533861或QQ:619920289
您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

帖子推荐:
客服咨询

QQ:2753533861

服务时间 9:00-22:00

快速回复 返回顶部 返回列表