一、背景

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

彻底消灭if-else嵌套的更多相关文章

  1. (1)消灭初级程序员常用的多层if-else嵌套--------------【表驱动法】

    表驱动法 1.相信很多刚从事工作的程序员或一些初级程序员在写代码的时候会出现对一些逻辑判断写成多层if-else嵌套的经历,这种方式在一些简单的层次中运用起来确实可行,但对于一些大型项目逻辑判断比较多 ...

  2. angular2系列教程(十一)路由嵌套、路由生命周期、matrix URL notation

    今天我们要讲的是ng2的路由的第二部分,包括路由嵌套.路由生命周期等知识点. 例子 例子仍然是上节课的例子:

  3. CRL快速开发框架系列教程十三(嵌套查询)

    本系列目录 CRL快速开发框架系列教程一(Code First数据表不需再关心) CRL快速开发框架系列教程二(基于Lambda表达式查询) CRL快速开发框架系列教程三(更新数据) CRL快速开发框 ...

  4. ScrollView嵌套ListView,GridView数据加载不全问题的解决

    我们大家都知道ListView,GridView加载数据项,如果数据项过多时,就会显示滚动条.ScrollView组件里面只能包含一个组件,当ScrollView里面嵌套listView,GridVi ...

  5. Android 中关于Fragment嵌套Fragment的问题

    转载请注明出处:http://www.cnblogs.com/Joanna-Yan/p/5802146.html 问题描述: 在项目中Activity A中嵌套Fragment B,Fragment ...

  6. C#语句2——循环语句(for循环与for循环嵌套)

    循环:反复执行某段代码. 循环四要素:初始条件,循环条件,循环体,状态改变. for(初始条件;循环条件;状态改变) { 循环体 } break ——中断循环,跳出整个循环 continue——停止本 ...

  7. C#用链式方法表达循环嵌套

    情节故事得有情节,不喜欢情节的朋友可看第1版代码,然后直接跳至“三.想要链式写法” 一.起缘 故事缘于一位朋友的一道题: 朋友四人玩LOL游戏.第一局,分别选择位置:中单,上单,ADC,辅助:第二局新 ...

  8. SQL连接操作符介绍(循环嵌套, 哈希匹配和合并连接)

    今天我将介绍在SQLServer 中的三种连接操作符类型,分别是:循环嵌套.哈希匹配和合并连接.主要对这三种连接的不同.复杂度用范例的形式一一介绍. 本文中使用了示例数据库AdventureWorks ...

  9. 关于对For循环嵌套优化的问题

    1.案例描述 由于一次Java面试的笔试题,当时没有写出很好的解决方案,特此专门撰写一篇博客来加以记录,方便日后的查看 面试题目如下:从性能上优化如下代码并说明优化理由? for (int i = 0 ...

随机推荐

  1. [CTS2019]无处安放(提交答案)

    由于蒟蒻太菜没报上CTS,只能在家打VP. 感觉这题挺有意思的,5h中有3h在玩这题,获得74分的“好”成绩. 说说我的做法吧: subtask1~3:手玩,不知道为什么sub2我只能玩9分,但9和1 ...

  2. Oracle不同版本中序列的注意点

    <span style="font-size:14px;">create table manager ( userid NUMBER(10), username VAR ...

  3. 监控 Linux 服务器活动的几个命令(watch top ac)

    watch.top 和 ac 命令为我们监视 Linux 服务器上的活动提供了一些十分高效的途径. 为了在获取系统活动时更加轻松,Linux 系统提供了一系列相关的命令.在这篇文章中,我们就一起来看看 ...

  4. 吴裕雄--天生自然C语言开发:文件读写

    #include <stdio.h> int main() { FILE *fp = NULL; fp = fopen("/tmp/test.txt", "w ...

  5. ACM-ICPC Nanjing Onsite 2018 I. Magic Potion

    题意:类似二分图匹配给的题目,不过这次在这里给出了k,表示没人可以再多一次匹配机会,这次匹配不能用上一次被匹配的对象 分析:不能用匈牙利做俩次匹配,因为俩次的最大匹配并不等价于总和的匹配,事实证明,你 ...

  6. 关于vyos 防火墙配置

    VyOS是一个基于Debian的网络操作系统,是Vyatta的社区fork.Vyatta是博通的企业级的产品,通过这套系统,能在x86平台提供路由,防火墙和×××的功能. 这个系统提供了和其他诸如Ci ...

  7. 项目中docker swarm实践

    docker swarm 集群服务通信 前置要求 服务需要在同一个docker swarm集群中 服务需要处于同一个overlay网络上 服务需要暴露容器端口 有2个以上服务名不同的服务 服务部署流程 ...

  8. matplotlib.pyplot.contour 简单等高线绘制

    contour(X, Y, Z) X,Y是与Z形状相同的二维数组,可以通过 numpy.meshgrid()创建. numpy.meshgrid()----从坐标向量返回坐标矩阵 生成的x,y坐标矩阵 ...

  9. highcharts 柱状图在柱子顶部显示y轴数据

    var plotOptions={ column:{ //borderColor: "#CCCC66",//边框 shadow: true, //阴影 dataLabels:{ / ...

  10. MySQL 之全文索引

    最近在复习数据库索引部分,看到了 fulltext,也即全文索引,虽然全文索引在平时的业务中用到的不多,但是感觉它有点儿意思,所以花了点时间研究一下,特此记录. 引入概念通过数值比较.范围过滤等就可以 ...