2012-08-31 1 views
1

Я хотел бы создать DSL с синтаксисом:Как передать методMissing вызовы вложенных классов?

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 

Как я могу охватить отсутствующий вызов метода для внутреннего закрытия для внутреннего объекта, который я создаю?

ответ

0

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

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

    closure.resolveStrategy = Closure.DELEGATE_FIRST 
    
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.definedMethod1 или ObjImpl.methodMissing, если это необходимо.

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

+0

Ах! Интересно. Таким образом, это похоже на проблему с охватом и может быть преодолено путем использования ключевого слова «this». В частности, вызов метода this.calculateSomething() 'разрешит этот метод. Преимущество, заключающееся в том, что он не держится вокруг стека, заключается в том, что код для устранения недостающих методов в> 2 вложенных замыканиях может храниться в соответствующем контексте (и я ожидаю, что он будет на 3-4 уровня) –

+0

Правда, но поскольку большинство владельцев DSL работают сначала принцип наименьшего удивления предполагает, что вам не нужно этого делать. Я добавил альтернативный ответ, используя Groovy 'BuilderSupport', который может работать лучше для вас. –

+0

Спасибо, я ценю альтернативные подходы. После некоторых экспериментов я по-прежнему предпочитаю делегировать поведение вложенных замыканий на вложенный объект. Хотя он отличается от стандартного Groovy DSL impl, он, как представляется, обеспечивает лучшее разделение проблем, и, учитывая мой стиль и предпочтительный стиль моих коллег, я думаю, что в конечном итоге DSL мы будем работать над более ремонтопригодными (и это важно). Я перерабатываю реализацию, в которой в настоящее время используется BuilderSupport, что, на мой взгляд, сложно выполнить. Тем не менее, я поддержал ваши ответы за ваши усилия и помощь. Благодаря! –

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 узла в этом примере).