徹底消滅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 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.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
。
5.2 示例程式碼及參考文章
- 非框架版
- Spring Boot 框架版
- 如何從 if-else 的引數校驗中解放出來?
5.3 技術交流
- 風塵部落格
- 風塵部落格-掘金
- 風塵部落格-部落格園
- Github