Curiously-Recurring Generic Pattern (CRGP) 是指說符合以下形式的generic寫法:
1 2 3 4 5
| class Generic<T> { } class Derived extends Generic<Derived> { }
|
其實前陣子在工作中有用過一次這個寫法,但這次才知道這個寫法有個專屬的名字。它之所以叫做curiously-recurring,是因為Generic的type parameter就是要繼承Generic的Derived class本身,這個形式看起來有點怪;也因為Derived class是把自己帶入為type parameter,所以就有recursive的味道。
那為什麼會用到這個寫法呢?以我自己最近遇到的問題來說,我需要為幾個API clients設計它們各自的Builder class,像是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| public class FooApiClient { public static class Builder { public Builder setX(String x) { return this; } public FooApiClient build() { return new FooApiClient(); } } } public class BarApiClient { public static class Builder { public Builder setX(String x) { return this; } public BarApiClient build() { return new BarApiClient(); } } }
|
所以當我要建立FooApiClient或BarApiClient時,就可以很直覺地透過它們的Builder建立:
1 2 3 4
| FooApiClient client = new FooApiClient.Builder() .setX("Hello") .build();
|
但實際上,這些Builders建立FooApiClient和BarApiClient所需的設定方式是差不多的,在這些ApiClient裡還保持著各自的Builder的話就違反了DRY原則。其中一個解決方法就是把FooApiClient和BarApiClient共用的邏輯和Builder classes都提取到一個BaseApiClient中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| class BaseApiClient { protected abstract static class Builder { public Builder setX(String x) { return this; } public BaseApiClient build() { return new BaseApiClient() } } } public class FooApiClient extends BaseApiClient { public static class Builder extends BaseApiClient.Builder { } }
|
不過一開始這樣寫會有兩個明顯的問題,第一是(1)所指的地方,setX()回傳的是BaseApiClient.Builder,所以當使用FooApiClient.Builder做鏈式呼叫時,最後就會呼叫到(2)所指的BaseApiClient.Builder#build(),因為它回傳的是BaseApiClient,還必須透過type casting才能真正當作FooApiClient使用。
接下來我們可以開始引入generic,來攜帶FooApiClient的型態訊息到BaseApiClient.Builder中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| class BaseApiClient { protected abstract static class Builder<T extends BaseApiClient, R extends Builder<T, R>> { @SupressWarnings("unchecked") public R setX(String x) { return (R) this; } public T build() { return buildApiClient() } protected abstract T buildApiClient(); } } public class FooApiClient extends BaseApiClient { public static class Builder extends BaseApiClient.Builder<FooApiClient, Builder> { @Override protected FooApiClient buildApiClient() { return new FooApiClient(); } } }
|
因為我們希望在執行build()時,能夠直接取得FooApiClient型別的回傳值,勢必需要能夠讓derived class把正確的型別訊息傳入BaseApiClient.Builder中,所以在此引入了一個type parameter T來記錄型別資訊。接下來,為了處理上述的問題(1),我們要能夠讓setX()回傳適當的Builder子類別,也就是FooApiClient.Builder呼叫setX()的時候,就要能得到FooApiClient.Builder型別的回傳值;BarApiClient.Builder呼叫setX()的時候,就要能得到BarApiClient.Builder型別的回傳值。故此,我們也需要把FooApiClient.Builder等子類別的型別傳入,這樣我們才有回傳正確型別的資訊。所以BaseApiClient.Builder應該要有兩個type parameters,而其中第二個type parameter R被限制為BaseApiClient.Build的子類別。為了保持型別資訊,R要完整地寫成:
1
| protected abstract static class Builder<T extends BaseApiClient, R extends Builder<T, R>>
|
至此,CRGP形式就出現了。根據這樣的邏輯,FooApiClient.Builder在宣告的時候必須要把FooApiClient和自身的型別訊息傳入base class的type parameters,所以也同樣呈現出了CRGP的形式:
1
| public static class Builder extends BaseApiClient.Builder<FooApiClient, Builder>
|
Reference
[1] Curiously Recurring Generic Pattern