徹底消滅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.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 容器管理
  • 普通會員計費規則

@Componentpublic 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();    }}

  • 初級會員計費規則

@Componentpublic 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();    }}

  • 中級會員計費規則

@Componentpublic 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();    }}

  • 高級會員計費規則

@Componentpublic 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 策略工廠@Componentpublic 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 策略模式角色

 
170878d0ec4647c0?w=764&h=329&f=jpeg&s=27512uploading.4e448015.gif轉存失敗重新上傳取消
  • Context: 環境類
Context叫做上下文角色,起承上啓下封裝作用,屏蔽高層模塊對策略、算法的直接訪問,封裝可能存在的變化,對應本文的ServiceFeeFactory.java。
  • Strategy: 抽象策略類
定義算法的接口,對應本文的FeeService.java。
  • ConcreteStrategy: 具體策略類
實現具體策略的接口,對應本文的OrdinaryMember.java/JuniorMember.java/IntermediateMember.java/SeniorMember.java。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章