Scott Davis 將繼續有關 Groovy 元編程的討論,這一次他將深入研究 @Delegate 注釋,@Delegate 注釋模糊了數據類型和行為以及靜態和動態類型之間的區別。
在過去幾期 實戰 Groovy 文章中,您已經了解了閉包和元編程之類的 Groovy 語言特性如何將動態功能添加到 Java™ 開發中。本文提供了更多這方面的內容。您將看到 @Delegate 注釋如何演變自 ExpandoMetaClass 使用的 delegate。您將再一次領略到 Groovy 的動態功能如何使它成為單元測試的理想語言。
在 “使用閉包、ExpandoMetaClass 和類別進行元編程” 一文中,您了解了 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 1.6 中,@Delegate 注釋被添加到該語言中。該注釋允許您向任意 類添加一個或多個委托 — 而不僅僅是 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 所示:
清單 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 語言使用靜態 類型。。換句話說,由於 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")
}
}
}
測試操作符重載
在 “美妙的操作符” 中,您了解到 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 的實際應用。