別再用if-else了,用注解去代替他吧

來自公眾號:咖啡拿鐵
作者:謝英豪

本文來自謝英豪同學的投稿,希望大家讀完能有所收獲。

策略模式

經常在網上看到一些名為“別再if-else走天下了”,“教你干掉if-else”等之類的文章,大部分都會講到用策略模式去代替if-else。策略模式實現的方式也大同小異。主要是定義統一行為(接口或抽象類),并實現不同策略下的處理邏輯(對應實現類)。客戶端使用時自己選擇相應的處理類,利用工廠或其他方式。

注解實現

本文要說的是用注解實現策略模式的方式,以及一些注意點。
話不多說,還是以最常 見的訂單處理為例。首先定義這樣一個訂單實體類:

@Data
public class Order {
    /**
     * 訂單來源
     */
    private String source;
    /**
     * 支付方式
     */
    private String payMethod;
    /**
     * 訂單編號
     */
    private String code;
    /**
     * 訂單金額
     */
    private BigDecimal amount;
    // ...其他的一些字段
}

假如對于不同來源(pc端、移動端)的訂單需要不同的邏輯處理。項目中一般會有OrderService這樣一個類,如下,里面有一坨if-else的邏輯,目的是根據訂單的來源的做不同的處理。

@Service
public class OrderService {

    public void orderService(Order order) {
        if(order.getSource().equals("pc")){
            // 處理pc端訂單的邏輯
        }else if(order.getSource().equals("mobile")){
            // 處理移動端訂單的邏輯
        }else {
            // 其他邏輯
        }
    }
}

策略模式就是要干掉上面的一坨if-else,使得代碼看起來優雅且高大上。
現在就讓我們開始干掉這一坨if-else。先總覽下結構:

image

1.首先定義一個OrderHandler接口,此接口規定了處理訂單的方法。

public interface OrderHandler {
    void handle(Order order);
}

2.定義一個OrderHandlerType注解,來表示某個類是用來處理何種來源的訂單。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Service
public @interface OrderHandlerType {
    String source();
}

3.接下來就是實現pc端和移動端訂單處理各自的handler,并加上我們所定義的OrderHandlerType注解。

@OrderHandlerType(source = "mobile")
public class MobileOrderHandler implements OrderHandler {
    @Override
    public void handle(Order order) {
        System.out.println("處理移動端訂單");
    }
}

@OrderHandlerType(source = "pc")
public class PCOrderHandler implements OrderHandler {
    @Override
    public void handle(Order order) {
        System.out.println("處理PC端訂單");
    }
}

4.以上準備就緒后,就是向spring容器中注入各種訂單處理的handler,并在OrderService.orderService方法中,通過策略(訂單來源)去決定選擇哪一個OrderHandler去處理訂單。我們可以這樣做:

@Service
public class OrderService {

    private Map<String, OrderHandler> orderHandleMap;

    @Autowired
    public void setOrderHandleMap(List<OrderHandler> orderHandlers) {
        // 注入各種類型的訂單處理類
        orderHandleMap = orderHandlers.stream().collect(
                Collectors.toMap(orderHandler -> AnnotationUtils.findAnnotation(orderHandler.getClass(), OrderHandlerType.class).source(),
                        v -> v, (v1, v2) -> v1));
    }

    public void orderService(Order order) {
        // ...一些前置處理

        // 通過訂單來源確定對應的handler
        OrderHandler orderHandler = orderHandleMap.get(order.getSource());
        orderHandler.handle(order);

        // ...一些后置處理
    }
}

在OrderService中,維護了一個orderHandleMap,它的key為訂單來源,value為對應的訂單處理器Handler。通過@Autowired去初始化orderHandleMap(這里有一個lambda表達式,仔細看下其實沒什么難度的)。這樣一來,OrderService.orderService里的一坨if-else不見了,取而代之的僅僅是兩行代碼。即,先從orderHandleMap中根據訂單來源獲取對應的OrderHandler,然后執行OrderHandler.handle方法即可。
這種做法的好處是,不論以后業務如何發展致使訂單來源種類增加,OrderService的核心邏輯不會改變,我們只需要實現新增來源的OrderHandler即可,且團隊中每人開發各自負責的訂單來源對應的OrderHandler即可,彼此間互不干擾。

到此,似乎已經講完了通過注解實現策略模式,干掉if-else的方法,就這樣結束了嗎?不,真正的重點從現在才開始。

現在回過頭看orderHandleMap這個Map,它的key是訂單來源,假如,我們想通過訂單來源+訂單支付方式這兩個屬性來決定到底使用哪一種OrderHandler怎么辦?比如PC端支付寶支付的訂單是一種處理邏輯(PCAliPayOrderHandler),PC端微信支付的訂單是另外一種處理邏輯(PCWeChatOrderHandler),對應的還有移動端支付寶支付(MobileAliPayOrderHandler)和移動端微信支付(MobileWeChatOrderHandler)。

這時候我們的key應該存什么呢,可能有人會說,我直接存訂單來源+訂單支付方式組成的字符串不就行了嗎?確實可以,但是如果這時業務邏輯又變了(向pm低頭),PC端支付寶支付和微信支付是同一種處理邏輯,而移動端支付寶支付和微信支付是不同的處理邏輯,那情況就變成了PCAliPayOrderHandler和PCWeChatOrderHandler這兩個類是同一套代碼邏輯。我們干掉了if-else,但卻造出了兩份相同的代碼,這是一個作為有強迫癥的程序員所不能容忍的。怎么干掉這兩個邏輯相同的類呢?

首先,我們可以回顧下,注解它究竟是個什么玩意?不知道大家有沒有注意到定義注解的語法,也就是@interface,與定義接口的語法想比,僅僅多了一個@。翻看jdk,可以找到這么一個接口Annotation,如下

/**
 * The common interface extended by all annotation types.  Note that an
 * interface that manually extends this one does <i>not</i> define
 * an annotation type.  Also note that this interface does not itself
 * define an annotation type.
 *
 * More information about annotation types can be found in section 9.6 of
 * <cite>The Java&trade; Language Specification</cite>.
 *
 * The {@link java.lang.reflect.AnnotatedElement} interface discusses
 * compatibility concerns when evolving an annotation type from being
 * non-repeatable to being repeatable.
 *
 * @author  Josh Bloch
 * @since   1.5
 */
public interface Annotation {
  // …省略
}

開頭就表明了,The common interface extended by all annotation types。說的很明白了,其實注解它就是個接口,對,它就是個接口而已,@interface僅僅是個語法糖。那么,注解既然是個接口,就必然會有相應的實現類,那實現類哪里來呢?上述中我們僅僅定義了OrderHandlerType注解,別的什么也沒有做。這時候不得不提動態代理了,一定是jdk在背后為我們做了些什么。
為了追蹤JVM在運行過程中生成的JDK動態代理類。我們可以設置VM啟動參數如下:

-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true

該參數可以保存所生成的JDK動態代理類到本地。額外說一句,若我們想追蹤cglib所生成的代理類,即對應的字節碼文件,可以設置參數:

System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "保存的路徑");

添加參數后再次啟動項目,可以看到我們項目里多了許多class文件(這里只截取了部分,筆者啟動的項目共生成了97個動態代理類。由于我的項目是springboot環境,因此生成了許多如ConditionalOnMissingBean、Configuration、Autowired等注解對應的代理類):

image

那接下來就是要找到我們所關心的,即jdk為我們自定義的OrderHandlerType注解所生成的代理類。由于jdk生成的動態代理類都會繼承Proxy這個類,而java又是單繼承的,所以,我們只需要找到實現了OrderHandlerType的類即可。遍歷這些類文件,發現$Proxy63.class實現了我們定義的OrderHandlerType注解:

public final class $Proxy63 extends Proxy implements OrderHandlerType {
    private static Method m1;
    private static Method m2;
    private static Method m4;
    private static Method m3;
    private static Method m0;

    public $Proxy63(InvocationHandler var1) throws  {
        super(var1);
    }

    // …省略
}

我們知道,jdk動態代理其實現的核心是:

image

也就是這個構造函數的InvocationHandler對象了。那么注解的InvocationHandler是哪個具體實現呢?不難發現就是:


image

這個類里面的核心屬性,就是那個 memberValues,我們在使用注解時給注解屬性的賦值,都存儲在這個map里了。而代理類中的各種方法的實現,實際上是調用了 AnnotationInvocationHandler 里的 invoke 方法。
好了,現在我們知道了注解就是個接口,且通過動態代理實現其中所定義的各種方法。那么回到我們的OrderService,為什么不把key的類型設置為OrderHandlerType?就像這樣。

private Map<OrderHandlerType, OrderHandler> orderHandleMap;

如此一來,不管決定訂單處理器orderhandler的因素怎么變,我們便可以以不變應萬變(這不就是我們所追求的代碼高擴展性和靈活性么)。那當我們的map的key變成了OrderHandlerType之后,注入和獲取的邏輯就要相應改變,注入的地方很好改變,如下:

public class OrderService {
    private Map<OrderHandlerType, OrderHandler> orderHandleMap;

    @Autowired
    public void setOrderHandleMap(List<OrderHandler> orderHandlers) {
        // 注入各種類型的訂單處理類
        orderHandleMap = orderHandlers.stream().collect(
            Collectors.toMap(orderHandler -> AnnotationUtils.findAnnotation(orderHandler.getClass(), OrderHandlerType.class),
                    v -> v, (v1, v2) -> v1));
    }
    // ...省略
}

那獲取的邏輯要怎么實現?我們怎么根據order的來源和支付方式去orderHandleMap里獲取對應的OrderHandler呢?問題變成了如何關聯order的來源和支付方式與OrderHandlerType注解。
還記得剛才所說的注解就是個接口嗎,既然是個接口,我們自己實現一個類不就完事了么,這樣就把order的來源和支付方式與OrderHandlerType注解關聯起來了。說干就干,現在我們有了這么一個類,

public class OrderHandlerTypeImpl implements OrderHandlerType {

    private String source;
    private String payMethod;

    OrderHandlerTypeImpl(String source, String payMethod) {
        this.source = source;
        this.payMethod = payMethod;
    }

    @Override
    public String source() {
        return source;
    }

    @Override
    public String payMethod() {
        return payMethod;
    }

    @Override
    public Class<? extends Annotation> annotationType() {
        return OrderHandlerType.class;
    }

}

在獲取對應OrderHandler時我們可以這樣寫,

public void orderService(Order order) {
    // ...一些前置處理

    // 通過訂單來源確以及支付方式獲取對應的handler
    OrderHandlerType orderHandlerType = new OrderHandlerTypeImpl(order.getSource(), order.getPayMethod());
    OrderHandler orderHandler = orderHandleMap.get(orderHandlerType);
    orderHandler.handle(order);

    // ...一些后置處理
}

看起來沒什么問題了,來運行一下。不對勁啊,空指針,那個異常它來了。

image

我們斷點打在NPE那一行,


image

一定是姿勢不對,漏掉了什么。那我們就來分析下。orderHandleMap中確實注入了所定義的幾個OrderHandler(PCAliPayOrderHandler、PCWeChatOrderHandler、MobileAliPayOrderHandler、MobileWeChatOrderHandler),但是get卻沒有獲取到,這是為什么呢?我們來回憶下,map的get方法邏輯,還記得最開始學習java時的hashCode和equals方法嗎?
不知道大家注意到沒有,map中key對應的類型是$Proxy63(jdk動態代理給我們生成的),跟我們自己實現的OrderHandlerTypeImpl是不同類型的。
梳理下,在Autowied時,我們放進orderHandleMap的key是動態代理類對象,而獲取時,自定義了OrderHandlerTypeI實現類OrderHandlerTypeImpl,而又沒有重寫hashCode和equals方法,才導致從map中沒有獲取到我們所想要的OrderHandler,那么,我們把實現類OrderHandlerTypeImpl的hashCode和equals這兩個方法重寫,保持跟動態代理類生成的一樣不就行了嗎?再回看下動態代理給我們生成的這兩個方法,前面說了,注解對應的代理類方法調用實際上都是AnnotationInvocationHandler里面的方法,翻看AnnotationInvocationHandler里面的hashCode和equals方法:

private int hashCodeImpl() {
    int var1 = 0;
    Entry var3;
    for(Iterator var2 = this.memberValues.entrySet().iterator(); var2.hasNext(); var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())) {
        var3 = (Entry)var2.next();
    }
    return var1;
}

private Boolean equalsImpl(Object var1) {
    if (var1 == this) {
        return true;
    } else if (!this.type.isInstance(var1)) {
        return false;
    } else {
        Method[] var2 = this.getMemberMethods();
        int var3 = var2.length;
        for(int var4 = 0; var4 < var3; ++var4) {
            Method var5 = var2[var4];
            String var6 = var5.getName();
            Object var7 = this.memberValues.get(var6);
            Object var8 = null;
            AnnotationInvocationHandler var9 = this.asOneOfUs(var1);
            if (var9 != null) {
                var8 = var9.memberValues.get(var6);
            } else {
                try {
                    var8 = var5.invoke(var1);
                } catch (InvocationTargetException var11) {
                    return false;
                } catch (IllegalAccessException var12) {
                    throw new AssertionError(var12);
                }
            }
            if (!memberValueEquals(var7, var8)) {
                return false;
            }
        }
        return true;
    }
}

具體的邏輯也比較簡單,就不分析了。那我們就按照AnnotationInvocationHandler中的實現,在我們的OrderHandlerTypeImpl中按照相同的邏輯重寫下這兩個方法,如下

public class OrderHandlerTypeImpl implements OrderHandlerType {

    // …省略

    @Override
    public int hashCode() {
        int hashCode = 0;
        hashCode += (127 * "source".hashCode()) ^ source.hashCode();
        hashCode += (127 * "payMethod".hashCode()) ^ payMethod.hashCode();
        return hashCode;
    }


    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof OrderHandlerType)) {
            return false;
        }
        OrderHandlerType other = (OrderHandlerType) obj;
        return source.equals(other.source()) && payMethod.equals(other.payMethod());
    }
}

再次運行看看是否達到我們預期,果不其然,這次可以正常獲取到了handler,至此,大功告成。

image

這樣以來,不管以后業務怎么發展,OrderService核心邏輯不會改變,只需要擴展OrderHandler即可。

如果大家覺得這篇文章對你有幫助,你的關注和轉發是對我最大的支持,O(∩_∩)O:

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,119評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,382評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,038評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,853評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,616評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,112評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,192評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,355評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,869評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,727評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,928評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,467評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,165評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,570評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,813評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,585評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,892評論 2 372