1

次のような構文でDSLを作成したいと思います。

Graph.make {
    foo {
        bar()
        definedMethod1() // isn't missing!
    }
    baz()
}

このツリーのハンドラーが最も外側のクロージャーに遭遇すると、いくつかのクラスのインスタンスが作成されます。このクラスには、いくつかの定義済みメソッドと、欠落しているメソッド用の独自のハンドラーがあります。

これは、次のような構造で十分簡単だと思いました。

public class Graph {
    def static make(Closure c){
        Graph g = new Graph()
        c.delegate = g
        c()
    }

    def methodMissing(String name, args){
        println "outer " + name
        ObjImpl obj = new ObjImpl(type: name)
        if(args.length > 0 && args[0] instanceof Closure){
            Closure closure = args[0]
            closure.delegate = obj
            closure()
        }
    }

    class ObjImpl {
        String type
        def methodMissing(String name, args){
            println "inner " + name
        }
        def definedMethod1(){ 
                println "exec'd known method"
        }
    }
}

ただし、me​​thodMissingハンドラーは、内部クロージャーをObjImplに委任するのではなく、Graph内のクロージャー全体を解釈し、次の出力を生成します。

outer foo
outer bar
exec'd known method
outer baz

内部クロージャの欠落しているメソッド呼び出しを、作成した内部オブジェクトにスコープするにはどうすればよいですか?

4

3 に答える 3

2

簡単な答えは、内部クロージャresolveStrategyを「最初にデリゲート」に設定することですが、デリゲートがすべてのmethodMissingメソッド呼び出しをインターセプトするように定義するときにこれを行うと、クロージャの外部でメソッドを定義して内部から呼び出すことができなくなります。

def calculateSomething() {
  return "something I calculated"
}

Graph.make {
  foo {
    bar(calculateSomething())
    definedMethod1()
  }
}

この種のパターンを可能にするには、すべてのクロージャをデフォルトの「所有者優先」の解決戦略のままにしておくことをお勧めしますmethodMissingが、内部クロージャが進行中であることを外部に認識させ、それに戻します。

public class Graph {
    def static make(Closure c){
        Graph g = new Graph()
        c.delegate = g
        c()
    }

    private ObjImpl currentObj = null

    def methodMissing(String name, args){
        if(currentObj) {
            // if we are currently processing an inner ObjImpl closure,
            // hand off to that
            return currentObj.invokeMethod(name, args)
        }
        println "outer " + name
        if(args.length > 0 && args[0] instanceof Closure){
            currentObj = new ObjImpl(type: name)
            try {
                Closure closure = args[0]
                closure()
            } finally {
                currentObj = null
            }
        }
    }

    class ObjImpl {
        String type
        def methodMissing(String name, args){
            println "inner " + name
        }
        def definedMethod1(){ 
                println "exec'd known method"
        }
    }
}

このアプローチでは、上記のDSLの例を考えると、calculateSomething()呼び出しは所有者のチェーンを通過し、呼び出し元のスクリプトで定義されたメソッドに到達します。bar(...)anddefinedMethod1()呼び出しは、所有者のチェーンを上って、最も外側のスコープからaを取得し、MissingMethodException最も外側のクロージャーのデリゲートを試行して、で終わりGraph.methodMissingます。次に、が存在することを確認しcurrentObj、メソッド呼び出しをそれに戻します。これにより、最終的には、ObjImpl.definedMethod1またはObjImpl.methodMissing適切になります。

DSLを2レベル以上深くネストできる場合は、単一の参照ではなく「現在のオブジェクト」のスタックを保持する必要がありますが、原則はまったく同じです。

于 2012-08-31T23:45:50.677 に答える
1

groovy.util.BuilderSupport別のアプローチは、あなたのようなツリー構築DSL用に設計されたを利用することかもしれません:

class Graph {
  List children
  void addChild(ObjImpl child) { ... }

  static Graph make(Closure c) {
    return new GraphBuilder().build(c)
  }
}

class ObjImpl {
  List children
  void addChild(ObjImpl child) { ... }
  String name

  void definedMethod1() { ... }
}

class GraphBuilder extends BuilderSupport {

  // the various forms of node builder expression, all of which
  // can optionally take a closure (which BuilderSupport handles
  // for us).

  // foo()
  public createNode(name) { doCreate(name, [:], null) }

  // foo("someValue")
  public createNode(name, value) { doCreate(name, [:], value) }

  // foo(colour:'red', shape:'circle' [, "someValue"])
  public createNode(name, Map attrs, value = null) {
    doCreate(name, attrs, value)
  }

  private doCreate(name, attrs, value) {
    if(!current) {
      // root is a Graph
      return new Graph()
    } else {
      // all other levels are ObjImpl, but you could change this
      // if you need to, conditioning on current.getClass()
      def = new ObjImpl(type:name)
      current.addChild(newObj)
      // possibly do something with attrs ...
      return newObj
    }
  }

  /**
   * By default BuilderSupport treats all method calls as node
   * builder calls.  Here we change this so that if the current node
   * has a "real" (i.e. not methodMissing) method that matches
   * then we call that instead of building a node.
   */
  public Object invokeMethod(String name, Object args) {
    if(current?.respondsTo(name, args)) {
      return current.invokeMethod(name, args)
    } else {
      return super.invokeMethod(name, args)
    }
  }
}

BuilderSupportが機能する方法では、ビルダー自体がDSLツリーのすべてのレベルでのクロージャーデリゲートです。デフォルトの「所有者優先」解決戦略を使用してすべてのクロージャを呼び出します。つまり、DSLの外部でメソッドを定義し、内部から呼び出すことができます。

def calculateSomething() {
  return "something I calculated"
}

Graph.make {
  foo {
    bar(calculateSomething())
    definedMethod1()
  }
}

ただし、同時に、によって定義されたメソッドへの呼び出しはすべてObjImpl、現在のオブジェクト(fooこの例ではノード)にルーティングされます。

于 2012-09-01T12:05:02.917 に答える
0

このアプローチには少なくとも2つの問題があります。

  1. ObjImplと同じコンテキスト内で定義すると、すべての呼び出しが最初にヒットすることをGraph意味しますmissingMethodGraph
  2. 委任は、aが設定されていない限り、ローカルで発生するように見えますresolveStrategy。例:

    closure.resolveStrategy = Closure.DELEGATE_FIRST
    
于 2012-08-31T22:13:55.863 に答える