# 實戰 Groovy: @Delegate 注釋
_探索靜態類型語言中的 duck 類型的極限_
Scott Davis 將繼續有關 Groovy 元編程的討論,這一次他將深入研究 `@Delegate` 注釋,@Delegate 注釋模糊了數據類型和行為以及靜態和動態類型之間的區別。
在過去幾期 [_實戰 Groovy_](http://www.ibm.com/developerworks/cn/java/j-pg/) 文章中,您已經了解了閉包和元編程之類的 Groovy 語言特性如何將動態功能添加到 Java? 開發中。本文提供了更多這方面的內容。您將看到 `@Delegate` 注釋如何演變自 `ExpandoMetaClass` 使用的 `delegate`。您將再一次領略到 Groovy 的動態功能如何使它成為單元測試的理想語言。
在 “[使用閉包、ExpandoMetaClass 和類別進行元編程](http://www.ibm.com/developerworks/cn/java/j-pg06239.html)” 一文中,您了解了 `delegate` 的概念。當將一個 `shout()` 方法添加到 `java.lang.String` 的 `ExpandoMetaClass` 中時,您使用 `delegate` 來表示兩個類之間的關系,如清單 1 所示:
##### 清單 1\. 使用 `delegate` 訪問 `String.toUpperCase()`
```
String.metaClass.shout = {->
return delegate.toUpperCase()
}
println "Hello MetaProgramming".shout()
//output
HELLO METAPROGRAMMING
```
您不能表示為 `this.toUpperCase()`,因為 `ExpandoMetaClass` 并未包含 `toUpperCase()` 方法。類似地,也不能表示為 `super.toUpperCase()`,因為 `ExpandoMetaClass` 沒有擴展 `String`。(事實上,它不可能擴展 `String`,因為 `String` 是一個 `final` 類)。Java 語言并不具備用于表示這兩個類之間的共生關系的詞匯。這就是為什么 Groovy 要引入 `delegate` 概念。
## 關于本系列
Groovy 是在 Java 平臺上運行的一種現代編程語言。它能夠與現有 Java 代碼無縫集成,同時引入了各種生動的新特性,比如閉包和元編程。簡單來講,Groovy 是 Java 語言的 21 世紀版本。
將任何新工具整合到開發工具包中的關鍵是知道何時使用它以及何時將它留在工具包中。Groovy 的功能可以非常強大,但唯一的條件是正確應用于適當的場景。因此,[_實戰 Groovy_](http://www.ibm.com/developerworks/cn/java/j-pg/) 系列將探究 Groovy 的實際應用,以便幫助您了解何時以及如何成功使用它們。
在 Groovy 1.6 中,`@Delegate` 注釋被添加到該語言中。(從 [參考資料](#resources) 部分可以獲得添加到 Groovy 1.6 中的所有新注釋的列表)。該注釋允許您向_任意_ 類添加一個或多個委托 — 而不僅僅是 `ExpandoMetaClass`。
要充分地認識到 `@Delegate` 注釋的威力,考慮 Java 編程中一個常見但復雜的任務:在 `final` 類的基礎上創建一個新類。
## 復合模式和 `final` 類
假設您希望創建一個 `AllCapsString` 類,它具有 `java.lang.String` 的所有行為,唯一的不同是 — 正如名稱暗示的那樣 — 值始終以大寫的形式返回。`String` 是一個 `final` 類 — Java 演化到盡頭的產物。清單 2 證明您無法直接擴展 `String`:
##### 清單 2\. 擴展 `final` 類是不可能的
```
class AllCapsString extends String{
}
$ groovyc AllCapsString.groovy
org.codehaus.groovy.control.MultipleCompilationErrorsException:
startup failed, AllCapsString.groovy: 1: You are not allowed to
overwrite the final class 'java.lang.String'.
@ line 1, column 1.
class AllCapsString extends String{
^
1 error
```
這段代碼無效,因此您的下一個最佳選擇就是使用符合模式,如清單 3 所示(有關復合模式的更多信息,請參見 [參考資料](#resources)):
##### 清單 3\. 對 `String` 類的新類型使用復合模式
```
class AllCapsString{
final String body
AllCapsString(String body){
this.body = body.toUpperCase()
}
String toString(){
body
}
//now implement all 72 String methods
char charAt(int index){
return body.charAt(index)
}
//snip...
//one method down, 71 more to go...
}
```
因此,`AllCapsString` 類_擁有_ 一個 `String`,但是其_行為_ 不同于 `String`,除非您映射了所有 72 個 `String` 方法。要查看需要添加的方法,可以參考 Javadocs 中有關 `String` 的內容,或者運行清單 4 中的代碼:
##### 清單 4\. 輸出 `String` 類的所有方法
```
String.class.methods.eachWithIndex{method, i->
println "${i} ${method}"
}
//output
0 public boolean java.lang.String.contentEquals(java.lang.CharSequence)
1 public boolean java.lang.String.contentEquals(java.lang.StringBuffer)
2 public boolean java.lang.String.contains(java.lang.CharSequence)
...
```
將 72 個 `String` 方法手動添加到 `AllCapsString` 并不是一種明智的方法,而是在浪費開發人員的寶貴時間。這就是 `@Delegate` 注釋發揮作用的時候了。
* * *
## 了解 `@Delegate`
`@Delegate` 是一個編譯時注釋,指導編譯器將所有 delegate 的方法和接口推到外部類中。
在將 `@Delegate` 注釋添加到 `body` 之前,編譯 `AllCapsString` 并使用 `javap` 進行檢驗,看看大部分 `String` 方法是否缺失,如清單 5 所示:
##### 清單 5\. 在使用 `@Delegate` 前使用 `AllCapsString`
```
$ groovyc AllCapsString.groovy
$ javap AllCapsString
Compiled from "AllCapsString.groovy"
public class AllCapsString extends java.lang.Object
implements groovy.lang.GroovyObject{
public AllCapsString(java.lang.String);
public java.lang.String toString();
public final java.lang.String getBody();
//snip...
```
現在,將 `@Delegate` 注釋添加到 `body`,如清單 6 所示。重復 `groovyc` 和 `javap` 命令,將看到 `AllCapsString` 具有與 `java.lang.String` 相同的所有方法和接口。
##### 清單 6\. 使用 `@Delegate` 注釋將 `String` 的所有方法推到周圍的類中
```
class AllCapsString{
@Delegate final String body
AllCapsString(String body){
this.body = body.toUpperCase()
}
String toString(){
body
}
}
$ groovyc AllCapsString.groovy
$ javap AllCapsString
Compiled from "AllCapsString.groovy"
public class AllCapsString extends java.lang.Object
implements java.lang.CharSequence, java.lang.Comparable,
java.io.Serializable,groovy.lang.GroovyObject{
//NOTE: AllCapsString methods:
public AllCapsString(java.lang.String);
public java.lang.String toString();
public final java.lang.String getBody();
//NOTE: java.lang.String methods:
public boolean contains(java.lang.CharSequence);
public int compareTo(java.lang.Object);
public java.lang.String toUpperCase();
//snip...
```
然而,注意,您仍然可以調用 `getBody()`,從而繞過被推入到環繞的 `AllCapsString` 類中的所有方法。通過將 `private` 添加到字段聲明中 — `@Delegate final private String body` — 可以禁止顯示普通的 getter/setter 方法。這將完成轉換:`AllCapsString` 提供了 `String` 的全部行為,允許您根據情況覆蓋 `String` 方法。
* * *
## 在靜態語言中使用 duck 類型的限制
盡管 `AllCapsString` 目前擁有 `String` 的所有行為,但是它仍然不是一個_真正的_`String`。在 Java 代碼中,無法使用 `AllCapsString` 作為 `String` 的臨時替代,因為它并不是一個真正的 duck — 它只不過是冒充的。(動態語言被認為是使用 _duck_ 類型;Java 語言使用_靜態_ 類型。參見 [參考資料](#resources) 獲得更多與此有關的差異)。換句話說,由于 `AllCapsString` 并未真正擴展 `String`(或實現并不存在的 `Stringable` 接口),因此無法在 Java 代碼中與 `String` 互相替換。清單 7 展示了在 Java 語言中將 `AllCapsString` 轉換為 `String` 的失敗例子:
##### 清單 7\. Java 語言中的靜態類型阻止 `AllCapsString` 與 `String` 之間互相替換
```
public class JavaExample{
public static void main(String[] args){
String s = new AllCapsString("Hello");
}
}
$ javac JavaExample.java
JavaExample.java:5: incompatible types
found : AllCapsString
required: java.lang.String
String s = new AllCapsString("Hello");
^
1 error
```
因此,通過允許您擴展被最初的開發人員明確禁止擴展的類,Groovy 的 `@Delegate` 并沒有真正破壞 Java 的 `final` 關鍵字,但是您仍然可以獲得與在不越界的情況下相同程度的威力。
請記住,您的類可以擁有多個 `delegate`。假設您希望創建一個 `RemoteFile` 類,它將同時具有 `java.io.File` 和 `java.net.URL` 的特征。Java 語言并不支持多重繼承,但是您可以非常接近一對 `@Delegate`,如清單 8 所示。`RemoteFile` 類不是 `File` 也不是 `URL`,但是它卻具有兩者的行為。
##### 清單 8\. 多個 `@Delegate` 提供了多重繼承的行為
```
class RemoteFile{
@Delegate File file
@Delegate URL url
}
```
如果 `@Delegate` 只能修改類的行為 — 而不是類型 — 這是否意味著對 Java 開發人員毫無價值?未必,即使是 Java 之類的靜態類型語言也為 duck 類型提供了一種有限的形式,稱為_多態_。
### 具有多態性的 duck
多態 — 該詞源于希臘,用于描述 “多種形狀” — 意味著只要一組類通過實現相同接口顯式地共享相同的行為,它們就可以互相替換著使用。換句話說,如果定義了一個 `Duck` 類型的變量(假設 `Duck` 是一個正式定義 `quack()` 和 `waddle()` 方法的接口),那么可以將 `new Mallard()`、`new GreenWingedTeal()` 或者(我最喜愛的)`new PekingWithHoisinSauce()` 分配給它。
通過將 delegate 類的方法和接口全部提升到其他類,`@Delegate` 注釋為多態提供了完整的支持。這意味著如果 delegate 類實現了接口,您又回到了為它創建一個臨時替代這件事上來。
* * *
## `@Delegate` 和 `List` 接口
假設您希望創建一個名為 `FixedList` 的新類。它的行為應該類似 `java.util.ArrayList`,但是有一個重要的區別:您應當能夠為可以添加到其中的元素的數量定義一個上限。這允許您創建一個 `sportsCar` 變量,該變量可以容納兩個乘客,但是不能比這再多了,`restaurantTable` 可以容納 4 個用餐者,但是同樣不能超過這個數字,以此類推。
`ArrayList` 類實現 `List` 接口。它為您提供了兩個選項。您也可以讓您的 `FixedList` 類實現 `List` 接口,但是您需要面對一項煩人的工作:為所有 `List` 方法提供一個實現。由于 `ArrayList` 并不是 `final` 類,另一個選擇就是讓 `FixedList` 擴展 `ArrayList`。這是一個非常有效的做法,但是如果(假設)`ArrayList` 被聲明為 `final`,`@Delegate` 注釋將提供第三個選擇:通過將 `ArrayList` 作為 `FixedList` 的委托,您可以獲得 `ArrayList` 的所有行為,同時自動實現 `List` 接口。
首先,使用一個 `ArrayList` 委托創建 `FixedList` 類,如清單 9 所示。`groovyc` / `javap` 是否可以檢驗 `FixedList` 不僅提供了與 `ArrayList` 相同的方法,還提供了相同的接口。
##### 清單 9\. 第一步創建 `FixedList` 類
```
class FixedList{
@Delegate private List list = new ArrayList()
final int sizeLimit
/**
* NOTE: This constructor limits the max size of the list,
* not just the initial capacity like an ArrayList.
*/
FixedList(int sizeLimit){
this.sizeLimit = sizeLimit
}
}
$ groovyc FixedList.groovy
$ javap FixedList
Compiled from "FixedList.groovy"
public class FixedList extends java.lang.Object
implements java.util.List,java.lang.Iterable,
java.util.Collection,groovy.lang.GroovyObject{
public FixedList(int);
public java.lang.Object[] toArray(java.lang.Object[]);
//snip..
```
目前我們還沒有對 `FixedList` 的大小做任何限制,但這是一個很好的開始。如何確定 `FixedList` 的大小此時并不是_固定的_?您可以編寫一些用后即扔的樣例代碼,但是如果 `FixedList` 將投入到生產中,您最好立即為其編寫一些測試用例。
* * *
## 使用 `GroovyTestCase` 測試 `@Delegate`
要開始測試 `@Delegate`,編寫一個單元測試,驗證您可以將比您實際可添加的更多元素添加到 `FixedList`。清單 10 展示了這樣一個測試:
##### 清單 10\. 首先編寫一個失敗的測試
```
class FixedListTest extends GroovyTestCase{
void testAdd(){
List threeStooges = new FixedList(3)
threeStooges.add("Moe")
threeStooges.add("Larry")
threeStooges.add("Curly")
threeStooges.add("Shemp")
assertEquals threeStooges.sizeLimit, threeStooges.size()
}
}
$ groovy FixedListTest.groovy
There was 1 failure:
1) testAdd(FixedListTest)junit.framework.AssertionFailedError:
expected:<3> but was:<4>
```
似乎 `add()` 方法應當在 `FixedList` 中被重寫,如清單 11 所示。重新運行這些測試仍然失敗,但是這一次是因為拋出了異常。
##### 清單 11\. 重寫 `ArrayList` 的 `add()` 方法
```
class FixedList{
@Delegate private List list = new ArrayList()
final int sizeLimit
//snip...
boolean add(Object element){
if(list.size() < sizeLimit){
return list.add(element)
}else{
throw new UnsupportedOperationException("Error adding ${element}:" +
" the size of this FixedList is limited to ${sizeLimit}.")
}
}
}
$ groovy FixedListTest.groovy
There was 1 error:
1) testAdd(FixedListTest)java.lang.UnsupportedOperationException:
Error adding Shemp: the size of this FixedList is limited to 3.
```
由于使用了 `GroovyTestCase` 的方便的 `shouldFail` 方法,您可以捕捉到這個預期的異常,如清單 12 所示,這一次您終于成功運行了測試:
##### 清單 12\. `shouldFail()` 方法捕捉到預期的異常
```
class FixedListTest extends GroovyTestCase{
void testAdd(){
List threeStooges = new FixedList(3)
threeStooges.add("Moe")
threeStooges.add("Larry")
threeStooges.add("Curly")
assertEquals threeStooges.sizeLimit, threeStooges.size()
shouldFail(java.lang.UnsupportedOperationException){
threeStooges.add("Shemp")
}
}
}
```
* * *
## 測試操作符重載
在 “[美妙的操作符](http://www.ibm.com/developerworks/cn/java/j-pg10255.html)” 中,您了解到 Groovy 支持_操作符重載_。對于 `List`,可以使用 `<<` 添加元素以及傳統的 `add()` 方法。編寫如清單 13 所示的快速單元測試,確定使用 `<<` 不會意外破壞 `FixedList`:
##### 清單 13\. 測試操作員重載
```
class FixedListTest extends GroovyTestCase{
void testOperatorOverloading(){
List oneList = new FixedList(1)
oneList << "one"
shouldFail(java.lang.UnsupportedOperationException){
oneList << "two"
}
}
}
```
這次測試的成功應該能夠讓您感到輕松一些。
您還可以測試出錯的情況。比如,清單 14 測試了在創建包含一個負數元素的 `FixedList` 時出現的情況:
##### 清單 14\. 測試極端情況
```
class FixedListTest extends GroovyTestCase{
void testNegativeSize(){
List badList = new FixedList(-1)
shouldFail(java.lang.UnsupportedOperationException){
badList << "will this work?"
}
}
}
```
* * *
## 測試將一個元素插入到列表中間的情況
現在,您已經確信這個簡單的重寫過的 `add()` 方法可以正常工作,下一步是實現重載的 `add()` 方法,可以獲取索引以及元素,如清單 15 所示:
##### 清單 15\. 使用索引添加元素
```
class FixedList{
@Delegate private List list = new ArrayList()
final int sizeLimit
void add(int index, Object element){
list.add(index, element)
trimToSize()
}
private void trimToSize(){
if(list.size() > sizeLimit){
(sizeLimit..<list.size()).each{
list.pop()
}
}
}
}
```
注意,您可以(也應該)在任何可能的情況下使用 delegate 自帶的功能 — 畢竟,這正是您優先選擇 delegate 的原因。在這種情況下,您將讓 `ArrayList` 執行添加操作,并去掉任何超出 `FixedList` 的大小的元素。(這個 `add()` 方法是否應該像另一個 `add()` 方法那樣拋出一個 `UnsupportedOperationException`,您可以自己做出這個設計決策)。
`trimToSize()` 方法包含了一些值得關注的語法糖。首先,`pop()` 方法是由 Groovy 元編程到所有 `List` 中的內容。它刪除了 `List` 中的最后一個元素,使用后進先出(last-in first-out,LIFO)的方式。
接下來,注意 `each` 循環中使用了一個 Groovy `range`。使用實數替換變量可能有助于使這一行為更加清晰。假設 `FixedList` 的 `sizeLimit` 的值為 `3`,并且在添加了新元素后,它的 `size()` 的值為 `5`。那么這個范圍看上去應當類似于 `(3..5).each{}`。但是 `List` 使用的是基于 0 的標記法,因此列表中的元素不會擁有值為 `5` 的索引。通過指定 `(3..<5).each{}`,您將 5 排除到了這個范圍之外。
編寫兩個測試,如清單 16 所示,檢驗新的重載后的 `add()` 方法是否如期望的那樣運行:
##### 清單 16\. 測試將元素添加到 `FixedList` 中的情況
```
class FixedListTest extends GroovyTestCase{
void testAddWithIndex(){
List threeStooges = new FixedList(3)
threeStooges.add("Moe")
threeStooges.add("Larry")
threeStooges.add("Curly")
threeStooges.add(2,"Shemp")
assertEquals 3, threeStooges.size()
assertFalse threeStooges.contains("Curly")
}
void testAddWithIndexOnALessThanFullList(){
List threeStooges = new FixedList(3)
threeStooges.add("Curly")
assertEquals 1, threeStooges.size()
threeStooges.add(0, "Larry")
assertEquals 2, threeStooges.size()
assertEquals "Larry", threeStooges[0]
threeStooges.add(0, "Moe")
assertEquals 3, threeStooges.size()
assertEquals "Moe", threeStooges[0]
assertEquals "Larry", threeStooges[1]
assertEquals "Curly", threeStooges[2]
}
}
```
您是否注意到編寫的測試代碼的數量要多于生產代碼?很好!我想說的是,對于每一段生產代碼,您應當編寫至少兩倍數量的測試代碼。
* * *
## 實現 `addAll()` 方法
要實現 `FixedList` 類,重寫 `ArrayList` 中的 `addAll()` 方法,如清單 17 所示:
##### 清單 17\. 實現 `addAll()` 方法
```
class FixedList{
@Delegate private List list = new ArrayList()
final int sizeLimit
boolean addAll(Collection collection){
def returnValue = list.addAll(collection)
trimToSize()
return returnValue
}
boolean addAll(int index, Collection collection){
def returnValue = list.addAll(index, collection)
trimToSize()
return returnValue
}
}
```
現在編寫相應的單元測試,如清單 18 所示:
##### 清單 18\. 測試 `addAll()` 方法
```
class FixedListTest extends GroovyTestCase{
void testAddAll(){
def quartet = ["John", "Paul", "George", "Ringo"]
def trio = new FixedList(3)
trio.addAll(quartet)
assertEquals 3, trio.size()
assertFalse trio.contains("Ringo")
}
void testAddAllWithIndex(){
def quartet = new FixedList(4)
quartet << "John"
quartet << "Ringo"
quartet.addAll(1, ["Paul", "George"])
assertEquals "John", quartet[0]
assertEquals "Paul", quartet[1]
assertEquals "George", quartet[2]
assertEquals "Ringo", quartet[3]
}
}
```
您現在完成了全部工作。感謝 `@Delegate` 注釋的強大威力,我們只使用大約 50 代碼就創建了 `FixedList` 類。感謝 `GroovyTestCase` 使我們能夠測試代碼,從而允許您將其放入到生產環境中,并且確信它可以按照期望的那樣操作。清單 19 展示了完整的 `FixedList` 類:
##### 清單 19\. 完整的 `FixedList` 類
```
class FixedList{
@Delegate private List list = new ArrayList()
final int sizeLimit
/**
* NOTE: This constructor limits the max size of the list,
* not just the initial capacity like an ArrayList.
*/
FixedList(int sizeLimit){
this.sizeLimit = sizeLimit
}
boolean add(Object element){
if(list.size() < sizeLimit){
return list.add(element)
}else{
throw new UnsupportedOperationException("Error adding ${element}:" +
" the size of this FixedList is limited to ${sizeLimit}.")
}
}
void add(int index, Object element){
list.add(index, element)
trimToSize()
}
private void trimToSize(){
if(list.size() > sizeLimit){
(sizeLimit..<list.size()).each{
list.pop()
}
}
}
boolean addAll(Collection collection){
def returnValue = list.addAll(collection)
trimToSize()
return returnValue
}
boolean addAll(int index, Collection collection){
def returnValue = list.addAll(index, collection)
trimToSize()
return returnValue
}
String toString(){
return "FixedList size: ${sizeLimit}\n" + "${list}"
}
}
```
* * *
## 結束語
通過將新的_行為_ 添加到類中而不是轉換其_類型_,Groovy 的元編程功能實現了一組全新的動態可能性,同時不會違背 Java 語言的靜態類型系統的規則。通過使用 `ExpandoMetaClass`(讓您能夠通過執行映射將任何新方法添加到現有類)和 `@Delegate`(讓您能夠通過外部包裝類公開復合內部類的功能),Groovy 讓 JVM 煥發新光彩。
在下一期文章中,我將演示一個得益于 Groovy 的靈活語法 Swing 而重新煥發生機的舊有技術。是的,Swing 的復雜性因為 Groovy 的 `SwingBuilder` 而消失。這使得桌面開發變得更加有趣和簡單。到那時,希望您能夠發現大量有關 Groovy 的實際應用。
* * *
## 下載
| 描述 | 名字 | 大小 |
| --- | --- | --- |
| 本文示例的源代碼 | [j-pg08259.zip](http://www.ibm.com/developerworks/apps/download/index.jsp?contentid=430289&filename=j-pg08259.zip&method=http&locale=zh_CN) | 5KB |
- 實戰 Groovy
- 實戰 Groovy: SwingBuilder 和 Twitter API,第 2 部分
- 實戰 Groovy: SwingBuilder 和 Twitter API,第 1 部分
- 實戰 Groovy: @Delegate 注釋
- 實戰 Groovy: 使用閉包、ExpandoMetaClass 和類別進行元編程
- 實戰 Groovy: 構建和解析 XML
- 實戰 Groovy: for each 剖析
- 實戰 Groovy: Groovy:Java 程序員的 DSL
- 實戰 Groovy: 關于 MOP 和迷你語言
- 實戰 Groovy: 用 curry 過的閉包進行函數式編程
- 實戰 Groovy: Groovy 的騰飛
- 實戰 Groovy: 在 Java 應用程序中加一些 Groovy 進來
- 實戰 Groovy: 用 Groovy 生成器作標記
- 實戰 Groovy: 用 Groovy 打造服務器端
- 實戰 Groovy: 使用 Groovy 模板進行 MVC 編程
- 實戰 Groovy: 用 Groovy 進行 JDBC 編程
- 實戰 Groovy: 用 Groovy 進行 Ant 腳本編程
- 實戰 Groovy: 用 Groovy 更迅速地對 Java 代碼進行單元測試
- alt.lang.jre: 感受 Groovy