レベル: 中級 Scott Davis, Founder, ThirstyHead.com
2009年 8月 25日 この記事では、著者の Scott Davis が Groovy のメタプログラミングについての説明を続け、@Delegate アノテーションを詳細に掘り下げます。@Delegate アノテーションを使うと、データの型と振る舞い、そして静的型付けと動的型付けの違いがあいまいになります。
この「実用的な Groovy」シリーズの過去数回の記事では、Groovy 言語でのクロージャーやメタプログラミングなどの機能によって、どのようにして Java™ 開発に新しい動的機能が追加されるのかを説明しました。今回の記事も内容が似ており、@Delegate アノテーションが、ExpandoMetaClass で使用される delegate のバリエーションであることを学びます。また、Groovy の持つ動的機能のおかげで Groovy がユニット・テストに最適な言語であることを (今回もまた) 学びます。
「実用的な Groovy: クロージャー、ExpandoMetaClass、そしてカテゴリーによるメタプログラミング」では delegate の概念を紹介しました。java.lang.String の ExpandoMetaClass に shout() メソッドを追加する場合には、2 つのクラスの間の関係を delegate を使って表現します (リスト 1)。
リスト 1. delegate を使って String.toUpperCase() にアクセスするdelegate to access String.toUpperCase()
String.metaClass.shout = {->
return delegate.toUpperCase()
}
println "Hello MetaProgramming".shout()
//output
HELLO METAPROGRAMMING
|
this.toUpperCase() と表現することはできません。ExpandoMetaClass には toUpperCase() メソッドがないからです。同様に、ExpandoMetaClass は String を継承しないため、super.toUpperCase() と表現することはできません (実際、String は final クラスなので、String を継承することは絶対にできません)。Java 言語には 2 つのクラスの共生関係を表現するための語彙がありません。そのため、Groovy では delegate の概念が導入されています。
 | このシリーズについて
Groovy は Java プラットフォーム上で実行される最新のプログラミング言語の 1 つです。Groovy は既存の Java コードとシームレスに統合できる一方、クロージャーやメタプログラミングなどの強力な新機能も導入することができます。簡単に言えば Groovy とは、21 世紀に Java 言語が作成されていたら Groovy のようになっていたであろう、そういった言語なのです。
開発ツールキットの一部として新しいツールを採用する際に重要なことは、どういう場合にそのツールを使い、どういう場合には使わずにおくかを理解することです。Groovy は非常に強力ですが、適切な方法で、適切な状況の中で使用した場合にのみ強力なツールとなるのです。そのため「実用的な Groovy」シリーズでは、どういう状況で、どのようにして Groovy を使えば効果的であるかを学べるように、Groovy の実用的な使い方を解説します。
|
|
Groovy 1.6 では Groovy 言語に @Delegate アノテーションが追加されています (Groovy 1.6 で追加された新しいアノテーションの一覧に関しては、「参考文献」を参照してください)。@Delegate アノテーションを使うことによって、(ExpandoMetaClass だけではなく) 任意のクラスに 1 つまたは複数の delegate を追加することができるのです。
@Delegate アノテーションの強力さを十分に理解するために、Java プログラミングにありがちな困難な状況、例えば final クラスをベースに新しいクラスを作成する場合を考えてみましょう。
Composite パターンと final クラス
例えば AllCapsString クラスを作成したい場合を考えてみてください。この 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
|
これはうまくいきませんでした。そこで 2 番目に最適な選択肢は、リスト 3 のように Composite パターンを使う方法です (Composite パターンの詳細については「参考文献」を参照)。
リスト 3. String クラスの新しい型に Composite パターンを使う
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 と同じにはなりません。どのメソッドを追加する必要があるかを調べるためには、String の Javadoc を参照するか、あるいはリスト 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)
...
|
String の 72 個のメソッドを手動で AllCapsString に追加するという方法は、開発者の貴重な時間の使い方として賢明ではありません。こうした場合に @Delegate アノテーションが便利なのです。
@Delegate について理解する
@Delegate はコンパイル時に機能するアノテーションであり、delegate のすべてのメソッドとインターフェースを外部のクラスに追加するように、コンパイラーに指示します。
@Delegate アノテーションを body に追加する前に、AllCapsString をコンパイルし、実際に String のメソッドの大部分がないことを javap を使って検証します (リスト 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) によって、通常のゲッター・メソッドやセッター・メソッドは現れないようにすることができます。これで変換は完了です。つまり AllCapsString は String とまったく同じ動作をするようになり、必要に応じてネイティブの String メソッドをオーバーライドできるようになりました。
静的言語でのダック・タイピングに関する注意
今や AllCapsString は String とまったく同じ動作をするようになりましたが、これでもまだ AllCapsString は真の String ではありません。Java コードでは、String の簡単な置き換えとして AllCapsString を使うことはできません。AllCapsString は真のアヒル (ダック) ではなく、アヒルのように鳴くだけなのです。(動的言語はダック・タイピングを使い、Java 言語は静的型付けを使うと言われます。この違いの詳細は「参考文献」を参照してください。) 言い換えると、AllCapsString は本当に String を継承するわけではない (つまり存在しない Stringable インターフェースを実装するわけではない) ため、Java コードで AllCapsString を 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 を使うと、元の開発者が明示的に継承を禁止していたクラスを継承することができますが、@Delegate は実際に Java の final キーワードを無効にするわけではなく、一線を越えることなく可能な限りの力を提供してくれるのです。
また、クラスが複数の delagate を持てることを忘れないでください。例えば、java.io.File とjava.net.URL の両方の特性を持つ RemoteFile クラスを作成したい場合を考えてみてください。Java 言語は多重継承をサポートしていませんが、いくつかの @Delegate を使うことで非常に多重継承に近いことを実現することができます (リスト 8)。RemoteFile クラスは File でも URL でもありませんが、File と URL の両方の動作をします。
リスト 8. 複数の @Delegates によって多重継承の動作を実現する
class RemoteFile{
@Delegate File file
@Delegate URL url
}
|
@Delegates がクラスの (型ではなく) 動作しか変更できないとしたら、@Delegates は Java 開発者にとっては価値がないということになるのでしょうか。決してそんなことはありません。Java 言語のように静的型付けの言語でさえ、ポリモーフィズムという限定的な形式のダック・タイピングを提供しています。
ポリモーフィズムのアヒルのように鳴く
ポリモーフィズム (「多くの形状」を意味するギリシャ語に由来する単語) の意味は、一連のクラスが同じインターフェースを実装することで同じ動作を明示的に共有する限り、それらのクラスは交換可能なものとして使用できる、ということです。つまり、Duck 型の変数を定義する場合 (Duck は quack() メソッドと waddle() メソッドを正式に定義するインターフェースであると仮定します)、その変数に new Mallard() を割り当てることができ、また new GreenWingedTeal() や (私の好きな) new PekingWithHoisinSauce() を割り当てることもできます。
@Delegate アノテーションはポリモーフィズムを完全にサポートしており、delegate クラスのメソッドを外部のクラスに追加するだけではなく、delegate のインターフェースも外部のクラスに追加します。これはつまり、delegate クラスがインターフェースを実装する場合には、そのインターフェースの簡単な置き換えを作成できるということです。
@Delegate と List インターフェース
ここで例えば、FixedList という新しいクラスを作成したいとします。FixedList は java.util.ArrayList とまったく同じ動作をする必要がありますが、1 つ重要な違いがあり、追加できる要素の数の上限を指定できなければなりません。そうすることによって、最大 2 人まで乗ることができる sportsCar 変数や、最大 4 人までディナーをとることができる restaurantTable などを作成することができます。
ArrayList クラスは List インターフェースを実装します。これによっていくつかの選択肢が得られます。FixedList クラスにも List インターフェースを実装させることができますが、そうすると List のすべてのメソッドに実装を提供するという面倒な作業をしなければなりません。ArrayList は final クラスではないため、別の選択肢として単純に FixedList に ArrayList を継承させる方法があります。この方法は完全に有効な方法ですが、もし (仮の話として) ArrayList が final と宣言されていた場合には、@Delegate アノテーションによって第 3 の選択肢が得られます。つまり ArrayList を FixedList の delegate にすることによって、ArrayList のすべての動作を実現できる一方、List インターフェースも自動的に実装することができるのです。
まず、ArrayList という delegate を持つ FixedList クラスを作成します (リスト 9)。groovyc / javap の繰り返しを行い、FixedList が ArrayList と同じメソッドを提供するだけではなく、ArrayList と同じインターフェースも提供していることを確認します。
リスト 9. FixedList クラスを作成するための第 1 のパス
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 のサイズが固定 (fixed) ではないことを、どのように確認するのでしょう。使い捨てのサンプル・コードを作成することもできますが、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>
|
FixedList の中で add() メソッドをオーバーライドする必要があるようです (リスト 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: スムースな演算子」では、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 のネイティブ機能に可能な限り従うことができる (また従う必要がある) ことに注意してください。つまり、そもそも ArrayList を delegate として選んだ理由はそこにあるはずなのです。この場合には ArrayList に追加を行わせ、FixedList のサイズを超えるすべての要素を単純に削除します。(この add() メソッドがもう一方の add() メソッドと同じように UnsupportedOperationException をスローする必要があるかどうかは、設計者が独自に判断する必要があります。)
trimToSize() メソッドには、注目に値する Groovy の構文糖が少し含まれています。第 1 に、pop() メソッドは Groovy のメタプログラミングによってすべての List に追加されます。pop() メソッドは List の最後の要素を LIFO (後入れ先出し) スタイルで削除します。
次に、each ループに Groovy の range が使われていることに注目してください。変数を実際の数字で置き換えると、この動作が明確にわかると思います。例えば FixedList の sizeLimit が 3 であるとし、新しい要素が追加された後の FixedList の size() が 5 だとします。すると、範囲は (3..5).each{} のようになります。しかし List では基数を 0 とする表記が使われているため、このリストのどの要素も 5 というインデックスを持ちません。(3..<5).each{} と表現することによって、範囲の最後の数字を除外することができます。
いくつかのテストを作成し (リスト 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]
}
}
|
ここで作成したテスト・コードの方が本番のコードよりも多いことにお気付きでしょうか。お気付きなら大変結構です。私は日頃、1 ポンドの本番コードに対して少なくとも 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 の強力さのおかげで FixedList クラスをテストすることができました。これによって、想定どおり動作するという自信を持って本番にこのコードを使用することができます。リスト 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 言語の静的型システムの規則に違反することはありません。Groovy では、(既存のクラスをラップすることにより既存のクラスに任意の新しいメソッドを追加することができる) ExpandoMetaClass と (外部のラッピング・クラスによって複合内部クラスの機能を公開することができる) @Delegate とを使いこなすことで、JVM は、これまでなかったほどアヒルのように歩き、そしてアヒルのように鳴くことができます。
次回の記事では、Groovy の柔軟な構文のおかげで新鮮で新たな関心を集めている昔ながらの技術、Swing について説明します。実際、Groovy の SwingBuilder によって Swing の複雑さが解消されるのです。また、あえて言えば SwingBuilder によってデスクトップ開発が楽しく容易なものになります。では次回まで、皆さんが Groovy の実用的な使い方をたくさん見つけられることを祈っています。
参考文献 学ぶために
製品や技術を入手するために
- Groovy の最新の ZIP ファイルまたは tarball をダウンロードしてください。
議論するために
著者について
記事の評価
|