実用的な Groovy: クロージャー、ExpandoMetaClass、そしてカテゴリーによるメタプログラミング

必要なときに、必要な場所にメソッドを追加する

Groovy スタイルのメタプログラミングの世界に入りましょう。クラスに対して (しかも Java™ クラスや、さらには final が指定されている Java クラスにさえ)、実行時に動的に新しいメソッドを追加できる機能は、信じられないほど強力です。本番コードの作成に使われる場合であれ、ユニット・テストの作成に使われる場合であれ、あるいはその他の目的で使われる場合であれ、Groovy のメタプログラミング機能は、経験が豊富すぎて新しいものに関心がない Java 開発者にとっても興味深いはずです。

Scott Davis, Founder, ThirstyHead.com

Scott DavisScott Davis は国際的に知られた著者、講演者、そしてソフトウェア開発者で、Groovy と Grails の教育を目的とした会社、ThirstyHead.com の創設者でもあります。彼の著書には、『Groovy Recipes: Greasing the Wheels of Java』、『GIS for Web Developers: Adding Where to Your Application』、『The Google Maps API』、『JBoss At Work』などがあります。現在、IBM developerWorks の「Grails をマスターする」と「実用的な Groovy」の 2 本の連載を執筆中です。



2009年 6月 23日

長年にわたり、Groovy は JVM のための動的プログラミング言語である、と言われていることは皆さんもご存知かと思います。しかし、それは一体何を意味するのでしょう。今回の「実用的な Groovy」では、メタプログラミングについて、つまりクラスに対して実行時に動的に新しいメソッドを追加できるという Groovy の機能について学びます。この Groovy の柔軟性は、標準的な Java 言語の柔軟性をはるかに超えています。一連のコード・サンプル (すべて「ダウンロード」することができます) をとおして、メタプログラミングが Groovy の最も強力で実用的な機能の 1 つであることを学びましょう。

世界をモデリングする

プログラマーとしての私達の仕事は、現実の世界をソフトウェアでモデリングすることです。現実の世界が単純であるなら (例えば鱗や羽のある動物は卵を産み、毛のある動物は子供を産む、など) そうした動作をソフトウェアで一般化することは容易です (リスト 1)。

リスト 1. Groovy で動物をモデリングする
class ScalyOrFeatheryAnimal{
  ScalyOrFeatheryAnimal layEgg(){
    return new ScalyOrFeatheryAnimal()
  }
}

class FurryAnimal{
  FurryAnimal giveBirth(){
    return new FurryAnimal()
  }
}

このシリーズについて

Groovy は Java プラットフォーム上で実行される最新のプログラミング言語の 1 つです。Groovy は既存の Java コードとシームレスに統合できる一方、クロージャーやメタプログラミングなどの強力な新機能も導入することができます。簡単に言えば Groovy とは、21 世紀に Java 言語が作成されていたら Groovy のようになっていたであろう、そういった言語なのです。

開発ツールキットの一部として新しいツールを採用する際に重要なことは、どういう場合にそのツールを使い、どういう場合には使わずにおくかを理解することです。Groovy は非常に強力ですが、適切な方法で、適切な状況の中で使用した場合にのみ強力なツールとなるのです。そのため「実用的な Groovy」シリーズでは、どういう状況で、どのようにして Groovy を使えば効果的であるかを学べるように、Groovy の実用的な使い方を解説します。

残念ながら、現実の世界は例外や稀なケースで満ちています (カモノハシは、毛があるのに卵を産みます)。まるで、私達が注意深く検討したソフトウェアによる抽象化がすべて、反対の行動をとることのみを目的とする忍者の一団の標的となっているかのようです。

あるドメインをモデリングする際に、不可避の例外処理に関してあまりにも厳格なソフトウェア言語を使用した場合には、それが与える印象は、狭量なお役所仕事から抜け出せない頭の固い公務員が、「失礼ですがカモノハシ様、私達のシステムであなたを追跡して欲しいのであれば、卵ではなく赤ちゃんを生まなければなりません」と言っているようなものになりかねません。

その一方、Groovy のような動的言語を使用すると、より正確に現実の世界をモデリングするようにソフトウェアを調整することができ、世界に対して無遠慮に (そして無駄な) 譲歩を迫る必要がなくなります。例えば Platypus クラス (カモノハシ・クラス) に layEgg() メソッド (卵を産むメソッド) が必要な場合、Groovy によってそれが可能になります (リスト 2)。

リスト 2. layEgg() メソッドを動的に追加する
Platypus.metaClass.layEgg = {->
  return new FurryAnimal()
}

def baby = new Platypus().layEgg()

こうした、毛のある動物と卵についての話がくだらないと思う人は、Java 言語で最も頻繁に使われるクラスの 1 つである、String の厳格さを考えてみてください。


java.lang.String に対する Groovy の新しいメソッド

Groovy を扱う際の楽しさの 1 つは、Groovy によって java.lang.String に新しいメソッドが追加されることです。例えば padRight()reverse() などのメソッドは、単純な String 変換を提供しています (リスト 3)。(String に追加される新しいメソッドの一覧が GDK の中にあります。GDK へのリンクは「参考文献」を参照してください。GDK の先頭ページには図々しいことに、「このドキュメントは、JDK をよりイケてるものに (groovy に) するために JDK に追加されたメソッドについて説明しています。」と書かれています。)

リスト 3. Groovy によって String に追加されたメソッド
println "Introduction".padRight(15, ".")
println "Introduction".reverse()

//output
Introduction...
noitcudortnI

しかし、String に追加されるのは、簡単なかくし芸のようなメソッドだけではありません。String が整形式の URL である場合には、その Stringjava.net.URL に変換し、HTTP GET リクエストの結果を返すという動作を 1 行で行うことができます (リスト 4)。

リスト 4. HTTP GET リクエストを実行する
println "http://thirstyhead.com".toURL().text

//output
<html>
  <head>
    <title>ThirstyHead: Training done right.</title>
<!-- snip -->

別の例としては、リモート・ネットワークを呼び出すだけでローカル・シェル・コマンドを実行することができます。通常は、コマンド・プロンプトで ifconfig en0 と入力し、ネットワーク・カードの TCP/IP 設定をチェックする必要があります。(Mac OS X や Linux® ではなく Windows® を使用している場合には、ifconfig の代わりに ipconfig と入力してください。) Groovy では、それと同じことをプログラムで行うことができます (リスト 5)。

リスト 5. Groovy でシェル・コマンドを作成する
println "ifconfig en0".execute().text

//output
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
	ether 00:17:f2:cb:bc:6b
	media: autoselect status: inactive
  //snip

私は、Groovy を使うと Java 言語ではできないことができるから Groovy が楽しいと言っているわけではありません。Java 言語でも、同じことができます。Groovy の楽しさは、これらのメソッドがまるで String クラスに直接追加されているかのように見える点にあります。Stringfinal クラスであることを考えると、これは見事なものです。(これについては、このすぐ後に説明します。) リスト 6 は String.execute().text と等価な Java のコードです。

リスト 6. Java 言語でシェル・コマンドを作成する
Process p = new ProcessBuilder("ifconfig", "en0").start();
BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line = br.readLine();
while(line != null){
  System.out.println(line);
  line = br.readLine();
}

リスト 6 はまるで、混雑した陸運局で窓口をあちこち回らされているときのような感じがするのではないでしょうか (訳注: 米国の陸運局は混んでいる上に、効率が悪く待たせられることで有名です)。「申し訳ございませんが、ご要求の String を見るためには、まずあそこの列に並んで BufferedReader を取得する必要があります」とでも言われそうです。

もちろん、コンビニエンス・メソッドやユーティリティー・クラスを作成することで、コードの醜い部分をなくすこともできますが、苦労して com.mycompany.StringUtil によって取りつくろっても、所属すべき場所 (String クラス) にメソッドを直接追加する方法 (Platypus.layEgg()) に比べると、大きく見劣りします。

では Groovy は直接の継承や変更ができないクラスに対して、正確にはどのようにして新しいメソッドを追加しているのでしょう。それを理解するためには、クロージャーと ExpandoMetaClass について知る必要があります。


クロージャーと ExpandoMetaClass

Groovy には、強力でありながらも悪い影響を及ぼすことのない言語機能であるクロージャーが用意されています (クロージャーがない限り、カモノハシは卵を産めないでしょう)。簡単に言えば、クロージャーとは名前付きの実行可能コードの断片です。クロージャーは、自分を取り囲むクラスを持たないメソッドです。リスト 7 は単純なクロージャーを示しています。

リスト 7. 単純なクロージャー
def shout = {src->
  return src.toUpperCase()
}

println shout("Hello World")

//output
HELLO WORLD

独立したメソッドを使えるのはかなりクールですが、それよりも、これらのメソッドを既存のクラスに追加できる機能の方がはるかにクールです。リスト 8 のコードを考えてみてください。ここでは String をパラメーターとして受け付けるメソッドを作成する代わりに、そのメソッドを String クラスに直接追加しています。

リスト 8. shout メソッドを String に追加する
String.metaClass.shout = {->
  return delegate.toUpperCase()
}

println "Hello MetaProgramming".shout()

//output
HELLO METAPROGRAMMING

引数を持たない shout() クロージャーが String の EMC (ExpandoMetaClass) に追加されています。すべてのクラス (Java クラスと Groovy クラスの両方) は、そのクラスへのメソッド呼び出しをインターセプトする EMC に囲まれています。つまり、String は確かに final ですが、その String の EMC にメソッドを追加できるということです。その結果、一見すると、まるで Stringshout() メソッドがあるかのように見えます。

この種の関係は Java 言語には存在しないため、Groovy では「委譲 (delegate)」という新しい概念を導入する必要がありました。delegate は EMC に囲まれるクラスです。

メソッド呼び出しはまず EMC を発見し、その次に delegate を発見するため、ありとあらゆる面白いことができます。例えば、リスト 9 では実際に StringtoUpperCase() メソッドを再定義していることに注目してください。

リスト 9. toUpperCase() メソッドを再定義する
String.metaClass.shout = {->
  return delegate.toUpperCase()
}

String.metaClass.toUpperCase = {->
  return delegate.toLowerCase()
}

println "Hello MetaProgramming".shout()


//output
hello metaprogramming

これも、くだらない (危険でさえある) と思えるかもしれません。実際には、toUpperCase() メソッドの動作を変更できないと困る可能性はほとんどないと思いますが、コードのユニット・テストをする際には、これがどれほど重宝するかを考えてみてください。また、メタプログラミングは手早く簡単な方法で、ランダムな振る舞いを確定的な動作に変更することもできます。例えばリスト 10 は、Math クラスの静的な random() メソッドを変更する方法を示しています。

リスト 10. Math.random() メソッドを変更する
println "Before metaprogramming"
3.times{
  println Math.random()
}

Math.metaClass.static.random = {->
  return 0.5
}

println "After metaprogramming"
3.times{
  println Math.random()
}

//output
Before metaprogramming
0.3452
0.9412
0.2932
After metaprogramming
0.5
0.5
0.5

今度は、コストの高い SOAP 呼び出しを行うクラスのユニット・テストを行う場合を想像してみてください。インターフェースを作成したり、モック・オブジェクト一式をスタブ化したりする必要はなく、その 1 つのメソッドを戦略的に変更し、モックとして作成した簡単なレスポンスを返せばよいのです。(ユニット・テストとモックの作成に Groovy を使う方法の例を次のセクションで説明します。)

Groovy のメタプログラミングは実行時に行われます。つまり、そのプログラムが実行されている限り継続されます。では、メタプログラミングが行われる範囲を限定したい場合にはどうすればよいのでしょう (これはユニット・テストを作成する際には特に重要です)。次のセクションでは、メタプログラミングのマジックが行われる範囲を限定する方法について学びます。


メタプログラミングが行われる範囲を限定する

リスト 11 は、私が GroovyTestCase で作成してきたデモ・コードをラップし、出力のテストを少し本格的に行えるようにしています。(GroovyTestCase の扱いに関する詳細は「Groovyを使って、より高速にJavaコードをユニット・テストする」を参照してください。

リスト 11. ユニット・テストでのメタプログラミングを調べる
class MetaTest extends GroovyTestCase{

  void testExpandoMetaClass(){
    String message = "Hello"
    shouldFail(groovy.lang.MissingMethodException){
      message.shout()
    }

    String.metaClass.shout = {->
      delegate.toUpperCase()
    }

    assertEquals "HELLO", message.shout()

    String.metaClass = null
    shouldFail{
      message.shout()
    }
  }
}

コマンド・プロンプトで groovy MetaTest と入力し、このテストを実行します。

メタプログラミングの実行を取り消すためには、単純に String.metaClassnull に設定すればよいことに注目してください。

では、すべての Stringshout() メソッドが現れるのを避けたい場合には、どうすればよいのでしょう。単純に、クラスではなく 1 つのインスタンスの EMC を変更すればよいのです (リスト 12)。

リスト 12. 1 つのインスタンスをメタプログラミングする
void testInstance(){
  String message = "Hola"
  message.metaClass.shout = {->
    delegate.toUpperCase()
  }

  assertEquals "HOLA", message.shout()
  shouldFail{
    "Adios".shout()
  }
}

いくつかのメソッドを同時に追加、あるいは変更する場合には、リスト 13 のように新しいメソッドをまとめて定義します。

リスト 13. 多くのメソッドを同時にメタプログラミングする
void testFile(){
  File f = new File("nonexistent.file")
  f.metaClass{
    exists{-> true}
    getAbsolutePath{-> "/opt/some/dir/${delegate.name}"}
    isFile{-> true}
    getText{-> "This is the text of my file."}
  }

  assertTrue f.exists()
  assertTrue f.isFile()
  assertEquals "/opt/some/dir/nonexistent.file", f.absolutePath
  assertTrue f.text.startsWith("This is")
}

こうすると、そのファイルが実際にファイルシステムに存在するかどうかが無関係になることに注目してください。このユニット・テストの中で、他のいくつものクラスにそのファイルを渡すことができ、そうするとそのファイルは実際のファイルのように動作します。このユニット・テストの最後で f 変数がスコープ外になると、カスタムの動作もスコープ外になります。

ExpandoMetaClass は確かに強力ですが、Groovy にはメタプログラミングのための第 2 の方法として、Groovy 独自の機能セットであるカテゴリーを使う方法が用意されています。


カテゴリーと use ブロック

カテゴリーについて説明する最も良い方法は、その動作を見ることです。リスト 14 はカテゴリーを使って Stringshout() メソッドを追加する方法を示しています。

リスト 14. メタプログラミングにカテゴリーを使う
class MetaTest extends GroovyTestCase{
  void testCategory(){
    String message = "Hello"
    use(StringHelper){
      assertEquals "HELLO", message.shout()
      assertEquals "GOODBYE", "goodbye".shout()
    }

    shouldFail{
      message.shout()
      "foo".shout()
    }
  }
}

class StringHelper{
  static String shout(String self){
    return self.toUpperCase()
  }
}

Objective-C による開発を行ったことがある人であれば、この手法はおなじみのはずです。StringHelper というカテゴリーは通常のクラスであり、特別な親クラスを継承したり特別なインターフェースを実装したりする必要はありません。T 型の特定のクラスに新しいメソッドを追加するためには、T 型を 1 番目のパラメーターとして受け付ける静的なメソッドを定義すればよいだけです。shout()String を 1 番目のパラメーターに取る静的なメソッドなので、use ブロックの中にラップされたすべての Stringshout() メソッドが追加されます。

では、どういう場合に EMC ではなくカテゴリーを選択するのでしょう。EMC を利用すると、ある特定のクラスの 1 つのインスタンス、またはすべてのインスタンスにメソッドを追加することができます。一方、上記からわかるように、カテゴリーを定義すると、一部のインスタンスに (この場合には use ブロックの中にあるインスタンスのみに) メソッドを追加することができます。

EMC を使うと新しい動作を即座に定義することができますが、カテゴリーを使うと、動作を切り離して別のクラス・ファイルの中に保存することができます。つまりその動作を、いくつものさまざまな状況 (例えばユニット・テストや本番コードなど) で使用できるということです。別にクラスを定義するためには手間がかかりますが、その手間の分は再利用ができることで取り戻すことができます。

リスト 15 は、StringHelper と新たに作成された FileHelper とを同じ use ブロックの中で使う方法を示しています。

リスト 15. use ブロックの中でいくつかのカテゴリーを使う
class MetaTest extends GroovyTestCase{
  void testFileWithCategory(){
    File f = new File("iDoNotExist.txt")
    use(FileHelper, StringHelper){
      assertTrue f.exists()
      assertTrue f.isFile()
      assertEquals "/opt/some/dir/iDoNotExist.txt", f.absolutePath
      assertTrue f.text.startsWith("This is")

      assertTrue f.text.shout().startsWith("THIS IS")
    }

    assertFalse f.exists()
    shouldFail(java.io.FileNotFoundException){
      f.text
    }
  }
}


class StringHelper{
  static String shout(String self){
    return self.toUpperCase()
  }
}


class FileHelper{
 static boolean exists(File f){
   return true
 }

 static String getAbsolutePath(File f){
   return "/opt/some/dir/${f.name}"
 }

 static boolean isFile(File f){
   return true
 }

 static String getText(File f){
   return "This is the text of my file."
 }
}

しかしカテゴリーで最も興味深い点は、その実装方法にあります。EMC の場合はクロージャーを使う必要がありますが、これは Groovy 以外では EMC を実装できないということです。一方、カテゴリーは静的なメソッドを持つクラスにすぎないため、Java コードでカテゴリーを定義することができます。実際、Groovy では、既存の Java クラス (つまり意図的にメタプログラミング用に作られたものではないクラス) を再利用することができます。

リスト 16 は Jakarta Commons の Lang パッケージ (「参考文献」を参照) のクラスをメタプログラミングに使用する方法を示しています。org.apache.commons.lang.StringUtils の中のすべてのメソッドは偶然にもカテゴリー・パターンに従っており、String を 1 番目のパラメーターとして受け付ける静的なメソッドになっています。したがって、StringUtils クラスをそのままカテゴリーとして使えるということです。

リスト 16. Java クラスをメタプログラミングに使う
import org.apache.commons.lang.StringUtils

class CommonsTest extends GroovyTestCase{
  void testStringUtils(){
    def word = "Introduction"

    word.metaClass.whisper = {->
      delegate.toLowerCase()
    }

    use(StringUtils, StringHelper){
      //from org.apache.commons.lang.StringUtils
      assertEquals "Intro...", word.abbreviate(8)

      //from the StringHelper Category
      assertEquals "INTRODUCTION", word.shout()

      //from the word.metaClass
      assertEquals "introduction", word.whisper()
    }
  }
}

class StringHelper{
  static String shout(String self){
    return self.toUpperCase()
  }
}

groovy -cp /jars/commons-lang-2.4.jar:. CommonsTest.groovy と入力し、テストを実行してみてください。(もちろん、システム上で JAR ファイルが保存されている場所にパスを変更する必要があります。)


メタプログラミングと REST

メタプログラミングはユニット・テストにしか役に立たない、という誤った印象を持たれないように、最後にもう 1 つ例を挙げましょう。「実用的な Groovy: XML を作成し、構文解析し、容易に扱う」で説明した、現在の気象状況を表示する Yahoo! の RESTful な Web サービスを思い出してください。その記事で身に付けた XmlSlurper のスキルと、今回の記事で身に付けたメタプログラミングのスキルとを組み合わせると、任意の郵便番号に対応する地域の気象状況を 10 行のコードでチェックすることができます (リスト 17)。

リスト 17. weather メソッドを追加する
String.metaClass.weather={->
  if(!delegate.isInteger()){
    return "The weather() method only works with zip codes like '90201'"
  }
  def addr = "http://weather.yahooapis.com/forecastrss?p=${delegate}"
  def rss = new XmlSlurper().parse(addr)
  def results = rss.channel.item.title
  results << "\n" + rss.channel.item.condition.@text
  results << "\nTemp: " + rss.channel.item.condition.@temp
}

println "80020".weather()


//output
Conditions for Broomfield, CO at 1:57 pm MDT
Mostly Cloudy
Temp: 72

これを見るとわかるように、メタプログラミングには非常に柔軟性があります。この記事で概要を説明した、どの手法を使っても (あるいはすべての手法を使って)、1 つのクラスに、あるいはいくつかのクラスに、またはすべてのクラスに、容易にメソッドを追加することができるのです。


まとめ

皆さんが使用する言語の勝手な制約に合わせるよう世界に要請しても、現実的ではありません。ソフトウェアで現実の世界をモデリングするということは、あらゆる稀なケースに対処できる柔軟なツールが必要だということです。幸いなことに、Groovy にはクロージャー、ExpandoMetaClasses、そしてカテゴリーといった、非常に優れたツール・セットがあるため、必要なときに必要な場所に動作を追加することができます。

次回の記事では、ユニット・テストを行う際に Groovy がいかに強力かを改めて検証します。Groovy でテストを作成すると、GroovyTestCase であれ、あるいはアノテーションを使った JUnit 4.x テスト・ケースであれ、実際にメリットがあります。また、Groovy で作成されたモック作成用のフレームワークである GMock の動作についても説明します。では次回まで、皆さんが Groovy の実用的な使い方をたくさん見つけられることを祈っています。


ダウンロード

内容ファイル名サイズ
Source code for the article examplesj-pg06239.zip7KB

参考文献

学ぶために

  • Groovy プロジェクトの Web サイトで Groovy について学んでください。
  • Groovy JDK API の仕様を見てください。Groovy によって String クラスに追加されたすべてのメソッドが説明されています。
  • AboutGroovy.com には Groovy に関する最新のニュースと記事へのリンクがあります。
  • Scott Davis による最新の書籍、『Groovy Recipes』(Pragmatic Programmers、2008年刊) を読み、Groovy と Grails について学んでください。
  • Scott Davis による関連のシリーズ「Grails をマスターする」では、Web 開発のための Groovy ベースの Grails プラットフォームに焦点を当てています。
  • Technology bookstore には、この記事や他の技術的な話題に関する本が豊富に取り揃えられています。
  • developerWorks の Java technology ゾーンには Java プログラミングのあらゆる側面を網羅した記事が豊富に用意されています。

製品や技術を入手するために

  • Jakarta Commons Lang を入手してください。この、java.lang API のためのヘルパー・ユーティリティーには、Groovy でのメタプログラミングに使用できるクラスが含まれています。
  • Groovy の最新の ZIP ファイルまたは tarball をダウンロードしてください。

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology, Open source
ArticleID=415473
ArticleTitle=実用的な Groovy: クロージャー、ExpandoMetaClass、そしてカテゴリーによるメタプログラミング
publish-date=06232009