### 歡迎訪問我的GitHub > 這裡分類和彙總了欣宸的全部原創(含配套源碼):[https://github.com/zq2599/blog_demos](https://github.com/zq2599/blog_demos) ### 本篇概覽 - 本篇是《quarkus依賴註入》系列的第 ...
歡迎訪問我的GitHub
這裡分類和彙總了欣宸的全部原創(含配套源碼):https://github.com/zq2599/blog_demos
本篇概覽
- 本篇是《quarkus依賴註入》系列的第八篇,目標是掌握quarkus實現的一個CDI特性:裝飾器(Decorator)
- 提到裝飾器,熟悉設計模式的讀者應該會想到裝飾器模式,個人覺得下麵這幅圖很好的解釋了裝飾器模式,左下角的紅框是關鍵點:自己的send方法中,先調用父類的send(也就是被裝飾類的send),然後才是自己的業務邏輯
- quarkus也支持裝飾器模式,通過註解Decorator和Delegate實現,今天咱們就通過實戰掌握如何在quarks框架下通過裝飾器擴展應用
- quarkus是按照CDI的標準來支持裝飾器模式的,下圖來自官方文檔
- 接下來進入實戰環節
實戰功能說明
- 網上講述裝飾器模式的文章中,有個咖啡價格的例子非常經典,如下圖所示:
- 一杯意式濃縮咖啡(Espresso)價格3美元
- 拿鐵(Latte)由意式濃縮+牛奶組成,價格是意式濃縮和牛奶之和,即5美元
- 焦糖瑪奇朵(CaramelMacchiato)由拿鐵+焦糖組成,價格比拿鐵多了焦糖的1美元,即6美元
- 每種咖啡都是一種對象,價格由getPrice方法返回
-
在上述場景中,當咖啡的內容不斷豐富,咖啡價格也要做相應調整,裝飾器的作用是讓代碼優雅的應對變化,對內代碼整潔低耦合,對外保持統一介面getPrice
-
裝飾器模式本身並不是本篇的重點,咱們還是聚焦quarkus下的裝飾器功能:在咖啡價格的基礎上,通過裝飾器計算出拿鐵的價格
-
接下來開始編碼
編碼實戰
- 首先定義介面Coffee.java,不論是意式濃縮、拿鐵、還是其他種類,對外都稱之為Coffee,都有getPrice方法
package com.bolingcavalry.decorator;
public interface Coffee {
/**
* 咖啡名稱
* @return
*/
String name();
/**
* 當前咖啡的價格
* @return
*/
int getPrice();
}
- 然後是最基礎的意式濃縮咖啡,非常簡單的一個bean,定價3美元,這裡有個細節要註意:name方法中寫死了字元串Espresso,而沒用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;
}
}
- 上述代碼有以下幾處要註意
- 先明確目的:我們設計Latte這個bean,本意是通過裝飾器模式來裝飾Espresso,因此才會用到quarkus的裝飾器功能
- 使用quarkus的裝飾器功能時,有兩件事必須要做:裝飾類要用註解Decorator修飾,被裝飾類要用註解Delegate修飾
- 因此,Latte被註解Decorator修飾,Latte的成員變數delegate是被裝飾類,要用註解Delegate修飾,
- Latte的成員變數delegate並未指明是Espresso,quarkus會選擇Espresso的bean註入到這裡
- 在getPrice方法中列印出delegate.name方法的返回值,驗證delegate的身份,以確認quarkus註入的是否正確
- 註解Priority很重要,留在接下來的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的裝飾器
- 要重點關註的是成員變數delegate,其類型、名稱、註解,都和Latte的delegate一模一樣:
@Delegate
@Inject
Coffee delegate;
重要知識點
- 看到這裡,相信您也發現了問題所在:CaramelMacchiato和Latte都有成員變數delegate,其註解和類型聲明都一模一樣,那麼,如何才能保證Latte的delegate註入的是Espresso,而CaramelMacchiato的delegate註入的是Latte呢?
- 此刻就是註解Priority在發揮作用了,CaramelMacchiato和Latte都有註解Priority修飾,屬性值卻不同,屬性值越大越接近原始類Espresso,如下圖,所以,Latte裝飾的就是Espresso,CaramelMacchiato裝飾的是Latte
單元測試類
- 最後是單元測試類,成員變數的類型是Coffee,也就是說quarkus容器會自動註入裝飾過的CaramelMacchiato類型的bean,而testDecoratorPrice方法中斷言coffee.getPrice()的值等於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類型,都按照咱們的預期準確註入了
-
緊接著再做個嘗試:將Latte的註解Priority的屬性值改小,小於CaramelMacchiato的10,如下圖紅框,如此一來,CaramelMacchiato的優先順序更大,因此更靠近Espresso,由它去裝飾Espresso,Latte離Espresso更遠,所以它裝飾的是CaramelMacchiato
- 再次運行單元測試,如下圖,首先測試依舊能通過,這個好理解,無論裝飾邏輯怎麼變,最終的bean的getPrice返回值,都是意式濃縮+牛奶+焦糖的價格之和,然後在看右側日誌信息,果然,CaramelMacchiato註入的成員變數是Espresso,Latte註入的成員變數是CaramelMacchiato
- 至此,裝飾器的編碼實戰已完成,相信您可以在應用中用熟練使用裝飾器來擴展bean能力,並且保持與原有bean之間的代碼低耦合
與攔截器的不同
- 如果您看過《攔截器》一文,應該會發現,同樣的功能用攔截器也能實現,那為何還要多出個裝飾器呢?
- 其實網上也有類似的討論,首先是Stack Overflow上分析,一個高贊的觀點是:通常情況下,一個裝飾器被用於一個特定類上,而攔截器用於攔截多個類
- 這篇2012年的關於CDI的文章《Interceptors and Decorators tutorial》中的對比更好理解:
- 個人理解:
- 攔截器適合做一些通用的事情,例如日誌、異常處理等,可以為多個bean服務
- 裝飾器適合做特定的事情,例如本篇的演示代碼中,計算價格是被裝飾類的特性,其他bean沒有這個功能,所以裝飾器也只能用在,作為核心功能的增強或者完善