quarkus依賴注入之八:裝飾器(Decorator)

發表於2023-09-20

歡迎訪問我的GitHub

這裡分類和彙總了欣宸的全部原創(含配套原始碼):https://github.com/zq2599/blog_demos

本篇概覽

  • 本篇是《quarkus依賴注入》系列的第八篇,目標是掌握quarkus實現的一個CDI特性:裝飾器(Decorator)
  • 提到裝飾器,熟悉設計模式的讀者應該會想到裝飾器模式,個人覺得下面這幅圖很好的解釋了裝飾器模式,左下角的紅框是關鍵點:自己的send方法中,先呼叫父類的send(也就是被裝飾類的send),然後才是自己的業務邏輯

<img src="https://typora-pictures-1253575040.cos.ap-guangzhou.myqcloud.com/54.png" alt="54" style="zoom:50%;" />

  • quarkus也支援裝飾器模式,透過註解DecoratorDelegate實現,今天我們們就透過實戰掌握如何在quarks框架下透過裝飾器擴充套件應用
  • quarkus是按照CDI的標準來支援裝飾器模式的,下圖來自官方文件

image-20220409210857612

  • 接下來進入實戰環節

實戰功能說明

  • 網上講述裝飾器模式的文章中,有個咖啡價格的例子非常經典,如下圖所示:
  1. 一杯意式濃縮咖啡(Espresso)價格3美元
  2. 拿鐵(Latte)由意式濃縮+牛奶組成,價格是意式濃縮和牛奶之和,即5美元
  3. 焦糖瑪奇朵(CaramelMacchiato)由拿鐵+焦糖組成,價格比拿鐵多了焦糖的1美元,即6美元
  4. 每種咖啡都是一種物件,價格由<font color="blue">getPrice</font>方法返回

<img src="https://typora-pictures-1253575040.cos.ap-guangzhou.myqcloud.com/%E6%B5%81%E7%A8%8B%E5%9B%BE%20-%202022-04-09T165959.292.jpg" alt="流程圖 - 2022-04-09T165959.292" style="zoom:50%;" />

  • 在上述場景中,當咖啡的內容不斷豐富,咖啡價格也要做相應調整,裝飾器的作用是讓程式碼優雅的應對變化,對內程式碼整潔低耦合,對外保持統一介面<font color="blue">getPrice</font>
  • <font color="red">裝飾器模式</font>本身並不是本篇的重點,我們們還是聚焦quarkus下的裝飾器功能:在咖啡價格的基礎上,透過裝飾器計算出拿鐵的價格
  • 接下來開始編碼

編碼實戰

  • 首先定義介面Coffee.java,不論是意式濃縮、拿鐵、還是其他種類,對外都稱之為Coffee,都有getPrice方法
package com.bolingcavalry.decorator;

public interface Coffee {

    /**
     * 咖啡名稱
     * @return
     */
    String name();

    /**
     * 當前咖啡的價格
     * @return
     */
    int getPrice();
}
  • 然後是最基礎的意式濃縮咖啡,非常簡單的一個bean,定價3美元,這裡有個細節要注意:name方法中寫死了字串<font color="blue">Espresso</font>,而沒用getClass().getSimpleName(),這是因為在quarkus容器中,Espresso的bean並非Espresso型別,而是動態生成的代理類,所以getClass返回的類不是Espresso
package com.bolingcavalry.decorator.impl;

import com.bolingcavalry.decorator.Coffee;

import javax.enterprise.context.ApplicationScoped;

/**
 * 意式濃縮咖啡,價格3美元
 */
@ApplicationScoped
public class Espresso implements Coffee {

    @Override
    public String name() {
        return "Espresso";
    }

    @Override
    public int getPrice() {
        return 3;
    }
}
  • 接下來就是重點了,拿鐵,由意式濃縮+牛奶組成,程式碼如下,有幾處要注意的地方稍後會提到
package com.bolingcavalry.decorator.impl;

import com.bolingcavalry.decorator.Coffee;
import io.quarkus.arc.Priority;
import io.quarkus.logging.Log;

import javax.decorator.Decorator;
import javax.decorator.Delegate;
import javax.inject.Inject;

@Decorator
@Priority(11)
public class Latte implements Coffee {
    /**
     * 牛奶價格:2美元
     */
    private static final int MILK_PRICE = 2;

    @Delegate
    @Inject
    Coffee delegate;

    @Override
    public String name() {
        return "Latte";
    }

    @Override
    public int getPrice() {
        // 將Latte的代理類列印出來,看quarkus注入的是否正確
        Log.info("Latte's delegate type : " + this.delegate.name());
        return delegate.getPrice() + MILK_PRICE;
    }
}
  • 上述程式碼有以下幾處要注意
  1. 先明確目的:我們設計Latte這個bean,本意是透過裝飾器模式來裝飾Espresso,因此才會用到quarkus的裝飾器功能
  2. 使用quarkus的裝飾器功能時,有兩件事必須要做:裝飾類要用註解<font color="blue">Decorator</font>修飾,被裝飾類要用註解<font color="blue">Delegate</font>修飾
  3. 因此,Latte被註解<font color="blue">Decorator</font>修飾,Latte的成員變數delegate是被裝飾類,要用註解<font color="blue">Delegate</font>修飾,
  4. Latte的成員變數delegate並未指明是Espresso,quarkus會選擇Espresso的bean注入到這裡
  5. 在getPrice方法中列印出delegate.name方法的返回值,驗證delegate的身份,以確認quarkus注入的是否正確
  6. 註解<font color="blue">Priority</font>很重要,留在接下來的CaramelMacchiato類(焦糖瑪奇朵)寫完後再說清楚
  • 接下來是CaramelMacchiato類(焦糖瑪奇朵),有幾處要注意的地方稍後會說明
package com.bolingcavalry.decorator.impl;

import com.bolingcavalry.decorator.Coffee;
import io.quarkus.arc.Priority;
import io.quarkus.logging.Log;

import javax.decorator.Decorator;
import javax.decorator.Delegate;
import javax.inject.Inject;

/**
 * 焦糖瑪奇朵:拿鐵+焦糖
 */
@Decorator
@Priority(10)
public class CaramelMacchiato implements Coffee {

    /**
     * 焦糖價格:1美元
     */
    private static final int CARAMEL_PRICE = 1;

    @Delegate
    @Inject
    Coffee delegate;

    @Override
    public String name() {
        return "CaramelMacchiato";
    }

    @Override
    public int getPrice() {
        // 將CaramelMacchiato的代理類列印出來,看quarkus注入的是否正確
        Log.infov("CaramelMacchiato's delegate type : " + this.delegate.name());
        return delegate.getPrice() + CARAMEL_PRICE;
    }
}
  • CaramelMacchiato程式碼的邏輯和Latte的差不多,都用了註解Decorator和Delegate,目的是為了做Latte的裝飾器
  • 要<font color="red">重點關注</font>的是成員變數<font color="blue">delegate</font>,其型別、名稱、註解,都和Latte的delegate一模一樣:
@Delegate
@Inject
Coffee delegate;

重要知識點

  • 看到這裡,相信您也發現了問題所在:CaramelMacchiato和Latte都有成員變數delegate,其註解和型別宣告都一模一樣,那麼,如何才能保證Latte的delegate注入的是Espresso,而CaramelMacchiato的delegate注入的是Latte呢?
  • 此刻就是註解<font color="blue">Priority</font>在發揮作用了,CaramelMacchiato和Latte都有註解<font color="blue">Priority</font>修飾,屬性值卻不同,<font color="red">屬性值越大越接近原始類Espresso</font>,如下圖,所以,Latte裝飾的就是Espresso,CaramelMacchiato裝飾的是Latte

<img src="https://typora-pictures-1253575040.cos.ap-guangzhou.myqcloud.com/%E6%B5%81%E7%A8%8B%E5%9B%BE%20-%202022-04-09T203421.135.jpg" alt="流程圖 - 2022-04-09T203421.135" style="zoom:50%;" />

單元測試類

  • 最後是單元測試類,成員變數的型別是Coffee,也就是說quarkus容器會自動注入裝飾過的CaramelMacchiato型別的bean,而testDecoratorPrice方法中斷言<font color="red">coffee.getPrice()</font>的值等於6,如果注入caffee的bean不是CaramelMacchiato型別,斷言就會失敗
package com.bolingcavalry;

import com.bolingcavalry.decorator.Coffee;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import javax.inject.Inject;

@QuarkusTest
public class DecoratorTest {

    @Inject
    Coffee coffee;

    @Test
    public void testDecoratorPrice() {
        Assertions.assertEquals(6, coffee.getPrice());
    }
}

驗證

  • 執行單元測試,如下圖,單元測試透過表示coffee注入的是CaramelMacchiato型別的bean,再看右側的日誌,CaramelMacchiato的成員變數delegate是Latte型別,Latte的成員變數delegate是Espresso型別,都按照我們們的預期準確注入了
    67
  • 緊接著再做個嘗試:將Latte的註解Priority的屬性值改小,小於CaramelMacchiato的10,如下圖紅框,如此一來,CaramelMacchiato的優先順序更大,因此更靠近Espresso,由它去裝飾Espresso,Latte離Espresso更遠,所以它裝飾的是CaramelMacchiato

<img src="https://typora-pictures-1253575040.cos.ap-guangzhou.myqcloud.com/image-20220409205649353.png" alt="image-20220409205649353" style="zoom:50%;" />

  • 再次執行單元測試,如下圖,首先測試依舊能透過,這個好理解,無論裝飾邏輯怎麼變,最終的bean的getPrice返回值,都是意式濃縮+牛奶+焦糖的價格之和,然後在看右側日誌資訊,果然,CaramelMacchiato注入的成員變數是Espresso,Latte注入的成員變數是CaramelMacchiato

68

  • 至此,裝飾器的編碼實戰已完成,相信您可以在應用中用熟練使用裝飾器來擴充套件bean能力,並且保持與原有bean之間的程式碼低耦合

與攔截器的不同

  • 如果您看過《攔截器》一文,應該會發現,同樣的功能用攔截器也能實現,那為何還要多出個裝飾器呢?
  • 其實網上也有類似的討論,首先是Stack Overflow上分析,一個高讚的觀點是:通常情況下,一個裝飾器被用於一個特定類上,而攔截器用於攔截多個類
  • 這篇2012年的關於CDI的文章《Interceptors and Decorators tutorial》中的對比更好理解:

55

  • 個人理解:
  • 攔截器適合做一些通用的事情,例如日誌、異常處理等,可以為多個bean服務
  • 裝飾器適合做特定的事情,例如本篇的演示程式碼中,<font color="blue">計算價格</font>是被裝飾類的特性,其他bean沒有這個功能,所以裝飾器也只能用在,作為核心功能的增強或者完善

歡迎關注思否:程式設計師欣宸

學習路上,你不孤單,欣宸原創一路相伴...

相關文章