1. 程式人生 > 實用技巧 >策略模式:助你消除醜陋的 if else 多分支程式碼

策略模式:助你消除醜陋的 if else 多分支程式碼

開發場景舉例

讓我們以一個實際開發場景來切入這篇文章的正題。現在,假設需要開發這樣一個需求:購物車商品結算時需要根據使用者會員等級進行打折。

我們假設使用者會員等級被分為幾個檔次:青銅、白銀、黃金、鑽石、王者,對應折扣分別為:九折、八折、七折、六折、五折。

那麼,我們很容易想到的一種實現方式,就是像下面這樣的程式碼:

/**
 * 計算使用者最終應支付的訂單總金額
 *
 * @param originalMoney 打折前使用者應支付的訂單總金額
 * @param userVipLevel 使用者會員等級
 *
 * @return 使用者最終應支付的訂單總金額
 */
public BigDecimal needPay(BigDecimal originalMoney, UserVipLevelEnum userVipLevel) {
    if (userVipLevel == UserVipLevelEnum.BRONZE) {
        return originalMoney.multiply(BigDecimal.valueOf(0.9));
    } else if (userVipLevel == UserVipLevelEnum.SILVER) {
        return originalMoney.multiply(BigDecimal.valueOf(0.8));
    } else if (userVipLevel == UserVipLevelEnum.GOLD) {
        return originalMoney.multiply(BigDecimal.valueOf(0.7));
    } else if (userVipLevel == UserVipLevelEnum.DIAMOND) {
        return originalMoney.multiply(BigDecimal.valueOf(0.6));
    } else if (userVipLevel == UserVipLevelEnum.SUPER_VIP) {
        return originalMoney.multiply(BigDecimal.valueOf(0.5));
    }
    return originalMoney;
}

這段程式碼乍一看沒有什麼問題,也可以滿足需求。但是我們不妨稍微思考一下,此刻這段程式碼看起來比較簡單,是因為需求本身很簡單,只是需要一個打折的運算。如果後期的需求繼續增加,需要我們根據使用者會員等級的不同做更多的區別性操作,那麼這個 needPay 方法可能會很快變得臃腫;再比如,後期的使用者會員又多了一些其它的等級,那麼 if else 的分支也將隨之增多,程式碼閱讀起來也會讓人很眼花。

初步優化思路

針對可能會增加的後期需求,假如需要我們根據使用者會員等級的不同做更多的區別性操作,也許可以考慮把各個操作按使用者會員等級抽取成不同的方法,就像下面的程式碼這樣:

public BigDecimal needPay(BigDecimal originalMoney, UserVipLevelEnum userVipLevel) {
    if (userVipLevel == UserVipLevelEnum.BRONZE) {
        return bronzeUserNeedPay(originalMoney);
    } else if (userVipLevel == UserVipLevelEnum.SILVER) {
        return silverUserNeedPay(originalMoney);
    } else if (userVipLevel == UserVipLevelEnum.GOLD) {
        return goldUserNeedPay(originalMoney);
    } else if (userVipLevel == UserVipLevelEnum.DIAMOND) {
        return diamondUserNeedPay(originalMoney);
    } else if (userVipLevel == UserVipLevelEnum.SUPER_VIP) {
        return superVipUserNeedPay(originalMoney);
    }
    return originalMoney;
}

這算是一個比較簡單的優化思路,也比較容易想到,它從一定程度上解決了 needPay 方法可能會臃腫的問題,但是很明顯,這段程式碼還是沒有解決 if else 分支過多的問題。那麼我們在這裡再進一步思考一下,其實可以使用設計模式當中的策略模式來解決 if else 分支過多的問題。

使用策略模式

策略模式的概念我這裡就不過多描述了,這篇文章主要以程式碼為切入點,力求理解起來更加直觀。

搭建策略模式框架

使用策略模式需要我們先定義一個 Java 介面,這個介面用來描述某種要實現的策略,比如對應本文舉的例子就是使用者支付策略。除此之外,還需要定義一個該介面需要規範的統一行為,即此處的使用者支付行為

。介面的定義大致像下面的程式碼這樣:

/**
 * 使用者支付策略統一介面規範
 */
public interface UserPaymentStrategy {
    /**
     * 計算使用者最終應支付的訂單總金額
     *
     * @param originalMoney 使用者應支付的原始訂單總金額
     *
     * @return 使用者最終應支付的訂單總金額
     */
    BigDecimal needPay(BigDecimal originalMoney);
}

有了介面定義,自然就應該有對應的介面實現,而實現這個策略介面的過程,其實就是在定義每一種不同策略的實現方式,比如此處我們以青銅、白銀、黃金會員為例,分別實現這三類使用者的具體支付策略,外加一種預設的使用者支付策略。程式碼大致像下面這樣:

/**
 * 青銅使用者支付策略
 */
public class BronzeUserPaymentStrategy implements UserPaymentStrategy {
    @Override
    public BigDecimal needPay(BigDecimal originalMoney) {
        return originalMoney.multiply(BigDecimal.valueOf(0.9));
    }
}
/**
 * 白銀使用者支付策略
 */
public class SilverUserPaymentStrategy implements UserPaymentStrategy {
    @Override
    public BigDecimal needPay(BigDecimal originalMoney) {
        return originalMoney.multiply(BigDecimal.valueOf(0.8));
    }
}
/**
 * 黃金使用者支付策略
 */
public class GoldUserPaymentStrategy implements UserPaymentStrategy {
    @Override
    public BigDecimal needPay(BigDecimal originalMoney) {
        return originalMoney.multiply(BigDecimal.valueOf(0.7));
    }
}
/**
 * 預設的使用者支付策略
 */
public class DefaultUserPaymentStrategy implements UserPaymentStrategy {
    @Override
    public BigDecimal needPay(BigDecimal originalMoney) {
        return originalMoney;
    }
}

策略模式簡單呼叫

有了具體的策略實現,那麼實際使用時的程式碼該怎麼寫呢?我們先來看一種簡單直接的使用方式,直接在業務程式碼中做策略選擇:

public BigDecimal needPay(BigDecimal originalMoney, UserVipLevelEnum userVipLevel) {
    UserPaymentStrategy userPaymentStrategy = new DefaultUserPaymentStrategy();

    if (userVipLevel == UserVipLevelEnum.BRONZE) {
        userPaymentStrategy = new BronzeUserPaymentStrategy();
    } else if (userVipLevel == UserVipLevelEnum.SILVER) {
        userPaymentStrategy = new SilverUserPaymentStrategy();
    } else if (userVipLevel == UserVipLevelEnum.GOLD) {
        userPaymentStrategy = new GoldUserPaymentStrategy();
    }

    return userPaymentStrategy.needPay(originalMoney);
}

更優雅的呼叫方式

上述的簡單直接呼叫策略的方式,看起來已經使用到了策略,但實際上並沒有消除掉 if else 的多分支程式碼。在實際呼叫策略模式的過程中,我們其實還需要結合策略工廠來封裝策略的選擇過程,以隱藏 if else 分支細節。這裡所說的策略工廠,其實就是實現一個選擇策略的簡單工廠模式

/**
 * 使用者支付策略工廠
 */
public class UserPaymentStrategyFactory {
    /**
     * 根據使用者會員等級選擇合適的使用者支付策略
     * 
     * @param userVipLevel 使用者會員等級
     *                     
     * @return 對應的使用者支付策略
     */
    public static UserPaymentStrategy getUserPaymentStrategy(UserVipLevelEnum userVipLevel) {
        if (userVipLevel == UserVipLevelEnum.BRONZE) {
            return new BronzeUserPaymentStrategy();
        } else if (userVipLevel == UserVipLevelEnum.SILVER) {
            return new SilverUserPaymentStrategy();
        } else if (userVipLevel == UserVipLevelEnum.GOLD) {
            return new GoldUserPaymentStrategy();
        }
        return new DefaultUserPaymentStrategy();
    }
}

然後業務程式碼在具體呼叫策略程式碼時就可以像下面這樣使用:

public BigDecimal needPay(BigDecimal originalMoney, UserVipLevelEnum userVipLevel) {
    return UserPaymentStrategyFactory.getUserPaymentStrategy(userVipLevel).needPay(originalMoney);
}

這樣一來,對於最初的業務方法 needPay 就已經隱藏了很多實現細節,業務層程式碼看起來會更乾淨優雅,想要修改某種使用者的支付策略,只需要到對應使用者的支付策略類中修改對應的實現,而不用擔心其它的策略會怎麼樣。

策略模式優化

徹底消除 if else 分支

上面的程式碼已經實現了一個基本的策略模式,但是從更加嚴格的角度來講,在策略選擇工廠裡面,其實還是存在著 if else 分支程式碼。所以我們能否想一個方法來優化下這段程式碼呢?那麼,要想不按照使用者會員等級來做 if 判斷,就得提前知道使用者會員等級和使用者支付策略的對應關係,一一對應?可以考慮用 Map<UserVipLevelEnum, UserPaymentStrategy> 來解決,具體程式碼大致像下面這樣:

/**
 * 使用者支付策略工廠
 */
public class UserPaymentStrategyFactory {
    /**
     * 儲存使用者會員等級和使用者支付策略的對應關係
     */
    private static Map<UserVipLevelEnum, UserPaymentStrategy> userPaymentStrategyMap;

    // userPaymentStrategyMap 靜態初始化
    static {
        userPaymentStrategyMap = new HashMap<>(UserVipLevelEnum.values().length);
        userPaymentStrategyMap.put(UserVipLevelEnum.DEFAULT, new DefaultUserPaymentStrategy());
        userPaymentStrategyMap.put(UserVipLevelEnum.BRONZE, new BronzeUserPaymentStrategy());
        userPaymentStrategyMap.put(UserVipLevelEnum.SILVER, new SilverUserPaymentStrategy());
        userPaymentStrategyMap.put(UserVipLevelEnum.GOLD, new GoldUserPaymentStrategy());
    }
    
    /**
     * 根據使用者會員等級選擇合適的使用者支付策略
     *
     * @param userVipLevel 使用者會員等級
     *
     * @return 對應的使用者支付策略
     */
    public static UserPaymentStrategy getUserPaymentStrategy(UserVipLevelEnum userVipLevel) {
        return userPaymentStrategyMap.get(userVipLevel);
    }
}

實現策略的自動註冊

上面的程式碼已經藉助 Map 消除了之前大段的 if else 分支程式碼,但是細想一下,還會發現一個小問題,就是當我們需要新增一種支付策略的時候,必須得進入策略工廠來修改現有工廠類的程式碼。那麼,能不能做到新增策略但不需要修改策略工廠類的程式碼呢?答案是可以的。

怎麼做呢?這裡提出一種策略註冊的思想,大致的思路如下:

  1. 先由策略工廠類提供一個方法,該方法用於進行策略註冊,每一種具體的策略實現都必須呼叫該方法將自己註冊進策略工廠,即放進 Map 中;
  2. 同時,在最初的支付策略介面 UserPaymentStrategy 中新增一種行為,就是將策略物件自身註冊到策略工廠的方法 register()
  3. 在策略工廠類中再提供一個靜態方法,通過反射獲取到 UserPaymentStrategy 介面的所有實現類,並依次呼叫它們的 register() 方法;
  4. 最後,在策略工廠類的初始化靜態程式碼塊中呼叫自動註冊所有策略的方法,完成所有支付策略的物件註冊。

這樣一來,就可以達到新增支付策略時並不需要修改策略工廠類的目的。上述步驟中所提到的程式碼大致像下面這樣:

public class UserPaymentStrategyFactory {
    /**
     * 儲存使用者會員等級和使用者支付策略的對應關係
     */
    private static Map<UserVipLevelEnum, UserPaymentStrategy> userPaymentStrategyMap;

    // userPaymentStrategyMap 靜態初始化
    static {
        userPaymentStrategyMap = new HashMap<>(UserVipLevelEnum.values().length);
        autoRegisterAllPaymentStrategies();
    }

    /**
     * 註冊具體支付策略到策略工廠
     * 
     * @param userVipLevel 使用者會員等級
     * @param paymentStrategy 具體支付策略物件
     */
    public static void registerPaymentStrategy(UserVipLevelEnum userVipLevel, UserPaymentStrategy paymentStrategy) {
        userPaymentStrategyMap.put(userVipLevel, paymentStrategy);
    }

    /**
     * 自動註冊所有的支付策略
     */
    public static void autoRegisterAllPaymentStrategies() {
        // 此處用到了 java.util.ServiceLoader 類來獲取 UserPaymentStrategy 介面的所有實現類
        // 該類的具體使用方式可以參考網路上其它相關的資源,這裡不再贅述
        ServiceLoader.load(UserPaymentStrategy.class).forEach(UserPaymentStrategy::register);
    }
}

注意:上述程式碼中用到了 java.util.ServiceLoader 這個類來獲取 UserPaymentStrategy 介面的所有實現類,該類的具體使用方式可以參考網路上其它相關的資源,這裡不再贅述。當然,如果專案是依託於 Spring 開發框架,那麼可以利用 Spring 的容器來獲取所有的實現類。

支付策略介面 UserPaymentStrategy 所需要做出的改動如下,新增 register() 方法:

public interface UserPaymentStrategy {

    BigDecimal needPay(BigDecimal originalMoney);

    /**
     * 策略將自身物件註冊到策略工廠
     */
    void register();
}

青銅使用者支付策略實現類為例,將它自己的例項物件註冊到策略工廠當中,大致的程式碼像下面這樣:

public class BronzeUserPaymentStrategy implements UserPaymentStrategy {
    @Override
    public BigDecimal needPay(BigDecimal originalMoney) {
        return originalMoney.multiply(BigDecimal.valueOf(0.9));
    }

    @Override
    public void register() {
        UserPaymentStrategyFactory.registerPaymentStrategy(UserVipLevelEnum.BRONZE, this);
    }
}

使用註解進一步優化

上面的程式碼看似已經比較完美了,既消除了 if else 又保護了策略工廠類,但是仔細看彷彿又引入了新的問題:比如策略實現類中的 register() 方法,其實相似性是非常高的。在我們編碼的過程當中,相似性非常高的程式碼往往是一種警示,提示你需要對它們做更進一步的抽象。那麼,上面的程式碼還能再進行怎樣的優化呢?我們可以考慮使用註解來進一步降低程式碼的耦合度,達到更進一步的優化。

先建立一個註解類,用於標識使用者會員等級:

/**
 * 使用者會員等級註解
 */
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface UserVipLevel {
    UserVipLevelEnum value() default UserVipLevelEnum.DEFAULT;
}

接著,可以刪掉 UserPaymentStrategy 介面及其實現類當中的 register() 方法了,取而代之的是,在各個策略實現類上加上上面這個使用者會員等級註解,以青銅使用者支付策略的實現類為例:

@UserVipLevel(UserVipLevelEnum.BRONZE)
public class BronzeUserPaymentStrategy implements UserPaymentStrategy {
    @Override
    public BigDecimal needPay(BigDecimal originalMoney) {
        return originalMoney.multiply(BigDecimal.valueOf(0.9));
    }
}

最後,需要對策略工廠類的自動註冊所有策略方法做一點修改,具體的實現可以參考最終修改好的策略工廠程式碼:

public class UserPaymentStrategyFactory {
    /**
     * 儲存使用者會員等級和使用者支付策略的對應關係
     */
    private static Map<UserVipLevelEnum, UserPaymentStrategy> userPaymentStrategyMap;

    // 自動註冊所有的支付策略
    static {
        userPaymentStrategyMap = new HashMap<>(UserVipLevelEnum.values().length);
        autoRegisterAllPaymentStrategies();
    }

    // 之前這裡的 registerPaymentStrategy() 方法也已經不再需要了

    /**
     * 自動註冊所有的支付策略
     */
    public static void autoRegisterAllPaymentStrategies() {
        // 此處用到了 java.util.ServiceLoader 類來獲取 UserPaymentStrategy 介面的所有實現類
        // 該類的具體使用方式可以參考網路上其它相關的資源,這裡不再贅述
        ServiceLoader.load(UserPaymentStrategy.class).forEach(paymentStrategy -> {
            // 獲取使用者會員等級註解
            UserVipLevel userVipLevel = paymentStrategy.getClass().getAnnotation(UserVipLevel.class);
            userPaymentStrategyMap.put(userVipLevel.value(), paymentStrategy);
        });
    }

    /**
     * 根據使用者會員等級選擇合適的使用者支付策略
     *
     * @param userVipLevel 使用者會員等級
     *
     * @return 對應的使用者支付策略
     */
    public static UserPaymentStrategy getUserPaymentStrategy(UserVipLevelEnum userVipLevel) {
        return userPaymentStrategyMap.get(userVipLevel);
    }
}

至此,整個策略模式的實現程式碼又少了一點,也更優雅了一點,再新增新的支付策略還是無需修改策略工廠。而且你或許也注意到了,這整個過程中也都沒有再修改過業務程式碼對策略模式的呼叫過程,還是當初那一行最簡潔的呼叫方式:

public BigDecimal needPay(BigDecimal originalMoney, UserVipLevelEnum userVipLevel) {
    return UserPaymentStrategyFactory.getUserPaymentStrategy(userVipLevel).needPay(originalMoney);
}

總結

  1. 必要時使用 Map 建立物件對映關係,可以避免 if else 多分支操作。
  2. 想通過介面呼叫到所有實現類的某個方法,可以考慮是否能在相關程式碼中使用反射技術。
  3. 能用到反射的地方就可以用到註解,二者結合,往往可以達到更好的降低程式碼耦合效果。

以上全部程式碼均已 push 到我個人的程式碼倉庫,歡迎點選查閱