【问题标题】:How do I delegate methodMissing calls to nested classes?如何将 methodMissing 调用委托给嵌套类?
【发布时间】:2012-08-31 21:56:39
【问题描述】:

我想创建一个语法如下的 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

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

【问题讨论】:

    标签: groovy dsl method-missing


    【解决方案1】:

    简单的答案是将内部闭包的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 可以嵌套超过两层,那么您需要保留一堆“当前对象”而不是单个引用,但原理完全相同。

    【讨论】:

    • 啊!有趣的。所以这似乎是一个范围问题,可以通过使用“this”关键字来解决范围问题。特别是,调用this.calculateSomething() 将解析该方法。不保留堆栈的好处是处理>2个嵌套闭包中缺失方法的代码可以保留在适当的上下文中(我希望有3-4层深)
    • 是的,但由于大多数 DSL 的工作所有者首先是最不意外的原则,因此您不必这样做。我使用 Groovy 的 BuilderSupport 添加了一个替代答案,它可能对您更有效。
    • 谢谢,我很欣赏其他方法。经过一些实验,我仍然更喜欢将嵌套闭包的行为委托给嵌套对象。虽然它不同于标准的 Groovy DSL impl,但它似乎提供了更好的关注点分离,并且考虑到我的风格和我同事的首选风格,我认为最终将使我们正在开发的 DSL 更易于维护(这很关键)。我正在修改当前使用 BuilderSupport 的实现,在我看来,这很难遵循。尽管如此,对于您的努力和帮助,我还是赞成您的回答。谢谢!
    【解决方案2】:

    另一种方法可能是使用 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 的工作方式,builder 本身是 DSL 树所有级别的闭包委托。它使用默认的“所有者优先”解析策略调用其所有闭包,这意味着您可以在 DSL 外部定义一个方法并从内部调用它,例如

    def calculateSomething() {
      return "something I calculated"
    }
    
    Graph.make {
      foo {
        bar(calculateSomething())
        definedMethod1()
      }
    }
    

    但同时任何对ObjImpl 定义的方法的调用都将被路由到当前对象(本例中为foo 节点)。

    【讨论】:

      【解决方案3】:

      这种方法至少有两个问题:

      1. 在与Graph 相同的上下文中定义ObjImpl 意味着任何missingMethod 调用都会首先到达Graph
      2. 除非设置了resolveStrategy,否则委托似乎在本地进行,例如:

        closure.resolveStrategy = Closure.DELEGATE_FIRST
        

      【讨论】:

        猜你喜欢
        • 2018-05-07
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多