滿屏的if-else,怎么消滅它們?
最近在做代碼重構(gòu),發(fā)現(xiàn)了很多代碼的爛味道。其他的不多說,今天主要說說那些又臭又長的 if...else 要如何重構(gòu)。
圖片來自 Pexels
在介紹更更優(yōu)雅的編程之前,讓我們一起回顧一下,不好的 if...else 代碼。
又臭又長的 if...else
廢話不多說,先看看下面的代碼:
- public interface IPay {
- void pay();
- }
- @Service
- public class AliaPay implements IPay {
- @Override
- public void pay() {
- System.out.println("===發(fā)起支付寶支付===");
- }
- }
- @Service
- public class WeixinPay implements IPay {
- @Override
- public void pay() {
- System.out.println("===發(fā)起微信支付===");
- }
- }
- @Service
- public class JingDongPay implements IPay {
- @Override
- public void pay() {
- System.out.println("===發(fā)起京東支付===");
- }
- }
- @Service
- public class PayService {
- @Autowired
- private AliaPay aliaPay;
- @Autowired
- private WeixinPay weixinPay;
- @Autowired
- private JingDongPay jingDongPay;
- public void toPay(String code) {
- if ("alia".equals(code)) {
- aliaPay.pay();
- } else if ("weixin".equals(code)) {
- weixinPay.pay();
- } else if ("jingdong".equals(code)) {
- jingDongPay.pay();
- } else {
- System.out.println("找不到支付方式");
- }
- }
- }
PayService 類的 toPay 方法主要是為了發(fā)起支付,根據(jù)不同的 code,決定調(diào)用用不同的支付類(比如:aliaPay)的 pay 方法進行支付。
這段代碼有什么問題呢?也許有些人就是這么干的。
試想一下,如果支付方式越來越多,比如:又加了百度支付、美團支付、銀聯(lián)支付等等,就需要改 toPay 方法的代碼,增加新的 else...if 判斷,判斷多了就會導(dǎo)致邏輯越來越多?
很明顯,這里違反了設(shè)計模式六大原則的:
- 開閉原則:對擴展開放,對修改關(guān)閉。就是說增加新功能要盡量少改動已有代碼。
- 單一職責原則:顧名思義,要求邏輯盡量單一,不要太復(fù)雜,便于復(fù)用。
那有什么辦法可以解決這個問題呢?
消除 if...else 的錦囊妙計
使用注解
代碼中之所以要用 code 判斷使用哪個支付類,是因為 code 和支付類沒有一個綁定關(guān)系,如果綁定關(guān)系存在了,就可以不用判斷了。
我們先定義一個注解:
- @Retention(RetentionPolicy.RUNTIME)
- @Target(ElementType.TYPE)
- public @interface PayCode {
- String value();
- String name();
- }
在所有的支付類上都加上該注解:
- @PayCode(value = "alia", name = "支付寶支付")
- @Service
- public class AliaPay implements IPay {
- @Override
- public void pay() {
- System.out.println("===發(fā)起支付寶支付===");
- }
- }
- @PayCode(value = "weixin", name = "微信支付")
- @Service
- public class WeixinPay implements IPay {
- @Override
- public void pay() {
- System.out.println("===發(fā)起微信支付===");
- }
- }
- @PayCode(value = "jingdong", name = "京東支付")
- @Service
- public class JingDongPay implements IPay {
- @Override
- public void pay() {
- System.out.println("===發(fā)起京東支付===");
- }
- }
然后增加最關(guān)鍵的類:
- @Service
- public class PayService2 implements ApplicationListener<ContextRefreshedEvent> {
- private static Map<String, IPay> payMap = null;
- @Override
- public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
- ApplicationContext applicationContext = contextRefreshedEvent.getApplicationContext();
- Map<String, Object> beansWithAnnotation = applicationContext.getBeansWithAnnotation(PayCode.class);
- if (beansWithAnnotation != null) {
- payMap = new HashMap<>();
- beansWithAnnotation.forEach((key, value) ->{
- String bizType = value.getClass().getAnnotation(PayCode.class).value();
- payMap.put(bizType, (IPay) value);
- });
- }
- }
- public void pay(String code) {
- payMap.get(code).pay();
- }
- }
PayService2 類實現(xiàn)了 ApplicationListener 接口,這樣在 onApplicationEvent 方法中,就可以拿到 ApplicationContext 的實例。
我們再獲取打了 PayCode 注解的類,放到一個 map 中,map 中的 key 就是 PayCode 注解中定義的 value,跟 code 參數(shù)一致,value 是支付類的實例。
這樣,每次就可以每次直接通過 code 獲取支付類實例,而不用 if...else 判斷了。
如果要加新的支付方法,只需在支付類上面打上 PayCode 注解定義一個新的 code 即可。
注意:這種方式的 code 可以沒有業(yè)務(wù)含義,可以是純數(shù)字,只有不重復(fù)就行。
動態(tài)拼接名稱
該方法主要針對 code 是有業(yè)務(wù)含義的場景。
- @Service
- public class PayService3 implements ApplicationContextAware {
- private ApplicationContext applicationContext;
- private static final String SUFFIX = "Pay";
- @Override
- public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
- this.applicationContext = applicationContext;
- }
- public void toPay(String payCode) {
- ((IPay) applicationContext.getBean(getBeanName(payCode))).pay();
- }
- public String getBeanName(String payCode) {
- return payCode + SUFFIX;
- }
- }
我們可以看到,支付類 bean 的名稱是由 code 和后綴拼接而成,比如:aliaPay、weixinPay 和 jingDongPay。
這就要求支付類取名的時候要特別注意,前面的一段要和 code 保持一致。
調(diào)用的支付類的實例是直接從 ApplicationContext 實例中獲取的,默認情況下 bean 是單例的,放在內(nèi)存的一個 map 中,所以不會有性能問題。
特別說明一下,這種方法實現(xiàn)了 ApplicationContextAware 接口跟上面的 ApplicationListener 接口不一樣,是想告訴大家獲取 ApplicationContext 實例的方法不只一種。
模板方法判斷
當然除了上面介紹的兩種方法之外,Spring 的源碼實現(xiàn)中也告訴我們另外一種思路,解決 if...else 問題。
我們先一起看看 Spring AOP 的部分源碼,看一下 DefaultAdvisorAdapterRegistry 的 wrap 方法:
- public Advisor wrap(Object adviceObject) throws UnknownAdviceTypeException {
- if (adviceObject instanceof Advisor) {
- return (Advisor) adviceObject;
- }
- if (!(adviceObject instanceof Advice)) {
- throw new UnknownAdviceTypeException(adviceObject);
- }
- Advice advice = (Advice) adviceObject;
- if (advice instanceof MethodInterceptor) {
- return new DefaultPointcutAdvisor(advice);
- }
- for (AdvisorAdapter adapter : this.adapters) {
- if (adapter.supportsAdvice(advice)) {
- return new DefaultPointcutAdvisor(advice);
- }
- }
- throw new UnknownAdviceTypeException(advice);
- }
重點看看 supportAdvice 方法,有三個類實現(xiàn)了這個方法。
我們隨便抽一個類看看:
- class AfterReturningAdviceAdapter implements AdvisorAdapter, Serializable {
- @Override
- public boolean supportsAdvice(Advice advice) {
- return (advice instanceof AfterReturningAdvice);
- }
- @Override
- public MethodInterceptor getInterceptor(Advisor advisor) {
- AfterReturningAdvice advice = (AfterReturningAdvice) advisor.getAdvice();
- return new AfterReturningAdviceInterceptor(advice);
- }
- }
該類的 supportsAdvice 方法非常簡單,只是判斷了一下 advice 的類型是不是 AfterReturningAdvice。
我們看到這里應(yīng)該有所啟發(fā)。
其實,我們可以這樣做,定義一個接口或者抽象類,里面有個 support 方法判斷參數(shù)傳的 code 是否自己可以處理,如果可以處理則走支付邏輯。
- public interface IPay {
- boolean support(String code);
- void pay();
- }
- @Service
- public class AliaPay implements IPay {
- @Override
- public boolean support(String code) {
- return "alia".equals(code);
- }
- @Override
- public void pay() {
- System.out.println("===發(fā)起支付寶支付===");
- }
- }
- @Service
- public class WeixinPay implements IPay {
- @Override
- public boolean support(String code) {
- return "weixin".equals(code);
- }
- @Override
- public void pay() {
- System.out.println("===發(fā)起微信支付===");
- }
- }
- @Service
- public class JingDongPay implements IPay {
- @Override
- public boolean support(String code) {
- return "jingdong".equals(code);
- }
- @Override
- public void pay() {
- System.out.println("===發(fā)起京東支付===");
- }
- }
每個支付類都有一個 support 方法,判斷傳過來的 code 是否和自己定義的相等。
- @Service
- public class PayService4 implements ApplicationContextAware, InitializingBean {
- private ApplicationContext applicationContext;
- private List<IPay> payList = null;
- @Override
- public void afterPropertiesSet() throws Exception {
- if (payList == null) {
- payList = new ArrayList<>();
- Map<String, IPay> beansOfType = applicationContext.getBeansOfType(IPay.class);
- beansOfType.forEach((key, value) -> payList.add(value));
- }
- }
- @Override
- public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
- this.applicationContext = applicationContext;
- }
- public void toPay(String code) {
- for (IPay iPay : payList) {
- if (iPay.support(code)) {
- iPay.pay();
- }
- }
- }
- }
這段代碼中先把實現(xiàn)了 IPay 接口的支付類實例初始化到一個 list 集合中,返回在調(diào)用支付接口時循環(huán)遍歷這個 list 集合,如果 code 跟自己定義的一樣,則調(diào)用當前的支付類實例的 pay 方法。
策略+工廠模式
這種方式也是用于 code 是有業(yè)務(wù)含義的場景:
- 策略模式定義了一組算法,把它們一個個封裝起來, 并且使它們可相互替換。
- 工廠模式用于封裝和管理對象的創(chuàng)建,是一種創(chuàng)建型模式。
- public interface IPay {
- void pay();
- }
- @Service
- public class AliaPay implements IPay {
- @PostConstruct
- public void init() {
- PayStrategyFactory.register("aliaPay", this);
- }
- @Override
- public void pay() {
- System.out.println("===發(fā)起支付寶支付===");
- }
- }
- @Service
- public class WeixinPay implements IPay {
- @PostConstruct
- public void init() {
- PayStrategyFactory.register("weixinPay", this);
- }
- @Override
- public void pay() {
- System.out.println("===發(fā)起微信支付===");
- }
- }
- @Service
- public class JingDongPay implements IPay {
- @PostConstruct
- public void init() {
- PayStrategyFactory.register("jingDongPay", this);
- }
- @Override
- public void pay() {
- System.out.println("===發(fā)起京東支付===");
- }
- }
- public class PayStrategyFactory {
- private static Map<String, IPay> PAY_REGISTERS = new HashMap<>();
- public static void register(String code, IPay iPay) {
- if (null != code && !"".equals(code)) {
- PAY_REGISTERS.put(code, iPay);
- }
- }
- public static IPay get(String code) {
- return PAY_REGISTERS.get(code);
- }
- }
- @Service
- public class PayService3 {
- public void toPay(String code) {
- PayStrategyFactory.get(code).pay();
- }
- }
這段代碼的關(guān)鍵是 PayStrategyFactory 類,它是一個策略工廠,里面定義了一個全局的 map,在所有 IPay 的實現(xiàn)類中注冊當前實例到 map 中。
然后在調(diào)用的地方通過 PayStrategyFactory 類根據(jù) code 從 map 獲取支付類實例即可。
責任鏈模式
這種方式在代碼重構(gòu)時用來消除 if...else 非常有效。
責任鏈模式:將請求的處理對象像一條長鏈一般組合起來,形成一條對象鏈。請求并不知道具體執(zhí)行請求的對象是哪一個,這樣就實現(xiàn)了請求與處理對象之間的解耦。
常用的 filter、spring aop 就是使用了責任鏈模式,這里我稍微改良了一下,具體代碼如下:
- public abstract class PayHandler {
- @Getter
- @Setter
- protected PayHandler next;
- public abstract void pay(String pay);
- }
- @Service
- public class AliaPayHandler extends PayHandler {
- @Override
- public void pay(String code) {
- if ("alia".equals(code)) {
- System.out.println("===發(fā)起支付寶支付===");
- } else {
- getNext().pay(code);
- }
- }
- }
- @Service
- public class WeixinPayHandler extends PayHandler {
- @Override
- public void pay(String code) {
- if ("weixin".equals(code)) {
- System.out.println("===發(fā)起微信支付===");
- } else {
- getNext().pay(code);
- }
- }
- }
- @Service
- public class JingDongPayHandler extends PayHandler {
- @Override
- public void pay(String code) {
- if ("jingdong".equals(code)) {
- System.out.println("===發(fā)起京東支付===");
- } else {
- getNext().pay(code);
- }
- }
- }
- @Service
- public class PayHandlerChain implements ApplicationContextAware, InitializingBean {
- private ApplicationContext applicationContext;
- private PayHandler header;
- public void handlePay(String code) {
- header.pay(code);
- }
- @Override
- public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
- this.applicationContext = applicationContext;
- }
- @Override
- public void afterPropertiesSet() throws Exception {
- Map<String, PayHandler> beansOfTypeMap = applicationContext.getBeansOfType(PayHandler.class);
- if (beansOfTypeMap == null || beansOfTypeMap.size() == 0) {
- return;
- }
- List<PayHandler> handlers = beansOfTypeMap.values().stream().collect(Collectors.toList());
- for (int i = 0; i < handlers.size(); i++) {
- PayHandler payHandler = handlers.get(i);
- if (i != handlers.size() - 1) {
- payHandler.setNext(handlers.get(i + 1));
- }
- }
- header = handlers.get(0);
- }
- }
這段代碼的關(guān)鍵是每個 PayHandler 的子類,都定義了下一個需要執(zhí)行的 PayHandler 子類,構(gòu)成一個鏈式調(diào)用,通過 PayHandlerChain 把這種鏈式結(jié)構(gòu)組裝起來。
其他的消除 if...else 的方法
當然實際項目開發(fā)中使用 if...else 判斷的場景非常多,上面只是其中幾種場景。下面再列舉一下,其他常見的場景。
①根據(jù)不同的數(shù)字返回不同的字符串
代碼如下:
- public String getMessage(int code) {
- if (code == 1) {
- return "成功";
- } else if (code == -1) {
- return "失敗";
- } else if (code == -2) {
- return "網(wǎng)絡(luò)超時";
- } else if (code == -3) {
- return "參數(shù)錯誤";
- }
- throw new RuntimeException("code錯誤");
- }
其實,這種判斷沒有必要,用一個枚舉就可以搞定。
- public enum MessageEnum {
- SUCCESS(1, "成功"),
- FAIL(-1, "失敗"),
- TIME_OUT(-2, "網(wǎng)絡(luò)超時"),
- PARAM_ERROR(-3, "參數(shù)錯誤");
- private int code;
- private String message;
- MessageEnum(int code, String message) {
- this.code = code;
- this.message = message;
- }
- public int getCode() {
- return this.code;
- }
- public String getMessage() {
- return this.message;
- }
- public static MessageEnum getMessageEnum(int code) {
- return Arrays.stream(MessageEnum.values()).filter(x -> x.code == code).findFirst().orElse(null);
- }
- }
再把調(diào)用方法稍微調(diào)整一下:
- public String getMessage(int code) {
- MessageEnum messageEnum = MessageEnum.getMessageEnum(code);
- return messageEnum.getMessage();
- }
完美。
②集合中的判斷
上面的枚舉 MessageEnum 中的 getMessageEnum 方法,如果不用 java8 的語法的話,可能要這樣寫:
- public static MessageEnum getMessageEnum(int code) {
- for (MessageEnum messageEnum : MessageEnum.values()) {
- if (code == messageEnum.code) {
- return messageEnum;
- }
- }
- return null;
- }
對于集合中過濾數(shù)據(jù),或者查找方法,java8 有更簡單的方法消除 if...else 判斷。
- public static MessageEnum getMessageEnum(int code) {
- return Arrays.stream(MessageEnum.values()).filter(x -> x.code == code).findFirst().orElse(null);
- }
③簡單的判斷
其實有些簡單的 if...else 完全沒有必要寫,可以用三目運算符代替,比如這種情況:
- public String getMessage2(int code) {
- if(code == 1) {
- return "成功";
- }
- return "失敗";
- }
改成三目運算符:
- public String getMessage2(int code) {
- return code == 1 ? "成功" : "失敗";
- }
修改之后代碼更簡潔一些。
④Spring 中的判斷
對于參數(shù)的異常,越早被發(fā)現(xiàn)越好,在 Spring 中提供了 Assert 用來幫助我們檢測參數(shù)是否有效。
- public void save(Integer code,String name) {
- if(code == null) {
- throw Exception("code不能為空");
- } else {
- if(name == null) {
- throw Exception("name不能為空");
- } else {
- System.out.println("doSave");
- }
- }
- }
如果參數(shù)非常多的話,if...else 語句會很長,這時如果改成使用 Assert 類判斷,代碼會簡化很多:
- public String save2(Integer code,String name) {
- Assert.notNull(code,"code不能為空");
- Assert.notNull(name,"name不能為空");
- System.out.println("doSave");
- }
當然,還有很多其他的場景可以優(yōu)化 if...else,我再這里就不一一介紹了,感興趣的朋友可以給我留言,一起探討和研究一下。
作者:蘇三
編輯:陶家龍
出處:轉(zhuǎn)載自公眾號蘇三說技術(shù)