Как делегировать вызовы methodMissing во вложенные классы?

Я хотел бы создать 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 ответа

Решение

Есть как минимум две проблемы с этим подходом:

  1. определяющий ObjImpl в том же контексте, что и Graph означает, что любой missingMethod звонок ударит Graph первый
  2. Делегация, кажется, происходит на месте, если только resolveStrategy установлено, например:

    closure.resolveStrategy = Closure.DELEGATE_FIRST
    

Простой ответ - установить внутреннюю крышку 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() call передаст цепочку владельцев и достигнет метода, определенного в вызывающем скрипте. bar(...) а также definedMethod1() звонки будут идти вверх по цепочке владельцев и получить MissingMethodException из внешней области видимости, затем попробуйте делегат из наиболее внешней замыкания, заканчивающийся в Graph.methodMissing, Затем увидим, что есть currentObj и передать вызов метода обратно к тому, что, в свою очередь, в конечном итоге ObjImpl.definedMethod1 или же ObjImpl.methodMissing по мере необходимости.

Если ваш DSL может быть вложен глубже, чем на два уровня, тогда вам нужно будет хранить стек "текущих объектов", а не одну ссылку, но принцип точно такой же.

Альтернативный подход может заключаться в использовании 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 узел в этом примере).

Другие вопросы по тегам