靜態代理模式——時間都去哪兒了

蟬沐風發表於2022-01-06

我是蟬沐風,一個讓你沉迷於技術的講述者
微信公眾號【蟬沐風】,歡迎大家關注留言

時間都去哪兒了

「跑碼場」在陀螺的經營下,貓糧生意一直很紅火。

這一天,陀螺找到程式喵招財,說道:“年關將至,最近訂單有點多,我檢視了一下系統監控,發現RT有點長,你排查一下原因,別影響顧客下單。”

“RT是個啥?”招財撓了撓頭問道。

“RT就是系統響應時間啊,在你進行系統升級之後,系統響應時間比原來變長了。”

“......直接說系統變卡了不就得了,還說得這麼花裡胡哨”,招財小聲嘀咕,卻也不敢直接回懟自己的師傅。

陀螺看著招財,“你這傢伙在嘀咕什麼呢?”。

“啊,沒有沒有,”招財連忙解釋道,“我在想,時間都去哪兒了呢?前段時間系統做了下升級,對接了一種新的支付方式,問題很有可能出在第三方的支付介面上。”

public interface Payable {

    /**
     * 支付介面
     */
    void pay();
}
/**
 * @author 蟬沐風
 * @description 「四十大盜」金融公司
 * @date 2022/1/5
 */
public class SiShiDaDao implements Payable {

    @Override
    public void pay() {
        try {
            // ...
            System.out.println("「四十大盜」支付介面呼叫中......");
            //模擬方法呼叫延時
            TimeUnit.MILLISECONDS.sleep((long) (Math.random() * 6000));
            // ...
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

“你這鍋倒是甩得挺快,那你說說怎麼確定這個支付介面的執行時間呢?”陀螺問道。

“這還不簡單,我在呼叫pay()方法的位置前後各自新增一個記錄時間戳的語句就行了,就像這樣。”

public class Client {
    public static void main(String[] args) {
        Payable payable = new SiShiDaDao();

        System.out.println("方法計時開始");
        long startTime = System.currentTimeMillis();
        payable.pay();
        long endTime = System.currentTimeMillis();
        System.out.println("方法執行時長為:" + (endTime - startTime) + "毫秒");
    }
}

image-20220106000301573

"師傅,你看吶!這個方法居然執行了3秒多,問題果然出在了這個介面身上!"招財臉上掛著抑制不住的興奮。

“你的直覺和運氣都很好,這麼快就被你定位到了問題。”陀螺的表情未見波動,“先不著急修改,對於單純的排查測試而言,你的程式碼並沒有問題。但是如果現在恰好有一個在pay()方法執行前後新增記錄時間戳的業務需求,你會怎麼實現?”

“我並不覺得上面寫的測試程式碼用在實際業務場景下會有什麼問題。”招財自信地說道。

“如果還需要你在上述需求的邏輯前後再新增日誌記錄呢?”陀螺追問。

“那就繼續在前後新增日誌邏輯唄。”

“如果還需要你繼續新增支付許可權稽核邏輯以及積分變動邏輯呢?”陀螺繼續追問。

“這......雖然可以繼續在呼叫的位置前後追加各種邏輯,但是如此一來方法未免太臃腫了,但是真的會有這麼變態的需求嗎?”招財已然沒有了之前的興奮,眼神裡透著迷惘。

需求這種東西,唯一不變的就是變化本身。”

靜態代理的誕生

“那有沒有什麼好的解決辦法呢?”招財問道。

“我知道你現在住的房子是通過房產中介找到的,其實房產中介就是一個解決這個問題的思路。中介在你和真正的房屋出租者之間充當了媒介,中介對你提供的租房服務並不是中介本身有房子出租(排除中介自己買房子出租的情況),本質上是利用了房屋出租者提供的房屋出租功能,只不過中介加入了更強的宣傳推介。”陀螺解釋道。

“我還是沒懂,房產中介和這個第三方介面有什麼聯絡?”

陀螺繼續解釋道:“中介就是一個代理,代理本身使用了被代理物件提供的功能,但是又在功能的基礎上做了增強。再以支付介面為例,金融公司提供的支付介面就是被代理物件(相當於真正的房屋出租者),處於某種考慮(保護被代理物件,或者被代理物件已經邏輯完備,例如無法要求房屋出租者有中介那麼強有力的推銷渠道),我們不會要求這個介面給我們提供更多的邏輯功能(因為是第三方jar包,我們無法修改原始碼),我們需要創造一個類似於房產中介的一個支付代理物件,在實現支付功能的基礎上加上我們需要的業務邏輯。”

“我明白了,這是一種設計模式嗎?”

“是的,這就是靜態代理模式。以記錄執行時間為例,嘗試著實現一下靜態代理模式吧。”陀螺鼓勵招財。

臃腫的繼承

招財思考了一會兒,寫出瞭如下程式碼

/**
 * @author 蟬沐風
 * @description 「四十大盜」金融公司計時功能的代理
 * @date 2022/1/5
 */
public class SiShiDaDaoTimeProxy extends SiShiDaDao {

    @Override
    public void pay() {
        System.out.println("方法計時開始");
        long startTime = System.currentTimeMillis();

        super.pay();

        long endTime = System.currentTimeMillis();
        System.out.println("方法執行時長為:" + (endTime - startTime) + "毫秒");
    }
}

“我建立了一個繼承自SiShiDaDao的代理物件SiShiDaDaoTimeProxy,並重寫了pay()方法,在呼叫父類pay()方法的基礎上,前後新增了計時的邏輯,這就是你說的功能增強吧。如此一來,客戶端呼叫支付介面的時候表面上使用的是我寫的代理物件,但是本質上用的還是金融公司的介面。”

說罷,招財又寫出了客戶端呼叫的程式碼。

/**
 * @author chanmufeng
 * @description 呼叫客戶端
 * @date 2022/1/5
 */
public class Client {
    public static void main(String[] args) {
        SiShiDaDaoTimeProxy proxy = new SiShiDaDaoTimeProxy();
        proxy.pay();
    }
}

image-20220106000137455

陀螺欣慰地點點頭,"很好,你已經理解了靜態代理的本質,如果我現在要你在開始計時之前列印一條日誌,在計時結束之後再列印一條日誌,對你來說也不是什麼難事兒了。"

“簡單!看我的!”招財很快便寫出了程式碼。

/**
 * @author 蟬沐風
 * @description 「四十大盜」金融公司日誌計時代理
 * @date 2022/1/5
 */
public class SiShiDaDaoLogTimeProxy extends SiShiDaDaoTimeProxy {

    @Override
    public void pay() {
        System.out.println("列印日誌1");

        super.pay();

        System.out.println("列印日誌2");
    }
}

客戶端程式碼和執行結果如下

public class Client {
    public static void main(String[] args) {
        SiShiDaDaoLogTimeProxy proxy = new SiShiDaDaoLogTimeProxy();
        proxy.pay();
    }
}

image-20220106001325345

陀螺盯著招財,極力憋住笑聲,繼續問道,“我現在後悔了,想先計時,然後再列印日誌,你該怎麼辦?”

招財慌了,好傢伙,剛教育我的唯一不變的就是變化這個真理這麼快就讓我付諸實踐了。

招財想,需求雖然只是變化了一下邏輯順序,但是對於我實現而言簡直就是翻天覆地的變化,為了應對需求,我必須先建立一個繼承自SiShiDaDao的代理物件SiShiDaDaoLogProxy,然後再建立一個SiShiDaDaoTimeLogProxy繼承SiShiDaDaoLogProxy,這還只是兩層邏輯,萬一邏輯更多,需要修改的代價就太大了!

招財明白了,這是陀螺故意考驗自己。

“師傅,您就別玩兒我了,我意識到了我目前的實現方式不足以靈活地應付您說的需求,可是問題究竟出在哪兒呢?”招財求饒道。

陀螺笑著說:“看來你終於發現問題了,你使用繼承實現了靜態代理,可以達到目的,但是不夠靈活,看一下使用繼承時的UML類圖。”

image-20220106083508669

面向介面程式設計

招財看了一下,果然發現了問題,使用繼承得到的UML是一條筆直的邏輯鏈,毫無複用性可言,無法通過組合的方式來滿足不同的邏輯呼叫順序。

哎?組合?招財想到了什麼,“我好像知道如何走出這個困境了,看我程式碼”。

/**
 * @author 蟬沐風
 * @description 「四十大盜」金融公司計時代理
 * @date 2022/1/6
 */
public class SiShiDaDaoTimeProxy implements Payable {

    //被代理物件
    private Payable payable;

    public SiShiDaDaoTimeProxy(Payable payable) {
        this.payable = payable;
    }


    @Override
    public void pay() {
        System.out.println("方法計時開始");
        long startTime = System.currentTimeMillis();

        payable.pay();

        long endTime = System.currentTimeMillis();
        System.out.println("方法執行時長為:" + (endTime - startTime) + "毫秒");
    }
}
/**
 * @author 蟬沐風
 * @description 「四十大盜」金融公司日誌代理
 * @date 2022/1/6
 */
public class SiShiDaDaoLogProxy implements Payable {

    //被代理物件
    private Payable payable;

    public SiShiDaDaoLogProxy(Payable payable) {
        this.payable = payable;
    }


    @Override
    public void pay() {
        System.out.println("列印日誌1");

        payable.pay();

        System.out.println("列印日誌2");
    }
}

"我使用組合的方式來代替繼承,計時代理和日誌代理都實現了Payable介面,在建立代理的同時需要傳入被代理物件,然後在代理中呼叫傳入的被代理物件的方法,在方法前後就可以做一些增強的操作了。"招財解釋道。

“接著說說,用這種實現方式是怎麼解決我剛才的問題的?”陀螺繼續問道。

招財不慌不忙,“如果現在的需求是先列印日誌,再計算時間,客戶端只需要這麼呼叫。”

public class Client {
    public static void main(String[] args) {
        //先列印日誌,再計算時間
        Payable proxy = new SiShiDaDaoLogProxy(new SiShiDaDaoTimeProxy(new SiShiDaDao()));
        proxy.pay();
    }
}

image-20220106093507745

"同樣,如果想先計算時間,再列印日誌,只需要修改一下代理生成的順序就可以了,至於代理的內部實現一點也不需要變動。因為每個代理本身實現了Payable,因此又可以作為被代理物件傳入,繼續被其他物件所代理。"

public class Client {
    public static void main(String[] args) {
        //先計算時間,再列印日誌
        Payable proxy = new SiShiDaDaoTimeProxy(new SiShiDaDaoLogProxy(new SiShiDaDao()));
        proxy.pay();
    }
}

“有點俄羅斯套娃的意思了”,陀螺聽著招財這麼解釋,笑著說道。

“這個說法還真是形象,根據需求調整“套”的順序就可以了,UML圖我也給出來了。”

image-20220106095242821

“非常好,目前為止你已經解決了時間都去哪兒了的問題了。如果現在我讓你在訂單系統的所有方法前後都新增計時功能和日誌功能怎麼辦?而且我可能還想僅針對對某些類中的某些方法執行前後新增計時功能和日誌功能,這該怎麼辦?我還想......”。

招財趕緊打斷了陀螺,“......師傅,趕緊打住!您一旦這麼問,就說明我目前的實現指定是滿足不了您的需求了,等我先回去想想吧,目前的當務之急是趕緊跟金融公司提個pr,修復一下這個問題,別影響顧客下單。”

“我看你是怕影響你的年終獎”,陀螺嗔怪道。

“哈哈哈哈哈哈,不說了不說了,我pr去了”,招財趕緊一溜煙跑沒了影。

很快,金融公司修復了這個問題,訂單系統穩定如初。

相關文章