彻底消灭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 定义一个策略接口
该接口包含两个方法:
compute(Double amount)
:各计费规则的抽象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);
}
}
核心是通过Map
的get()
方法,根据传入 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);
}
}
该类为了便于读取配置,因为存入的是
Map
的key-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;
}
}
亮点:
- 将
Spring
中所有ServiceFee.java
的实现类注入到Map
中,不同策略通过其不同的key
获取其实现类; - 找不到对应的策略的实现类,抛出
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
五、总结
两种方案主要参考了设计模式中的策略模式,因为策略模式刚好符合本场景:
- 系统中有很多类,而他们的区别仅仅在于他们的行为不同。
- 一个系统需要动态地在几种算法中选择一种。
5.1 策略模式角色
Context
: 环境类
Context
叫做上下文角色,起承上启下封装作用,屏蔽高层模块对策略、算法的直接访问,封装可能存在的变化,对应本文的ServiceFeeFactory.java
。
Strategy
: 抽象策略类
定义算法的接口,对应本文的
FeeService.java
。
ConcreteStrategy
: 具体策略类
实现具体策略的接口,对应本文的
OrdinaryMember.java
/JuniorMember.java
/IntermediateMember.java
/SeniorMember.java
。