Curiously-Recurring Generic Pattern

2017-08-27

Curiously-Recurring Generic Pattern (CRGP) 是指說符合以下形式的generic寫法:

1
2
3
4
5
class Generic<T> {
}
class Derived extends Generic<Derived> { // CRGP
}

其實前陣子在工作中有用過一次這個寫法,但這次才知道這個寫法有個專屬的名字。它之所以叫做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
// FooApiClient.java
public class FooApiClient {
// FooApiClient implementation
public static class Builder {
public Builder setX(String x) {
// Implementation
return this;
}
public FooApiClient build() {
// Implementation
return new FooApiClient(/* parameters */);
}
}
}
// BarApiClient.java
public class BarApiClient {
// BarApiClient implementation
public static class Builder {
public Builder setX(String x) {
// Implementation
return this;
}
public BarApiClient build() {
// Implementation
return new BarApiClient(/* parameters */);
}
}
}

所以當我要建立FooApiClient或BarApiClient時,就可以很直覺地透過它們的Builder建立:

1
2
3
4
// Create FooApiClient
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 {
// Common implementation
protected abstract static class Builder {
public Builder setX(String x) { // —— (1)
// Implementation
return this;
}
public BaseApiClient build() { // —— (2)
// Implementation
return new BaseApiClient(/* parameters */)
}
}
}
public class FooApiClient extends BaseApiClient {
// Implementation
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 {
// Common implementation
protected abstract static class Builder<T extends BaseApiClient, R extends Builder<T, R>> {
@SupressWarnings("unchecked")
public R setX(String x) { // —— (1)
// Implementation
return (R) this;
}
public T build() { // —— (2)
// Implementation
return buildApiClient(/* parameters */)
}
protected abstract T buildApiClient(/* parameters */);
}
}
public class FooApiClient extends BaseApiClient {
// Implementation
public static class Builder extends BaseApiClient.Builder<FooApiClient, Builder> {
@Override
protected FooApiClient buildApiClient(/* parameters */) {
return new FooApiClient(/* parameters */);
}
}
}

因為我們希望在執行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