[Groovy、Grails]メタプログラミング

  • 8
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

参考

メタプログラミングGroovy入門
Groovyでクラスを静的に動的拡張する方法
Groovyで@Categoryを使ってカテゴリクラスを作成する
Groovy ノミックス (Class 編)

概要

Groovyでメタプログラミングするには以下の2つの方法があります。
ExpandoMetaClassの利用
Categoryクラスの利用

なお、ここで言うメタプログラミングは、実行時に本来存在していないメソッドを追加する、というすごく薄い意味です。

両方基本的に実現できることに差ありません。(Staticなメソッドを追加したい!となるとちょっと違う)

違いとしては、ExpandoMetaClassの場合は、一度メソッドを追加すればプログラムが実行されている間は常にそのメソッドが追加されている状態で、Categoryクラスの場合は自分が指定した範囲の中でのみ、メソッドが追加されている状態、というイメージです。

では実際にGrails上で使う際のサンプルをどうぞ。
なお、パッケージ名はmetatestとしました。

ExpandoMetaClass on Grails

ExpandoController.groovy
package metatest

class ExpandoController {

    def index1() {
        String.metaClass.twoTimes = { ->
            delegate * 2
        }
        render "index1".twoTimes()
    }

    def index2() {
        render "index2".twoTimes()
    }
}

index1()を見れば分かるとおり、String型のmetaClassというプロパティに対して、自分独自のメソッドであるtwoTimesというクロージャを代入しています。
delegateは自分自身のインスタンスを表します。通常のクラスで言うthisに似たものです。
これでindex1()にアクセスすれば、index1index1と表示されます!
デフォルトのString型には存在しない独自のメソッドが追加できました!
続いてindex2()にアクセスすると、すでにString型にtwoTimes()というメソッドが追加されている状態なので、同様にindex2index2と表示されます。

注意点

鋭い人はすでに感づいていると思いますが、index1にアクセスする前に、index2()にアクセスしてしまうと、まだString型にtwoTimes()というメソッドが追加されていない状態なので、当然エラーとなってしまいます。

No signature of method: java.lang.String.twice() is applicable for argument types: () values: [] Possible solutions: trim(), size(), size(), take(int), with(groovy.lang.Closure), wait()

この問題を回避するために、Grailsの起動時にすべて事前に追加しておくのがベターです。
(BootStrap.groovyに記述する。)

conf/BootStrap.groovy
import grails.util.Environment

class BootStrap {

    // Grails開始時に実行される
    def init = { servletContext ->
        Environment.executeForCurrentEnvironment {
            development {
                // 事前にメソッドを追加するようにしておく
                String.metaClass.twoTimes = { ->
                    delegate * 2
                }
            }
            test {
                // 事前にメソッドを追加するようにしておく
                String.metaClass.twoTimes = { ->
                    delegate * 2
                }
            }
            production{
                // 事前にメソッドを追加するようにしておく
                String.metaClass.twoTimes = { ->
                    delegate * 2
                }
            }
        }
    }

    // Grails終了時に実行される
    def destroy = {
        environments {
            development {
                println "dev end!"
            }
            test {
                println "test end!"
            }
            production{
                println "production end!"
            }
        }
    }
}

うーん、この方法だとグローバルにString型が汚されて気持ち悪いな。。。という場合にはCategoryクラスの利用がお勧めです。
Categoryならば、Categoryクラスをimportしておいて、useキーワードを使えばそのuseキーワードの中でのみ有効になります。

Category Class on Grails

Categoryクラスを利用する場合は、専用のクラスを用意します。
ExpandoMetaClassではメソッドを追加したいクラス自体をいじっていましたが、Categoryクラスの場合は通常のクラスを別途用意して、その中に追加したいメソッドを記述します。その際に@Categoryアノテーションを指定するだけでCategoryクラスとして利用できるようになります。

src/groovy/metatest/originalmeta/MyCategory.groovy
package metatest.originalmeta

// @Categoryの引数には、メソッドを追加したい型を指定する。
@Category(String)
class MyCategory {
    def twoTimesOnCategory() {
        this * 2
    }
}

実際の使い方は以下。

CategoryController.groovy
package metatest

import metatest.originalmeta.MyCategory

class CategoryController {

    def index1() {
        use(MyCategory) {
            render "index1".twoTimesOnCategory()
        }
    }

    def index2() {
        render "index2".twoTimesOnCategory()
    }

    def index3() {
        use(MyCategory) {
            render "index3".twoTimesOnCategory()
        }
    }
}

useキーワードで指定したブロックの中では、useキーワードに指定したカテゴリクラスが有効になるので、カテゴリクラス内で定義しているメソッドが利用可能になります。
当然、index1()にアクセスすれば、index1index1と表示されます。
その後、index2()にアクセエスしても、String型自体にはメソッドが追加されていないので、エラーになります。
こんなエラーが出ます。
Message
No signature of method: java.lang.String.twoTimesOnCategory() is applicable for argument types: () values: []

index3はindex1同様MyCategoryクラスが有効な状態なので、index3index3と表示されます。

最後

とりあえず以上。
今後もっと体裁とか、細かい使い方とか追記予定。