#参考
メタプログラミングGroovy入門
Groovyでクラスを静的に動的拡張する方法
Groovyで@Categoryを使ってカテゴリクラスを作成する
Groovy ノミックス (Class 編)
#概要
Groovyでメタプログラミングするには以下の2つの方法があります。
ExpandoMetaClassの利用
Categoryクラスの利用
なお、ここで言うメタプログラミングは、実行時に本来存在していないメソッドを追加する、というすごく薄い意味です。
両方基本的に実現できることに差ありません。(Staticなメソッドを追加したい!となるとちょっと違う)
違いとしては、ExpandoMetaClassの場合は、一度メソッドを追加すればプログラムが実行されている間は常にそのメソッドが追加されている状態で、Categoryクラスの場合は自分が指定した範囲の中でのみ、メソッドが追加されている状態、というイメージです。
では実際にGrails上で使う際のサンプルをどうぞ。
なお、パッケージ名はmetatestとしました。
ExpandoMetaClass on Grails
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に記述する。)
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クラスとして利用できるようになります。
package metatest.originalmeta
// @Categoryの引数には、メソッドを追加したい型を指定する。
@Category(String)
class MyCategory {
def twoTimesOnCategory() {
this * 2
}
}
実際の使い方は以下。
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
と表示されます。
#最後
とりあえず以上。
今後もっと体裁とか、細かい使い方とか追記予定。