Как делегировать вызовы 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 ответа
Есть как минимум две проблемы с этим подходом:
- определяющий
ObjImpl
в том же контексте, что иGraph
означает, что любойmissingMethod
звонок ударитGraph
первый Делегация, кажется, происходит на месте, если только
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
узел в этом примере).