程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 更多編程語言 >> Groovy >> 實戰Groovy - @Delegate注釋

實戰Groovy - @Delegate注釋

編輯:Groovy

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 的實際應用。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved