背景
JenkinsというCIツールではGroovyのコードでCI挙動を記述できるJenkins pipelineがあります。 Jenkins pipelineではGroovyのDSL(Domain-Specific Language:ドメイン固有言語)が使われています。 DSLにはクロージャが効果的に使用されています。
今回はクロージャの名前解決方法(resolveStrategy)です。
前回の記事
前回の記事で、
クロージャは故郷を忘れないためにthis
とowner
に生まれた場所の情報を格納していることが確認できました。また、クロージャで呼んでいるメソッドがowner
で定義されていない場合にdelegate
のメソッドを呼ぶことが分かりました。
名前解決とは
名前解決とは、ここでは、「メソッドやプロパティ名が現れたとき、どこに定義されている名前を使用するか決めること」とします。
クロージャの名前解決 (resolveStrategy)
クロージャは、内部で呼んでいるメソッドがowner
で定義されていない場合にdelegate
のメソッドを呼びます。この挙動はクロージャオブジェクトのresolveStrategy
プロパティに値をセットすることで変更できます。セットできるのは以下の5つです。
名前解決方法 | 説明 |
---|---|
Closure.OWNER_FIRST | デフォルト。プロパティ/メソッドがownerに存在するときは、ownerのものを使用し、なければdelegateのものを使用する |
Closure.DELEGATE_FIRST | OWNER_FIRSTの逆。プロパティ/メソッドがdelegateに存在するときは、delegateのものを使用し、なければownerのものを使用する |
Closure.OWNER_ONLY | プロパティ/メソッドの名前解決をownerのみで行う。delegateは無視される |
Closure.DELEGATE_ONLY | プロパティ/メソッドの名前解決をdelegateのみで行う。ownerは無視される |
Closure.TO_SELF | 名前解決はクロージャクラスで行われる。開発者がクロージャのサブクラスを作成して、クロージャの振る舞いをカスタマイズしたい場合にこの設定にする |
名前解決方法の挙動の確認
OWNER_FIRST
、DELEGATE_FIRST
、OWNER_ONLY
、DELEGATE_ONLY
の動作を確認するため、Main
クラスとPipelineSpec
クラスそれぞれに以下のようにメソッドを定義します。
a() | b() | c() | |
---|---|---|---|
Main | ○ | ○ | × |
PileineSpec | × | ○ | ○ |
class PipelineSpec { def b() { println "I'm PipelineSpec.b" } def c() { println "I'm PipelineSpec.c" } } class Pipeline { def pipeline(Closure cl) { cl.delegate = new PipelineSpec() // ここを書き換えててテストする cl.resolveStrategy = Closure.OWNER_FIRST println "before closure" cl() println "after closure" } } class Main { // 普通のインスタンスメソッド def closureTest() { def pipeline = new Pipeline() pipeline.pipeline { a() b() c() } } def a() { println "I'm Main.a" } def b() { println "I'm Main.b" } } def main = new Main() main.closureTest()
OWNER_FIRST
上記のコードを実行した結果です。
before closure I'm Main.a I'm Main.b I'm PipelineSpec.c after closure
a()
メソッドとb()
メソッドはMain
クラスに定義したものが呼ばれ、c()
メソッドはPipelineSpec
のものが呼ばれました。
DELEGATE_FIRST
上記コードを下記のように書き換えて実行します。
// ここを書き換えててテストする
cl.resolveStrategy = Closure.DELEGATE_FIRST
実行結果です。
before closure I'm Main.a I'm PipelineSpec.b I'm PipelineSpec.c after closure
a()
メソッドはMain
クラスに定義したものが呼ばれ、b()
メソッドとc()
メソッドはPipelineSpec
のものが呼ばれました。
OWNER_ONLY
上記コードを下記のように書き換えて実行します。
// ここを書き換えててテストする
cl.resolveStrategy = Closure.OWNER_ONLY
実行結果です。
before closure I'm Main.a I'm Main.b Exception thrown groovy.lang.MissingMethodException: No signature of method: Main.c() is applicable for argument types: () values: [] Possible solutions: a(), b(), is(java.lang.Object), any(), tap(groovy.lang.Closure), any(groovy.lang.Closure) at Main$_closureTest_closure1.doCall(closure_01.groovy:27) at Main$_closureTest_closure1.doCall(closure_01.groovy) // 以下略
Main
クラスに定義したa()
メソッドとb()
メソッドが呼ばれたあと、名前解決が失敗し、MissingMethodException
が発生しました。
PipelineSpec
での名前解決が行われなかったことがわかります。
DELEGATE_ONLY
上記コードを下記のように書き換えて実行します。
// ここを書き換えててテストする
cl.resolveStrategy = Closure.DELEGATE_ONLY
実行結果です。
before closure Exception thrown groovy.lang.MissingMethodException: No signature of method: PipelineSpec.a() is applicable for argument types: () values: [] Possible solutions: b(), c(), any(), tap(groovy.lang.Closure), any(groovy.lang.Closure), is(java.lang.Object) at Main$_closureTest_closure1.doCall(closure_01.groovy:25) at Main$_closureTest_closure1.doCall(closure_01.groovy) // 以下略
a()
メソッドが見つからず、MissingMethodException
が発生しました。Main
メソッドでの名前解決が行われなかったことがわかります。
DELEGATE_FIRSTとDELEGATE_ONLYの使いどころ
DELEGATE_FIRST
とDELEGATE_ONLY
は、時に使用者の意図しない挙動を引き起こします。
例えば、以下のように、closureTest
メソッド内でpipeline
メソッドに渡しているクロージャでa()
を呼び出したとき、Main
クラスの実装者はMain
クラスのa()
を呼ぶことを意図していますが、実際にはPipelineSpec
クラスのa()
メソッドが呼ばれてしまいます。これはバグの温床になりそうです。
class PipelineSpec { def a() { println "I'm PipelineSpec.a" } } class Pipeline { def pipeline(Closure cl) { cl.delegate = new PipelineSpec() cl.resolveStrategy = Closure.DELEGATE_FIRST println "before closure" cl() println "after closure" } } class Main { def closureTest() { def pipeline = new Pipeline() pipeline.pipeline { // Main#a()を呼ぶ意図で書いているのに、実際にはPipelineSpec#a()が呼ばれる a() } } def a() { println "I'm Main.a" } } def main = new Main() main.closureTest()
DELEGATE_ONLYの使い所は、DSL(Domain-Specific Language)です。 DELEGATE_ONLYでは、クロージャ内から呼び出せるメソッドを制限することができます。 次回の記事で詳しく解説します。
参考リンク
The Apache Groovy programming language - Closures