Groovyのクロージャ ③名前解決方法(resolveStrategy)の挙動

背景

JenkinsというCIツールではGroovyのコードでCI挙動を記述できるJenkins pipelineがあります。 Jenkins pipelineではGroovyのDSL(Domain-Specific Language:ドメイン固有言語)が使われています。 DSLにはクロージャが効果的に使用されています。

今回はクロージャの名前解決方法(resolveStrategy)です。

前回の記事

前回の記事で、 クロージャは故郷を忘れないためにthisownerに生まれた場所の情報を格納していることが確認できました。また、クロージャで呼んでいるメソッドがownerで定義されていない場合にdelegateのメソッドを呼ぶことが分かりました。

yucatio.hatenablog.com

名前解決とは

名前解決とは、ここでは、「メソッドやプロパティ名が現れたとき、どこに定義されている名前を使用するか決めること」とします。

クロージャの名前解決 (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_FIRSTDELEGATE_FIRSTOWNER_ONLYDELEGATE_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_FIRSTDELEGATE_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

環境

次回に続く

yucatio.hatenablog.com