如何从封闭代码中访问Closure属性?

时间:2016-07-26 14:32:54

标签: groovy scope closures

我现在在这墙上撞了一个多星期了,让我简要解释一下我想要实现的目标。

我有一个像这样定义的DSL(简短示例)

TestingSpec
    .newInstance()
    .sayHello()   --> print "Hello "+something
    .sayGoddbye() --> print "Goddbye"+something 

请注意那里没有故意传递的something因为我想限制用户使用我的DSL的范围,所以我真正想要的是以某种方式“注入”{以编程方式在该DSL实例中{1}}。这是通过使用Closure来完成的。

something

Myscript.execute( { TestingSpec .newInstance() .sayHello() --> print "Hello "+something .sayGoddbye() --> print "Goddbye"+something }) 将通过传递MyScript值来调用它,方法是传递参数,或者添加属性或添加Binding(不知道什么是最好的,真的)

something

但是现在我陷入了我认为容易的问题,如何从TestingSpec代码中访问Closure close.call("Toni") close.metaClass.something = "Toni" def binding = new Binding() binding.setVariable("something", "Toni") close.setBinding(binding) ?例如

something

我尝试了几个选项,比如搞乱关闭的public newInstance() { def instance = new TestingSpec() instance.something = *MY CLOSURE SOMETHING* return instance } thisowner,但是无法做到。

任何提示都将非常受欢迎......

干杯。

3 个答案:

答案 0 :(得分:1)

我认为这不可行。 TestingSpec在堆栈上太高,无法意识到他是从一个闭包中调用的。我想建议两种解决方案

<强> 1。以编程方式将something传递给TestingSpec

Myscript.execute( {
    TestingSpec
        .newInstance(something)
        .sayHello()   --> print "Hello "+something
        .sayGoddbye() --> print "Goddbye"+something 
})

<强> 2。通过TestingSpec

创建MyScript实例

我更喜欢这个解决方案。大约MyScript.execute({})负责创建和处理TestingSpec的生命周期:

class TestingSpec {
    def something
    def sayHello() {
        println "Hello " + something
        this
    }
    def sayGoddbye() {
        println "Goddbye " + something
        this
    }
}

class Myscript {
    static TestingSpec testingSpec
    static execute(closure) {
        def binding = new Binding(something: 'test for echo')
        testingSpec = new TestingSpec(something: binding.something)
        binding.testingSpec = testingSpec
        closure.binding = binding
        closure()
    }
}

Myscript.execute( {
    testingSpec // this is an instance, and not a static call
        .sayHello()   // print "Hello "+something
        .sayGoddbye() // print "Goddbye"+something 
})

输出:

$ groovy Spec.groovy 
Hello test for echo
Goddbye test for echo

第3。将结束委托给testingSpec

如果将闭包委托设置为testingSpec,则可以直接调用其方法而不直接引用testingSpec,从而提供非常干净的代码。注意我还更新了示例以删除MyScript

class TestingSpec {
    static TestingSpec testingSpec
    static execute(closure) {
        def binding = new Binding(something: 'test for echo')
        testingSpec = new TestingSpec(something: binding.something)
        binding.testingSpec = testingSpec
        closure.delegate = testingSpec
        closure.binding = binding
        closure()
    }

    def something
    def sayHello() {
        println "Hello " + something
        this
    }
    def sayGoddbye() {
        println "Goddbye " + something
        this
    }

}

TestingSpec.execute( {
    sayHello()   // print "Hello "+something
    sayGoddbye() // print "Goddbye"+something 
})

<强> 4。将testingSpec作为参数注入到闭包中

根据您的回答,一个简单的建议:考虑向用户提供您自己构建的testingSpec var,因为它允许您进行更多控制和自定义。想一个简单的控制反转(这允许testingSpec在闭包的绑定中看到动态变量):

.withDistinctNames({ testingSpec ->
    testingSpec
        .from(name)
        .withAddress(email) 
        .sendEmail(template) 
})

进行全面测试:

class TestingSpec {
    def commands = []
    static newInstance(listOfNames) {
        new TestingSpec()
    }

    def sayHello() { commands << "sayHello"; this }
    def waveGoddbye() { commands << "waveGoddbye"; this }
    def withAddress(address) { commands << "withAddress $address"; this }
    def sendEmail(template) { commands << "sendEmail $template"; this }

    def withDistinctNames(closure) {
        commands << "withDistinctNames"
        closure.binding = new Binding(address: "sunset boulevard", template: 'email is $email')
        closure(this)
        this
    }
}

test = TestingSpec
    .newInstance([:])
    .sayHello()
    .withDistinctNames({ me ->
        me
            .withAddress(address)
            .sendEmail(template)        
    })
    .waveGoddbye()

assert test.commands == [
    'sayHello', 
    'withDistinctNames', 
    'withAddress sunset boulevard', 
    'sendEmail email is $email', 
    'waveGoddbye'
]

答案 1 :(得分:0)

我终于找到了解决我的问题的方法,虽然不理想,但优雅&#34;足够的&#34;容易&#34;了解我的DSL的潜在用户。所以,我想做的是让DSL用户写下这样的东西:

TestingSpec
    .newInstance()
    .from(listOfNames)
    .sayHello()
    .withDistinctNames({
        TestingSpec
            .newInstance()
            .from(*****) <---- problem
            .withAddress()
            .sendEmail(template)        
    })
    .waveGoddbye()   

可能有许多带有方法的基本上用作过滤器+循环,因此在此示例中,它将过滤名称为唯一,然后依次将Closure应用于每个名称。当然,只需做

就可以轻松完成
    .withDistinctNames({
        TestingSpec
            .newInstance()
            .from(it)
            .withAddress()
            .sendEmail(template)        
    })

或更复杂的案例

    .withDistinctNames({ name, email, template ->
        TestingSpec
            .newInstance()
            .from(name)
            .withAddress(email)
            .sendEmail(template)        
    })

问题是我不知道谁将使用这个DSL,可能不熟悉闭包,参数,功能,诸如此类的人,甚至可能是来自我组织外的人,所以我的目标是通过将所需的变量从外部Spec传递到内部规范来保持简单。在我的TestingSpec实现中,我会:

public TestingSpec withAll(Closure k, Closure f) { // withAll is used by all with* methods
    def result = k.call(root) // root is the entire listOfNames
    def results = result
        .collect {
            // HERE'S THE PROBLEM, HOW TO INJECT ALL THE PARAMETERS INTO THE INNER SPEC?
            f.call(it) // this line passes vars to the Closure, not the inner Spec, and I found no way for the Closure itself to inject them in the Spec
        }
    return this;
}

在我看到无法在内部Spec本身中注入params(尚未实例化)之后,我尝试将它们传递给Closure,然后从那里传递给Spec,就像这样:

    .withDistinctNames({
        TestingSpec
            .newInstance()
            .from(this) <-------------------
            .withAddress()
            .sendEmail(template)        
    })

我认为这个将是包含我需要的所有信息的外部规范,但事实并非如此。我尝试使用 this this.thisObject this.owner this.delegate

但是,在上述第3条建议的帮助下,我最终做到了这一点:

public TestingSpec withAll(Closure k, Closure f) {

    f = f.rehydrate(f.getDelegate(), f.getOwner(), this)

    def result = k.call(root) 
    def results = result
        .collect {
            this.parameters = [whatever I need]
            f.call() 
        }
    return this;
}

这样,DSL中的 this 实际上是外部规范,因此用户现在可以使用更直观的&#34;这个&#34;关键词。为了避免混淆,我摆脱了 .from()方法,如下所示:

TestingSpec
    .newInstance(listOfNames)
    .sayHello()
    .withDistinctNames({
        TestingSpec
            .newInstance(this) // my newInstance implementation can parse all the parameters as it wishes
            .withAddress()
            .sendEmail(template)        
    })
    .waveGoddbye()

虽然它不够理想,但从潜在用户的角度来看,它是足够接近的,我认为。如果有人有建议请告诉我。

感谢Will P和所有人。

答案 2 :(得分:0)

最近在使用一个类似的Groovy DSL小工具并且稍微挖掘了griffon和swing以及其他材料后,我现在以下面的内容结束了,我提交了另一个建议来回答。 任何评论/建议将不胜感激。

IMHO明确区分域模型(模型类)和支持类(构建,聚合,上下文共享和模型的过滤部分,至少是构建器 class)是其中一个关键要素 拥有一个简单而有效的模型。这是在时髦的秋千和格里芬中使用的模式,并且显示出非常灵活。

它允许使用每个{} with {} 收集操作的构造,这些构造足够干净且易于理解, 并且大部分没有链接操作(即在每个操作结束时添加恼人的返回此)。

就像这里的例子(最后的源代码, MSys 是底层系统的Facade):

MSys.with {
        model.each {
            it.sayHello
            it.sayGoodBye
            it.setAddress randomAddress(it.name);
            it.sendEmail
        }

        println " Hello counts"
        model.each {
            def key = it.name
            def value = MSys.context[key]
            println "Counter for ${key} = ${value}"
        }
    }

MSys.model.each {
        it.with {
            sayHello
            sayGoodBye
            setAddress  randomAddress(name) ;
            sendEmail
        }
    }

MSys.eachModelDo {
        sayHello
        sayGoodBye
        setAddress randomAddress(it.name);
        sendEmail
    }

或...... (很多可能性)

它还允许非常容易地拥有一些共享上下文(在示例中, context 是在所有可以放置任何内容的模型元素之间共享的Map,连接信息,用户首选项,缓存等。), 并通过将所有锅炉板放在另一个类/脚本中来隐藏所有锅炉板。

责任是:

  • SpecModel :域模型:打招呼,再见,属性等

  • SpecBuilder :创建模型(来自示例中的列表),保存共享上下文(地图)并最终处理某些操作的闭包委托上下文

    < / LI>

这种分离对于一方面处理设置操作和另一方面处理实体(模型)操作非常重要。 除了建造者之外,用户POV还是一个门面

从开发人员POV BTW开始,它应该是几个类的Facade,包括构建器 - 但最好从简单开始。

下一步是在这个构建器中集成FactoryBuilderSupport,以便从Groovy DSL构建器工具中受益,但这又一个很大的进步,我对此感到不舒服......(现在的WIP)< / p>

如果你想要一个轻量级的语法,很少有Groovy重述你当然已经知道但问题的一部分:

  • 在Groovy中; ()是可选的,除非发生冲突。

即使没有直接冲突,代表团的策略并不总是直接的 运行时错误并不总是很清楚(请参阅下面的闭包文档链接)

  • 拥有属性的公共getter方法,允许使用不带()的访问方法之类的属性, 推断出get和set前缀

即使用像

这样的方法
public getSendEmail() {
    println "email for ${name}"
}

您可以使用以下语法:

myObject sendEmail

Groovy假设一个属性getter调用并完成剩下的工作。 这有助于删除dsl语法中的()

  • 上下文是闭包的委托上下文而不是 this

当您使用闭包时,使用来引用您在闭包中处理的对象 (即信息的接收者)。

示例:

[1, 2, 3].each { println it }

没问题

如果您使用过

[1, 2, 3].each { println this }

您将打印对外部对象的引用,即groovy控制台中的控制台

(哪个IIRC是你在第一篇文章中遇到的问题之一)

Groovy Closures doc:

对此进行了详细解释

(摘录:)

  • 这个:定义闭包的封闭类
  • 所有者:定义闭包的封闭对象,类或闭包
  • 委托:只要未定义消息的接收方,就会解析方法调用或属性的第三方对象

消息委派策略(在同一文档中解释)并不总是直接的,这是我认为的真正问题。

也就是说,DSL中的另一个关键点是来自用户POV的一致性。 这意味着数据访问模式的一致性,这里标准的Groovy集合(列表地图 每个{} 等)可以提供很多帮助。 此外,对于高级用户来说,可能是一个巨大的优势

eachModelDo 等语法糖方法可以在构建器/外观类上轻松完成:

MSys.eachModelDo {
        sayHello
        sayGoodBye
        setAddress randomAddress(it.name);
        sendEmail
    }

注意:eachModelDo非常简单,但调试有点棘手

有一次它工作了#34;没有访问正确的变量:(

我觉得这里有问题(?)或者至少应该改进(欢迎评论)

/**
 *  syntactic sugar
 *  direct access without 'it' (optional)
 */
public SpecBuilder eachModelDo(closure) {
    model.each {
        closure.delegate = it;
        closure(it)
    }
}

Bellow是我做过的小测试的源代码,你可以在一个时髦的控制台中剪切和粘贴

用户可见的唯一部分应该是方法 Demo.run() 所有其他的东西都应该隐藏

欢迎任何评论

/**
 * The builder build Specs and defines utility methods, filters
 * It shares its context with all elements in the domain
 */
class SpecBuilder {
    /** the context will be shared with all domain model objects */
    private context
    /** model = all the domain model objects */
    private model

    /** getters / setters */
    public getModel() { return model }

    public getContext() {
        return context
    }

    /** constructors and helpers */
    public SpecBuilder(aContext) {
        context = aContext
    }
    /** Default constructor forbidden */
    private SpecBuilder() {}

    public from(aList, closure) {
        from(aList);
        model.each { closure(it) }
        return this
    }

    public from(aList) {
        model = aList.collect { new SpecModel(it, context) }
        return this
    }



    /* TODO filters etc */


    /** stats: print counters */
    public stats() {
        println " Hello counts"
        model.each {
            def key = it.name
            def value = this.context[key]
            println "Counter for ${key} = ${value}"
        }
    }
    /**
     *  syntactic sugar
     *  direct access without 'it' (optional)
     */
    public SpecBuilder eachModelDo(closure) {
        model.each {
            closure.delegate = it;
            closure(it)
        }
    }
}

/**
 * The Spec Domain  Model
 */
class SpecModel {

    /** the shared context */
    private context;
    /** other properties */
    private name;
    public address;

    /** getters and setters */
    public getName() { return name }
    public void setAddress(a) { address = a }
    public getAddress() { return address }
    public sayHello() { return getSayHello }
    public sayGoodBye() { return getSayGoodBye }
    public sendEmail() { return getSendEmail }

    /** constructors */
    public SpecModel(aName, aContext) {
        name = aName
        context = aContext
    }

    /** Default constructor forbidden */
    private SpecModel() {}

    /**  method used like properties, without 'get' and ()*/
    public getSayHello() {
        println "(!) hello ${name}"
        context[name] = context.get(name,0) +1;
    }
    public getSayGoodBye() {
        println "goodBye ${name} !"
    }
    public getSendEmail() {
        println "email for ${name}"
        if (address)
            println "Address ${address}"
    }
    public getPrintContext() {
        println context
    }
    /**
     * Returns info to caller
     */
    public gatherInfo() {
        "info for ${name} : ${new java.util.Random().nextInt(50000)}"
    }


}

class Demo {

    // several Groots here to test uniques ...
    def customers = ['Groot', 'Gamora', 'Groot', 'Groot', 'Groot', 'Star-Lord']

    /**
     * Utility function who generates a random address
     * @param name will prefix the address
     * @return the address
     */
    public randomAddress(def name) {
        // good places ... :)
        def places = [
                "Grande Rue",
                "Cours Emile Zola",
                "Place Antonin Poncet",
                "Rue de la République",
                "Boulevard de la Croix Rousse",
                "Place Bellecour"
        ]
        def random = new java.util.Random();
        return  new StringBuilder().append(name).append(" ... ")
                        // why not 42?
                        .append( random.nextInt(155)).append("  ")
                        .append( places[random.nextInt(places.size())] )
                .toString();
    }

    /**
     * ======================== Main user program =========================
     */
    def run() {
        /**  the shared context */
        def context = [:].asSynchronized() // In case of multi threading access

        /** The whole system From a user POV : a big façade */
        def MSys = new SpecBuilder(context).from(customers) ;


        println "*** 1 ==================== "
        /** First form */
        MSys.model.each {
            it.with {
                sayHello
                sayGoodBye
                setAddress  randomAddress(name) ;
                sendEmail
            }
        }
        /** other forms
         * MSys.with{  is handy
         * one could write MSys... on each line
         */
        MSys.with {

            println "*** 2 ==================== "
            model.each { it.sayHello };

            println "*** 3 ==================== "
            model.with { println " a Model entry = ${it.name} + ${it.address}" }

            println "*** 4 ==================== "
            /** false not to mutate the model !!! */
            model.unique(false, { a, b -> a.name <=> b.name }).each { it.sayHello }

            println "*** 5 ==================== "
            context['aKey'] = 42
            // verify that each entity has the same context
            model.with { println " a shared context for ${it.name} : " + it.context }

            println "*** Stats ================ "
            /** stats on the shared context */
            stats()
        }

        println "*** 6 Info to process ======== "

        /** Gather info to process (addresses)*/
        def data = MSys.model.inject([:]) { result, entity ->
            result[entity.name] = entity.address
            result
        }
        println data

        MSys.with {

            println "*** 7 ==================== "
            model.each {
                it.sayHello
                it.sayGoodBye
                it.setAddress randomAddress(it.name);
                it.sendEmail
            }

            println "*** 8 ==================== "
            println " Hello counts"
            model.each {
                def key = it.name
                def value = MSys.context[key]
                println "Counter for ${key} = ${value}"
            }
        }
        println "*** 9 ==================== "
        MSys.eachModelDo {
            sayHello
            sayGoodBye
            setAddress randomAddress(it.name);
            sendEmail
        }

    }
}

new Demo().run()

/* end of script */