Java.next: 継承を伴わない拡張、第 3 回

Groovy メタプログラミングによって共通の問題を簡単に解決する

Java.next 言語 — Groovy、Scala、および Clojure — では、Java 言語の拡張に関する制約をさまざまな方法で是正しています。連載「Java.next」の今回の記事で取り上げるのは、Groovy のメタプログラミング機構で使用できる驚異的な拡張機能です。

Neal Ford, Director / Software Architect / Meme Wrangler, ThoughtWorks

Photo of Neal fordNeal Ford は世界的な IT コンサルティング企業 ThoughtWorks のディレクターであり、ソフトウェア・アーキテクトであり、Meme Wrangler でもあります。また彼は、アプリケーション、教育資料、雑誌記事、コースウェア、ビデオや DVD によるプレゼンテーションなどの設計と開発も行っています。さまざまな技術に関する本の著者、編集者でもあり、最新の著作は『Presentation Patterns』です。彼は大規模なエンタープライズ・アプリケーションの設計や構築を専門にしています。また彼は世界各地で開催される開発者会議での講演者としても国際的に有名です。彼の Web サイトをご覧ください。



2013年 10月 03日

この連載について

Java の遺産となるのは、プラットフォームであって、言語ではないでしょう。200 を超える言語が JVM 上で実行されている今、最終的にこれらの言語の 1 つが JVM のプログラミングに最適な方法としてJava 言語に取って代わることは避けられません。この連載では、Java 開発者が自分たちの近い将来を垣間見ることができるように、3 つの次世代 JVM 言語 — Groovy、Scala、Clojure — について、新しい機能やパラダイムを比較対照することで、詳しく探ります。

連載「Java.next」の前回と前々回の記事では、Java.next 言語が既存のクラスやその他の成果物の拡張を可能にする数え切れない方法のうち、いくつかを取り上げて調査しました。今回の記事でも、この調査を続行し、多種多様なコンテキストで拡張を可能にする Groovy メタプログラミング手法について、詳しく探ります。

継承を伴わない拡張、第 1 回」では、既存のクラスに新しい振る舞いを追加するメカニズムとしてカテゴリー・クラスと ExpandoMetaClass について説明しました。そのとき、たまたま Groovy の一部のメタプログラミング機能に触れましたが、Groovy のメタプログラミング機能はさらに奥深く、Java コードとの統合を容易にして、一般的な処理を Java 言語で必要とされる構文よりも少ない構文で実行できるようにします。

インターフェースの強制

Java 言語では、一般的な動作を再利用するメカニズムとなっているのはインターフェースです。したがって、Java コードを簡潔に統合しようとする他の言語は、インターフェースを簡単に具象化する手段を提供しなければなりません。Groovy の場合、従来の Java の方法でクラスがインターフェースを拡張することもできますが、クロージャーとマップをインターフェース・インスタンスに強制したほうが良い場合には、そうすることができるようになっています。

単一メソッドの強制

リスト 1 に記載する Java のサンプル・コードは、ファイルの場所を見つけるために FilenameFilter インターフェースを使用しています。

リスト 1. Java で FilenameFilter インターフェースを使用してファイルを一覧表示する
import java.io.File;
import java.io.FilenameFilter;

public class ListDirectories {
    public String[] listDirectoryNames(String root) {
        return new File(root).list(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                return new File(name).isDirectory();
            }
        });
    }
}

リスト 1 では、accept() メソッドをオーバーライドする新しい匿名内部クラスを作成し、フィルター基準を指定しました。一方、Groovy では、新規クラスを作成する手続きを省略して、単純にクロージャーをインターフェースに強制するだけで済みます (リスト 2 を参照)。

リスト 2. Groovy でクロージャーの強制を使用して FilenameFilter インターフェースをシミュレートする
new File('.').list(
    { File dir, String name -> new File(name).isDirectory() }
     as FilenameFilter).each { println it }

リスト 2list() メソッドは、引数として FilenameFilter インスタンスを要求しますが、私はこのインターフェースの代わりに、インターフェースの accept() シグニチャーと一致するクロージャーを作成し、クロージャーの本体にこのインターフェースの機能を実装しました。クロージャーの定義に続き、クロージャーを適切な FilenameFilter インスタンスに強制するために使用しているのは、as FilenameFilter の呼び出しです。Groovy の as 演算子により、クロージャーはインターフェースを実装するクラスに具象化されます。単一メソッドのインターフェースの場合、メソッドとクロージャーとの間には自然なマッピングが存在するため、この手法が功を奏します。

複数のメソッドを指定するインターフェースの場合、具象化されたクラスは各メソッドに対して同じクロージャー・ブロックを呼び出しますが、すべてのメソッド呼び出しを同じコードで処理するのが妥当な場合は、まれにしかありません。複数のメソッドが必要な場合には、単一のクロージャーではなく、メソッド名/クロージャーのペアからなる Map を使用してください。

マップ

Groovy では、インターフェースを表すためにマップを使用することもできます。マップのキーはメソッドの名前を表すストリングであり、キーの値はメソッドの振る舞いを実装するコード・ブロックです。リスト 3 に、マップを Iterator インスタンスに具象化する例を示します。

リスト 3. Groovy でマップを使用してインターフェースを具象化する
h = [hasNext: { h.i > 0 }, next: {h.i--}]
h.i = 10
def iterator = h as Iterator
                                                  
while (iterator.hasNext())
  print iterator.next() + ", "
// 10, 9, 8, 7, 6, 5, 4, 3, 2, 1,

リスト 3 で作成しているマップ (h) には、hasNext キー、next キー、そしてそれぞれのコード・ブロックが収容されています。Groovy では、マップのキーはストリングであることが前提となるため、キーを引用符で囲む必要はありません。各コード・ブロック内では、h マップの 3 番目のキー (i) を参照するためにドット表記 (h.i) が使用されています。お馴染みのオブジェクト構文から拝借したこのドット表記は、Groovy における構文糖の一例です。これらのコード・ブロックは、h がイテレーターとして機能するときに初めて実行されるので、h をイテレーターとして使用する前に i が確実に値を持つようにしなければなりません。そのため、i の値を h.i = 10 で設定してから、hIterator としてキャストして、10 から始まる整数のコレクションを取り込んでいます。

Groovy では、マップがオンザフライでインターフェースのインスタンスとして機能できるようにすることで、 Java 言語で強いることのある構文上の面倒な部分を一部大幅に緩和しています。この機能は、Java.next 言語がいかに開発者のエクスペリエンスを進化させるかを示す好例です。


ExpandoMetaClass

継承を伴わない拡張、第 1 回」で例示したように、ExpandoMetaClass は、ObjectString などのコア・クラスを含め、クラスに新しいメソッドを追加するために使用することができます。さらに ExpandoMetaClass は、例えばメソッドをオブジェクト・インスタンスに追加したり、例外処理を改善したりするなど、他のいくつかの目的にも役立ちます。

オブジェクトへのメソッドの追加と、オブジェクトからのメソッドの削除

ExpandoMetaClass を使用してクラスに対して行う変更は、クラスに振る舞いを追加した瞬間からグローバルに適用されます。このように、変更が瞬時に広まることが、この手法の利点です。これは、この拡張メカニズムが Grails Web フレームワーク (「参考文献」を参照) の作成に端を発していることを考えると当然のことでしょう。Grails はコア・クラスに対するグローバル変更に依存するからです。しかし場合によっては、すべてのインスタンスに影響を与えることなく、限定された方法でクラスに動作を追加しなければならないこともあります。そのような場合、Groovy にはオブジェクトのメタクラス・インスタンスと対話する方法が用意されています。例えば、リスト 4 に示すように、メソッドを特定のオブジェクト・インスタンスのみに追加することができます。

リスト 4. オブジェクト・インスタンスに振る舞いを追加する
def list = new ArrayList()
list.metaClass.randomize = { ->
    Collections.shuffle(delegate)
    delegate
}

list << 1 << 2 << 3 << 4
println list.randomize() // [2, 1, 4, 3]
println list             // [2, 1, 4, 3]

リスト 4 では、ArrayList のインスタンス (list) を作成した後、遅延してインスタンス化されるこのインスタンスの metaClass プロパティーにアクセスします。インスタンスには、shuffle を実行したコレクションを返すメソッド (randomize()) を追加します。メタクラス・メソッド宣言の中では、delegate がオブジェクト・インスタンスを表します。

ただし、shuffle() は状態を変更する呼び出しであるため、この randomize() メソッドはベースとなるコレクションの状態を変更します。リスト 4 の出力の 2 行目で、コレクションがランダム化された新しい順序に永続的に変更されていることに注目してください。幸い、Collections.shuffle() のような組み込みメソッドのデフォルトの振る舞いは、それらの奇妙な癖に対処することで簡単に変更することができます。例えば、リスト 5 の random プロパティーは、リスト 4randomize() メソッドを改善したものです。

リスト 5. 望ましくない動作を改善する
def list2 = new ArrayList()
list2.metaClass.getRandom = { ->
  def l = new ArrayList(delegate)
  Collections.shuffle(l)
  l
}

list2 << 1 << 2 << 3 << 4
println list2.random // [4, 1, 3, 2]
println list2        // [1, 2, 3, 4]

リスト 5getRandom() メソッドの本体では、リストの状態を変更する前にリストをコピーしています。これにより、元のリストは変更されないまま維持されます。また、プロパティーが自動的に get メソッドと set メソッドにマッピングされるようにするための Groovy の命名規則を使用して、random をメソッドではなくプロパティーにしました。

余分な括弧によるノイズを削減するためにプロパティー手法を使用することが、Groovy でのメソッド・チェーンの方法の最近の変化につながっています。この言語のバージョン 1.8 では、より流れるようなドメイン特化言語 (DSL) の作成を可能にする、コマンド・チェーンの概念が導入されました。DSL は一般に、既存のクラスまたはオブジェクト・インスタンスを増補して特殊な振る舞いを追加します。

mixin

Ruby や Ruby と同様の言語でよく使われている機能は、mixin です。mixin を使用すれば、継承を使用することなく、既存の階層に新しいメソッドやフィールドを追加することができます。Groovy は mixin をサポートしています (リスト 6 を参照)。

リスト 6. mixin を使用して振る舞いを追加する
class ListUtils {
  static randomize(List list) {
    def l = new ArrayList(delegate)
    Collections.shuffle(l)
    l
  }
}
List.metaClass.mixin ListUtils

リスト 6 では、ヘルパー・クラス (ListUtils) を作成し、それに randomize() メソッドを追加しました。そして最後の行で、ListUtils クラスを java.util.List にミックスインし、randomize() メソッドを java.util.List で使用できるようにしています。mixin は、オブジェクト・インスタンスでも使用することができます。この手法では、変更が個別のコード成果物に限定されるので、デバッグとトレースが容易になります。したがって、振る舞いをクラスに追加するには望ましい方法です。

拡張ポイントの結合

Groovy のメタプログラミング機能はそのそれぞれが強力であるだけでなく、これらの機能は効果的に結合します。動的言語に共通の巧妙な点は、メソッド・ミッシングによるフックです。これは、まだ定義されていないメソッドに対し、クラスが例外をスローするのではなく、制御された方法で応答できることを意味します。Groovy では、あるクラスの不明なメソッドが呼び出されると、そのクラスに methodMissing() が含まれている場合はこのメソッドが呼び出されます。methodMissing() は、ExpandoMetaClass によって行う追加に含めることができます。methodMissing()ExpandoMetaClass を結合することで、Logger などの既存のクラスに柔軟性を加えることが可能になります。リスト 7 はその一例です。

リスト 7. ExpandoMetaClassmethodMissing をミックスインする
import java.util.logging.*

Logger.metaClass.methodMissing = { String name, args ->
    println "inside methodMissing with $name"
    int val = Level.WARNING.intValue() +
        (Level.SEVERE.intValue() - Level.WARNING.intValue()) * Math.random()
    def level = new CustomLevel(name.toUpperCase(),val)
    def impl = { Object... varArgs ->
        delegate.log(level,varArgs[0])
    }
    Logger.metaClass."$name" = impl
    impl args
}

Logger log = Logger.getLogger(this.class.name)
log.neal "really messed this up"
log.minor_mistake "can fix later"

リスト 7 では、ExpandoMetaClass を使用して methodMissing() メソッドを既存の Logger クラスに追加しています。これで、後のほうのコードでは、この Logger クラスがスコープ内にあれば、リスト 7 の最後の 3 行に示されているように、創造的な方法でログを呼び出せるようになります。


アスペクト指向プログラミング

アスペクト指向プログラミング (AOP) は、Java テクノロジーを当初の設計の枠を超えて拡張するための一般的かつ有用な方法です。バイトコードとコンパイル・プロセスを操作することで、アスペクトが新しいコードを既存のメソッドに「織り込む (weave)」ことができます。AOP で定義しているいくつかの用語のうち、ポイントカットという用語は、増補が行われる場所を意味します。例えば、before ポイントカットは、メソッド呼び出しの前に追加されるコードを参照します。

Groovy のコンパイルでは Java バイトコードが生成されるため、Groovy でも AOP は可能です。ただし、Groovy での AOP は、Java 言語が必要とする面倒な形式を使用せずに、メタプログラミングで再現することができます。メソッドには ExpandoMetaClass によってアクセスできるため、そのメソッドの参照を保存することができます。その場合、後でそのメソッドを再定義したとしても、そのメソッドの元のバージョンを呼び出すことができます。リスト 8 に、AOP でこのように ExpandoMetaClass を使用する方法を示します。

リスト 8. アスペクト指向のポイントカットに ExpandoMetaClass を使用する
class Bank {
  def transfer(Account to, Account from, BigDecimal amount) {
    from.balance -= amount
    to.balance += amount
  }
}

class Account {
  def name, balance;

  @Override
  public String toString() {
    "Account{name:${name}, balance:${balance}}"
  }
}

def oldTransfer = 
  Bank.metaClass.getMetaMethod("transfer", [Account, Account, BigDecimal] as Object[])

Bank.metaClass.transfer = { Account to, Account from, BigDecimal amount ->
  println "Logging transfer: to: ${to}, from: ${from}, amount: ${amount}"
  oldTransfer.invoke(delegate, [to, from, amount] as Object[])
}

def bank = new Bank()
def acctA = new Account(name: "A", balance: 100.00)
def acctB = new Account(name: "B", balance: 200.00)
println("Balances: A = ${acctA.balance}, B = ${acctB.balance}")
bank.transfer(acctA, acctB, 10.00)
println("Balances: A = ${acctA.balance}, B = ${acctB.balance}")
//Balances: A = 100.00, B = 200.00
//Logging transfer: to: Account{name:A, balance:100.00},
//    from: Account{name:B, balance:200.00}, amount: 10.00
//Balances: A = 110.00, B = 190.00

リスト 8 では、transfer() メソッドだけを持つ典型的な Bank クラスを作成しました。補助的な Account クラスが保持するのは、単純なアカウント情報です。ExpandoMetaClass には、メソッドの参照を取得する getMetaMethod() メソッドが含まれます。リスト 8getMetaMethod() を使用して既存の transfer() メソッドの参照を取得します。次に、ExpandoMetaClass を使用して、古い transfer() メソッドに置き換わる新しい transfer() メソッドを作成します。ロギング・ステーメントを作成した後に、この新しいメソッドの本体の中で元のメソッドを呼び出します。

リスト 8 に記載されているのは、before ポイントカットの例です。この例では、元のメソッドを呼び出す前に、「追加」コードを実行します。これは、Ruby などの動的言語では一般的な手法です。Ruby コミュニティーでは、この手法を「モンキー・パッチ」と呼んでいます (当初は「ゲリラ・パッチ」と呼ばれていましたが、この用語は「ゴリラ・パッチ」と聞き間違えられたことから、洒落としてモンキー・パッチという名前に変更されました)。このサンプル・コードの結果は AOP と同じですが、Groovy の動的拡張機能により、この拡張を Groovy 言語の範囲内で実行することができます。


AST 変換

ExpandoMetaClass とその関連機能はどんなに強力でも、すべての拡張ポイントを網羅することはできません。突き詰めると、最も強力なメタプログラミングの機能は、コンパイラーの抽象構文木 (AST) — コンパイル・プロセスで維持される内部データ構造 — を変更できるところにあります。変換を接続できるフックの場所の 1 つは、アノテーションです。Groovy では数々の有用な言語拡張機能を AST 変換として事前定義しています。

一例として、@Lazy アノテーション (例えば、@Lazy pets = ['Cat', 'Dog', 'Bird']) は、データ構造が評価で必要になるまで、データ構造のインスタンス化を遅延します。リスト 9 に、Groovy 1.8 に導入されている多数の有用な構造関連のアノテーションうちのいくつかが示されています。

リスト 9. Groovy の有用な構造関連のアノテーション
import groovy.transform.*;

@Immutable
@TupleConstructor
@EqualsAndHashCode
@ToString(includeNames = true, includeFields=true)
final class Point {
  int x
  int y
}

リスト 9 では、Groovy ランタイムが自動的に以下の処理を行います。

  • タプル・スタイルのコンストラクターを生成する
  • equals() メソッドと hashCode() メソッドの両方を生成する
  • Point クラスを不変にする
  • toString() メソッドを生成する

IDE やリフレクションを使用してインフラストラクチャー・メソッドを生成するよりも、AST 変換を使用するほうが遥かに優れた方法です。IDE を使用する場合、変更の発生時にメソッドを再生成することを必ず覚えていなければなりません。また、リフレクションは、コンパイル時に行われるコード生成よりも時間がかかります。

豊富な種類の事前定義 AST 変換を使用するだけでなく、独自の AST 変換を作成するために Groovy が用意している完全な API を使用することもできます。この API では、ベースとなる抽象化の最も粒度の細かいレベルにアクセスして、コードの生成方法を変更することが可能です。


まとめ

今回の記事では、Groovy がそのメタプログラミング機能によって提供している拡張方法の数々について目まぐるしく説明しました。連載「Java.next」の次回の記事では、Scala での特徴 (mixin 機能) とその他のメタプログラミングを探ります。

参考文献

学ぶために

  • プロダクティブ・プログラマ — プログラマのための生産性向上術』(Neal Ford 著、オライリー・ジャパン、2008年): コーディングの効率性を改善するためのツールとプラクティスについて説明している、Neal Ford の著書です。
  • Clojure: Clojure は JVM 上で実行される最近の関数型 Lisp です。
  • Scala: Scala は JVM 上で実行される最近の関数型言語です。
  • Groovy は、Java の構文と機能が更新された動的バージョンです。
  • Grails: Grails は Groovy で書かれた人気のある Web フレームワークです。
  • 実用的な Groovy」(Andrew Glover、Scott Davis 共著、developerWorks、2004-2009年): この developerWorks の連載で Groovy の実用的な使い方を探り、この実用的な使い方をいつどのように適用するとうまくいくかを学んでください。
  • Grails をマスターする」(Scott Davis、developerWorks、2008-2009年): この連載記事で Grails についてのすべてを学んでください。
  • Advanced Groovy Tips and Tricks」: この記事での Groovy の高度な例のいくつかは、この素晴らしいプレゼンテーションから引用したもので、Ken Kousen に感謝いたします。
  • 連載「関数型の考え方」: developerWorks に公開された Neal Ford の連載記事を読み、関数型プログラミングの知識を深めてください。
  • この著者による他の記事 (Neal Ford 著、developerWorks、2005年6月から現在まで): Groovy、Scala、Clojure、関数型プログラミング、アーキテクチャー、設計、Ruby、Eclipse、その他の Java 関連の技術について学んでください。
  • developerWorks Java technology ゾーン: Java プログラミングのあらゆる側面を網羅した豊富な記事を調べてください。

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

議論するために

  • developerWorks コミュニティーに参加してください。ここでは他の developerWorks ユーザーとのつながりを持てる他、開発者によるブログ、フォーラム、グループ、Wiki を調べることができます。

コメント

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=946916
ArticleTitle=Java.next: 継承を伴わない拡張、第 3 回
publish-date=10032013