前言:開篇先放一張大神寫的代碼,反正我看到這張圖第一反應就是佩服(畫質有點渣,不過就算是4K藍光我也看不懂裏面的邏輯)
如果在開發中寫出這樣一段代碼,那麼恭喜你,擁有鐵飯碗了,連技術總監都拿你沒辦法,你走了這代碼誰來維護?
玩笑歸玩笑,那麼到底該如何避免在寫代碼中出現大量的if-else以及控制If-else的層級數?
阿里巴巴的java技術開發手冊其實已經給出了答案,我這裏結合實際場景再深入梳理一二,同時再給出一些除此之外的替代解決方案.
先來看下業務場景:現有某收費系統,針對不同身份的用戶有不同的計費邏輯,比如用戶是普通用戶,則收取全額費用,用戶是vip用戶,則收取費用按照vip等級進行打折,等級越高,打折力度也越大.(具體的計費邏輯已被我簡化成這樣,方便理解,實際中會更復雜)
上面的場景如果用傳統的If-else來實現:
/**
* 賬單
*/
@Data
@AllArgsConstructor
public class Bill {
/**
* 賬單類型 0:普通用戶賬單,1:vip1級賬單,2:vip2級賬單...
*/
private Integer type;
/**
* 賬單總金額
*/
private BigDecimal total;
}
public class BadExample {
public void calcTotal(Bill bill){
if (Objects.equals(bill.getTotal(),0)){
//TODO 普通賬單計費邏輯
}else if (Objects.equals(bill.getTotal(),1)){
//TODO vip1級用戶賬單計費邏輯
}else if (Objects.equals(bill.getTotal(),2)){
//TODO vip2級用戶賬單計費邏輯
}else if (Objects.equals(bill.getTotal(),3)){
//TODO vip3級用戶賬單計費邏輯
}
//...
}
}
代碼中會出現大量的if-else,如果裏面的計費邏輯很複雜,每一段計費邏輯裏又包含了很多層if-else,這片代碼看着就會很亂,難於維護,這時候我們就可以先嚐試去優化這段代碼了.
下面我們一起看看青銅,白銀,黃金,鑽石...段位的碼農是如何來優化的.
青銅:
先整個枚舉類(有些甚至枚舉類都不整,直接魔法值0,1,2,3...上陣),然後用switch case代替If,爲每一個計費邏輯封裝一個方法,然後在case中調用,這樣看的確代碼優雅了一些,但效果有限,而且不符合開閉原則,每當有新的計費邏輯時,就要加進switch中,再枚舉類中也新加類型.這塊的代碼比較簡單,我就不寫了,自行腦補.
白銀:
先寫一個計費的接口:
public interface CalcService {
/**
* 處理賬單計算
*/
void handleCalc(BigDecimal total);
}
然後不同的計費邏輯用不同的實現類去實現此接口:
public class OridinaryCalcServiceImpl implements CalcService {
@Override
public void handleCalc(BigDecimal total) {
System.out.println("處理普通用戶的賬單計費:" + total);
}
}
public class Vip1CalcServiceImpl implements CalcService {
@Override
public void handleCalc(BigDecimal total) {
System.out.println("處理vip1級用戶的賬單計費:"+total);
}
}
...
然後把這些實現類統一用一個Map<Integer,CalcService>封裝起來,在調用時根據key(賬單類型Integer)來get對應的service,就能優雅的調用具體的實現類了,不錯,這樣代碼確實優雅了很多,但一旦有新的計費邏輯出現,開發人員不得不去維護這個Map,同樣違背了代碼設計的開閉原則.
黃金:
黃金的實現思路與白銀的類似,只不過黃金平時比較喜歡裝X,喜歡在代碼裏搞點高逼格的東西,利用Java8提供的Function函數式編程,來讓自己的代碼逼格看起來高一點?話不多說,先上代碼後解釋:
public class CalcFunction {
private String calcOrdinary(Bill bill) {
return "處理普通用戶的賬單計費:" + bill.getTotal();
}
private String calcVip1(Bill bill) {
return "處理vip1級用戶的賬單計費:" + bill.getTotal();
}
private String calcVip2(Bill bill) {
return "處理vip2級用戶的賬單計費:" + bill.getTotal();
}
public Function<Bill, String> getFunction(Integer type) {
Function<Bill, String> ordinary = bill -> calcOrdinary(bill);
Function<Bill, String> vip1 = bill -> calcVip1(bill);
Function<Bill, String> vip2 = bill -> calcVip2(bill);
Supplier<Map<Integer, Function<Bill, String>>> supplier = () -> {
Map<Integer, Function<Bill, String>> map = new HashMap<>(3);
map.put(BillType.ORDINARY.getType(), ordinary);
map.put(BillType.VIP1.getType(), vip1);
map.put(BillType.VIP2.getType(), vip2);
return map;
};
return supplier.get().get(type);
}
}
@Test
public void testFunctional() {
CalcFunction calcFunction = new CalcFunction();
Bill bill1 = new Bill(BillTypeEnum.ORDINARY.getType(), new BigDecimal(500));
calcFunction.getFunction(bill1.getType()).apply(bill1);
Bill bill2 = new Bill(BillTypeEnum.VIP1.getType(), new BigDecimal(400));
calcFunction.getFunction(bill2.getType()).apply(bill2);
Bill bill3 = new Bill(BillTypeEnum.VIP2.getType(), new BigDecimal(300));
calcFunction.getFunction(bill3.getType()).apply(bill3);
}
具體實現思路就是,先按照不同計費規則封裝具體的計費方法,然後把方法通過Supplier<?>的get提供給調用方,內部也維護了一個Map用來存放不同的function,黃金的實現看上去也挺優雅的,但還是不滿足開閉原則,所以阿里的解決方案裏並沒有提到這幾種方式.
相比白銀,黃金的實現方式比較裝X,但也不全是裝X,通過function接口提供的compose,andThen方法可以靈活地加入其它處理邏輯,所以勉強給評個黃金段位.
鑽石:
鑽石的思路其實就是策略模式+自定義註解+Springboot監聽.
先定義一個統一的計算賬單接口,然後不同的計費規則分別去實現此接口,不同的是,鑽石採用了自定義註解+Spring監聽的模式,在Spring上下文環境發生變化時(一般是Spring項目啓動完成),將這些加了註解的實現類統一自動注入到自定義的上下文中,交由Spring來管理,不需要再手動維護.需要時可以直接從上下文中取出對應的處理邏輯,看不懂沒關係,不妨先看代碼:
①定義統一的賬單處理接口
public interface CalcService {
/**
* 處理賬單計算
*/
void handleCalc(BigDecimal total);
}
② 不同的計費邏輯分別去實現該接口中的handleCalc方法:
@BillTypeHandler(BillType.ORDINARY)
public class OridinaryCalcServiceImpl implements CalcService {
@Override
public void handleCalc(BigDecimal total) {
System.out.println("處理普通用戶的賬單計費:" + total);
}
}
@BillTypeHandler(BillType.VIP1)
public class Vip1CalcServiceImpl implements CalcService {
@Override
public void handleCalc(BigDecimal total) {
System.out.println("處理vip1級用戶的賬單計費:"+total);
}
}
省略vip3,vip4...代碼,與上面雷同
③定義自定義註解:
@Service
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface BillTypeHandler {
BillType value();
enum BillType {
ORDINARY(0, "普通用戶"),
VIP1(1, "一級會員"),
VIP2(2, "二級會員");
private Integer type;
private String desc;
BillType(Integer type, String desc) {
this.type = type;
this.desc = desc;
}
public Integer getType() {
return type;
}
}
}
④創建自定義的上下文環境:
此上下文環境中維護了一個Map,用來存放CalcService的實現類,然後通過@Component交由Spring容器管理
@Component
public class BillServiceContext {
@Getter
private final static Map<Integer, CalcService> calcServiceMap;
static {
calcServiceMap = new HashMap<>();
}
public CalcService get(Integer type) {
return calcServiceMap.get(type);
}
public void put(Integer type, CalcService calcService) {
calcServiceMap.put(type, calcService);
}
}
⑤定義Springboot的監聽器:
在項目啓動後用來處理自定義註解的邏輯,先拿到每個添加了自定義註解的類,然後通過反射拿到該類的自定義註解,再通過自定義註解中的值(計費類型type枚舉值),然後把該值put進④中自定義的那個上下文環境的Map中
@Component
public class BillTypeListener implements ApplicationListener<ContextRefreshedEvent> {
@Resource
private BillServiceContext billServiceContext;
@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
Map<String, CalcService> beans = contextRefreshedEvent.getApplicationContext().getBeansOfType(
CalcService.class);
beans.forEach((k, calcService) -> {
Class clazz = calcService.getClass();
BillTypeHandler billTypeHandler = (BillTypeHandler)clazz.getAnnotation(BillTypeHandler.class);
billServiceContext.put(billTypeHandler.value().getType(), calcService);
});
}
}
⑥在需要調用的地方通過@Resource或者@Autowired註解將自定義的上下文環境注入,然後直接調用即可:
@SpringBootTest
@RunWith(SpringRunner.class)
public class BillTest {
@Autowired
BillServiceContext billServiceContext;
@Test
public void test() {
Bill bill1 = new Bill(BillType.ORDINARY.getType(), new BigDecimal(500));
billServiceContext.get(bill1.getType()).handleCalc(bill1.getTotal());
Bill bill2 = new Bill(BillType.VIP1.getType(), new BigDecimal(400));
billServiceContext.get(bill2.getType()).handleCalc(bill2.getTotal());
Bill bill3 = new Bill(BillType.VIP2.getType(), new BigDecimal(300));
billServiceContext.get(bill3.getType()).handleCalc(bill3.getTotal());
}
}
測試結果符合預期:
這樣的代碼看着要比if-else清爽多了,而且在之後如果有新的計費邏輯進來,只需要新增實現類即可,無需改動原有代碼,符合開閉原則,調用也是十分簡單,後期維護會更方便.
白銀:這樣的代碼不香嗎?
塑料:真香!但我還是選擇用If-else,不整這些花裏胡哨5556的...
若干年過去了,白銀當上了CTO,塑料成爲了其下屬,儘管白銀對塑料各種嫌棄,一心想讓塑料拍屁股走人,但一想到塑料的這段拳皇代碼,還是算了...