彻底消灭if-else嵌套

260 阅读8分钟

一、背景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 追根溯源

  • 我们来分析下代码多分支的原因
  • 业务判断
  • 空值判断
  • 状态判断
  • 如何处理呢?
  • 在有多种算法相似的情况下,利用策略模式,把业务判断消除,各子类实现同一个接口,只关注自己的实现(本文核心);
  • 尽量把所有空值判断放在外部完成,内部传入的变量由外部接口保证不为空,从而减少空值判断(可参考);
  • 把分支状态信息预先缓存在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 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():获取枚举中维护的会员级别
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.88java.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.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。