倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

拡張モジュールを試してみる

以前に、既存のクラスに対してメソッドを追加するメタプログラミングの1つの方法として「マジック・パッケージを試してみる」ってことをやってみました。 この方法では、メタクラスを使う方法に比べて「動的にメソッドを追加する処理を実行する必要がない」メリットの反面、(全てのメソッド呼び出しに対して)パフォーマンスが少々劣化するというデメリットもありました。

先日リリースされた Groovy 2.0 では、マジック・パッケージの方法と同じような「拡張モジュール (Extension modules)」という機能が導入されたようなので*1、これを試してみます。

拡張モジュールを作成するにあたって必要となるコードは

  • ヘルパークラス
  • 拡張モジュール記述子

です。 以下で見ていきましょう。

ヘルパークラスの作成


ヘルパークラスの作成は、基本的に通常の Groovy クラスの作成と同じです。 というか、メソッドのシグニチャの約束事を守っていれば、Java クラスでも OK だそう。

についてそれぞれ見ていきましょう。

インスタンス・メソッド

既存のクラスにインスタンス・メソッドを追加したい場合、適当な(ユーティリティ)クラスに、第1引数に追加したいクラスのインスタンスをとる static メソッドを実装します。 例えば、Complex クラス*2に Complex オブジェクトを1つ引数にとる plus() メソッド

  • Complex#plus(Complex arg) : Complex

を追加したいとしましょう。 このとき、org.sample.ComplexExtension クラスに追加するメソッドの実装を書くことにして(実装は好きなクラスに書いて OK)、コードは以下のようになります:

  • org.sample.ComplexExtension.groovy
package org.sample

import org.apache.commons.math3.complex.Complex

class ComplexExtension {

    static Complex plus(Complex self, Complex arg){
        return self.add(arg)
    }
}

この plus() メソッドは(後で見る拡張モジュール記述子の設定をした後で)以下のように使うことが出来ます:

def c1 = new Complex(1, 2)
def c2 = new Complex(3, 4)

c1 + c2    // plus() メソッド呼び出し c1.plus(c2)

要は ComplexExtension#plus() メソッドの第1引数のインスタンス・メソッド呼び出しと同じになるってことですね。

この形式のメソッド実装はカテゴリー・クラスを作るのと全く同じなので、@Category アノテーションを使って作成することも出来ます(こちらを参照):

package org.sample

import org.apache.commons.math3.complex.Complex

@Category(Complex)
class ComplexExtension {

    Complex plus(Complex arg){
        return this.add(arg)
    }
}

メソッドを呼び出されたインスタンス(最初のサンプル・コードでの self)は this キーワードによって参照できます。 この方法では、コードがインスタンス・メソッドとして書けてスッキリしますが、パフォーマンスが劣化するようです。

static メソッド

static メソッドを追加したい場合も実装するクラスはほとんど同じです。 static メソッドの第1引数の型に対応するクラスに static メソッドが追加されます。

  • org.sample.StaticComplexExtionsion.groovy
package org.sample

import org.apache.commons.math3.complex.Complex

class StaticComplexExtension {

    static Complex sum(Complex type, Complex arg0, Complex arg1){
        return arg0.add(arg1)
    }
}

type オブジェクトは static メソッドを追加するクラスを参照するのに使われるだけで、実行時には null 値です。 コード中では参照することはないでしょう。 また、インスタンス・メソッドの場合と同じように @Category アノテーションを使って作成することもできますが、インスタンス・メソッドとして実装する必要があるので紛らわしいだけかと思います(this キーワードで参照されるオブジェクトはやはり null になります)。

このメソッドを使用する場合は(拡張モジュール記述子を設定した後で)以下のようにします:

def c1 = new Complex(1, 2)
def c2 = new Complex(3, 4)

Complex.sum(c1, c2)    // 追加した sum() メソッド

拡張モジュール記述子 (Extension module descriptor)


メソッドの実装を書いたヘルパークラスを作成した後は、それらのクラスを拡張モジュールとして指定してやる必要があります。 こういった記述子は、よくあるように META-INF/services ディレクトリ下に設定ファイルを配置します。 拡張モジュールの設定ファイルは org.codehaus.groovy.runtime.ExtensionModule です:

  • META-INF/services/org.codehaus.groovy.runtime.ExtensionModule ファイル

このファイルに以下のような設定を記述します:

moduleName = ComplexExtension
moduleVersion = 1.0
extensionClasses = org.sample.ComplexExtension
staticExtensionClasses = org.sample.StaticComplexExtension
  • extensionClasses はインスタンス・メソッドを追加する場合
  • staticExtensionClasses は static メソッドを追加する場合
  • 複数のヘルパークラスを使用したい場合はコンマで区切って指定

以上で設定は終了。 ちなみに、これらはリソースファイルなので Maven 2/3 や Gradle などでプロジェクトを管理している場合は

  • $PROJECT_ROOT/src/main/resources ディレクトリ

に配置してください。

パフォーマンス


んで、これらの方法でメソッドを追加した場合、パフォーマンスがどうなるのか試してみました。 ちなみに実行環境は

  • jdk1.7.0_04
  • groovy 2.0.0

です。

実行は「マジック・パッケージを試してみる」と同様の方法によって行いました。 結果は、拡張モジュールなしで Complex#add() メソッドを実行した時間を1として

  • 拡張モジュールありで Complex#add() メソッド実行 1.0
  • Complex#plus() メソッド(インスタンス・メソッド) 1.3
  • @Category で Complex#plus() メソッド(インスタンス・メソッド) 1.8
  • Complex#sum() メソッド(static メソッド) 1.3
  • メタクラスによるメソッドの動的追加 1.6*3

となりました。 マジック・パッケージの場合と違って、拡張モジュールの有無で通常のメソッド呼び出しに劣化はありませんでした。 また、拡張モジュールを使った場合は、メタクラスを使った動的なメソッド追加よりも速いですね。 ただし、@Category を使った場合はメタクラスの場合よりも劣化してますね。

拡張モジュール、結構使えそう。

プログラミングGROOVY

プログラミングGROOVY


Groovy で DSL 書く際にも、メタクラスによるプログラミングほどのパフォーマンス劣化しなくて使えるかも。
実践プログラミングDSL ドメイン特化言語の設計と実装のノウハウ (Programmer’s SELECTION)

実践プログラミングDSL ドメイン特化言語の設計と実装のノウハウ (Programmer’s SELECTION)

*1:こちらを参照。

*2:org.apache.commons.math3.complex.Complex クラス

*3:前回は1.7でした。