if-else嵌套的替代簡化兩種實現方式

一、背景

1.1 反面教材

不知大家有沒遇到過像橫放著的金字塔一樣的if-else嵌套:

<code>if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {

}
}
}
}
}
}/<code>

if-else作為每種編程語言都不可或缺的條件語句,我們在編程時會大量的用到。

但if-else一般不建議嵌套超過三層,如果一段代碼存在過多的if-else嵌套,代碼的可讀性就會急速下降,後期維護難度也大大提高。

2.2 親歷的重構

前陣子重構了服務費收費規則,重構前的if-else嵌套如下。

<code>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");
}/<code>

我們都寫過類似的代碼,回想起被 if-else 支配的恐懼,如果有新需求:新增計費規則或者修改既定計費規則,無所下手。

2.3 追根溯源

  • 我們來分析下代碼多分支的原因
  1. 業務判斷
  2. 空值判斷
  3. 狀態判斷
  • 如何處理呢?
  1. 在有多種算法相似的情況下,利用策略模式,把業務判斷消除,各子類實現同一個接口,只關注自己的實現(本文核心);
  2. 儘量把所有空值判斷放在外部完成,內部傳入的變量由外部接口保證不為空,從而減少空值判斷(可參考如何從 if-else 的參數校驗中解放出來?);
  3. 把分支狀態信息預先緩存在Map裡,直接get獲取具體值,消除分支(本文也有體現)。
  • 來看看簡化後的業務調用
<code>CalculationUtil.getFee(type, amount)/<code>

或者

<code>serviceFeeHolder.getFee(type, amount)/<code>

是不是超級簡單,下面介紹兩種實現方式(文末附示例代碼)。

二、通用部分

2.1 需求概括

我們擁有很多公司會員,暫且分為普通會員、初級會員、中級會員和高級會員,會員級別不同計費規則不同。該模塊負責計算會員所需的繳納的服務費。

2.2 會員枚舉

用於維護會員類型。

<code>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;
}

}/<code>

2.3 定義一個策略接口

該接口包含兩個方法:

  1. compute(Double amount):各計費規則的抽象
  2. getType():獲取枚舉中維護的會員級別
<code>public interface FeeService {

/**
* 計費規則
* @param amount 會員的交易金額
* @return
*/
Double compute(Double amount);

/**
* 獲取會員級別
* @return
*/
Integer getType();
}/<code>

三、非框架實現

3.1 項目依賴

<code><dependency>
<groupid>junit/<groupid>
<artifactid>junit/<artifactid>
<version>4.12/<version>
<scope>test/<scope>
/<dependency>/<code>

3.2 不同計費規則的實現

這裡四個子類實現了策略接口,其中 compute()方法實現各個級別會員的計費邏輯,getType()指定了該類所屬的會員級別。

  • 普通會員計費規則
<code>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();
}
}/<code>
  • 初級會員計費規則
<code>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();
}
}/<code>
  • 中級會員計費規則
<code>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();
}
}/<code>
  • 高級會員計費規則
<code>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();
}
}/<code>

3.3 核心工廠

創建一個工廠類ServiceFeeFactory.java,該工廠類管理所有的策略接口實現類。具體見代碼註釋。

<code>public class ServiceFeeFactory {

private Map<integer> 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);

}
}/<feeservice>/<integer>/<code>

3.4 工具類

新建通過一個工具類管理計費規則的調用,並對不符合規則的公司級別輸入拋IllegalArgumentException。

<code>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);
}
}/<code>

核心是通過Map的get()方法,根據傳入 type,即可獲取到對應會員類型計費規則的實現,從而減少了if-else的業務判斷。

3.5 測試

<code>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);
}
}/<code>
  • 執行結果
<code>8.88
java.lang.IllegalArgumentException: please input right value/<code>

四、Spring Boot 實現

上述方法無非是藉助策略模式+工廠模式+單例模式實現,但是實際場景中,我們都已經集成了Spring Boot,這一段就看一下如何藉助Spring Boot更簡單實現本次的優化。

4.1 項目依賴

<code><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>/<code>

4.2 不同計費規則的實現

這部分是與上面區別在於:把策略的實現類得是交給Spring 容器管理

  • 普通會員計費規則
<code>@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();
}
}/<code>
  • 初級會員計費規則
<code>@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();
}
}/<code>
  • 中級會員計費規則
<code>@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();
}
}/<code>
  • 高級會員計費規則
<code>@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();
}
}/<code>

4.3 別名轉換

思考:程序如何通過一個標識,怎麼識別解析這個標識,找到對應的策略實現類?

我的方案是:在配置文件中制定,便於維護。

  • application.yml
<code>alias:
aliasMap:
first: ordinaryMember
second: juniorMember
third: intermediateMember
fourth: seniorMember/<code>
  • AliasEntity.java
<code>@Component
@EnableConfigurationProperties
@ConfigurationProperties(prefix = "alias")
public class AliasEntity {

private HashMap<string> aliasMap;

public HashMap<string> getAliasMap() {
return aliasMap;
}


public void setAliasMap(HashMap<string> aliasMap) {
this.aliasMap = aliasMap;
}

/**
* 根據描述獲取該會員對應的別名
* @param desc
* @return
*/
public String getEntity(String desc) {
return aliasMap.get(desc);
}
}/<string>/<string>/<string>/<code>

該類為了便於讀取配置,因為存入的是Map的key-value值,key存的是描述,value是各級別會員Bean的別名。

4.4 策略工廠

<code>@Component
public class ServiceFeeHolder {

/**
* 將 Spring 中所有實現 ServiceFee 的接口類注入到這個Map中
*/
@Resource
private Map<string> 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;
}
}/<string>/<code>

亮點

  1. 將 Spring中所有 ServiceFee.java 的實現類注入到Map中,不同策略通過其不同的key獲取其實現類;
  2. 找不到對應的策略的實現類,拋出IllegalArgumentException異常。

4.5 測試

<code>@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"));
}
}/<code>
  • 執行結果
<code>8.88
1
java.lang.IllegalArgumentException: please input right value/<code>

五、總結

兩種方案主要參考了設計模式中的策略模式,因為策略模式剛好符合本場景:

  1. 系統中有很多類,而他們的區別僅僅在於他們的行為不同。
  2. 一個系統需要動態地在幾種算法中選擇一種。

5.1 策略模式角色


if-else嵌套的替代簡化兩種實現方式


  • Context: 環境類

Context叫做上下文角色,起承上啟下封裝作用,屏蔽高層模塊對策略、算法的直接訪問,封裝可能存在的變化,對應本文的ServiceFeeFactory.java。

  • Strategy: 抽象策略類

定義算法的接口,對應本文的FeeService.java。

  • ConcreteStrategy: 具體策略類

實現具體策略的接口,對應本文的OrdinaryMember.java/JuniorMember.java/IntermediateMember.java/SeniorMember.java。

鏈接:https://juejin.im/post/5e5dbe36f265da57455b4a67

關注我瞭解更多程序員資訊技術,領取豐富架構資料


分享到:


相關文章: