如何将methodMissing调用委托给嵌套类?

时间:2012-08-31 21:56:39

标签: groovy dsl method-missing

我想用以下语法创建一个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"
        }
    }
}

但是,methodMissing处理程序解释了Graph中的整个闭包,而不是将内部闭包委托给ObjImpl,从而产生输出:

outer foo
outer bar
exec'd known method
outer baz

如何将内部闭包的缺失方法调用范围限定为我创建的内部对象?

3 个答案:

答案 0 :(得分: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(...)definedMethod1()调用将上升到所有者链并从最外层范围获取MissingMethodException,然后尝试最外层闭包的委托,最后在Graph.methodMissing 。然后会看到有一个currentObj并将方法调用传回给它,然后根据需要将其传递到ObjImpl.definedMethod1ObjImpl.methodMissing

如果您的DSL可以嵌套超过两级,那么您需要保留一堆“当前对象”而不是单个引用,但原理完全相同。

答案 1 :(得分: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节点)。

答案 2 :(得分:0)

这种方法至少存在两个问题:

  1. 在与ObjImpl相同的上下文中定义Graph表示任何missingMethod来电都会先点击Graph
  2. 除非设置resolveStrategy,否则委托似乎在本地发生,例如:

    closure.resolveStrategy = Closure.DELEGATE_FIRST