我现在在这墙上撞了一个多星期了,让我简要解释一下我想要实现的目标。
我有一个像这样定义的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
}
,this
和owner
,但是无法做到。
任何提示都将非常受欢迎......
干杯。
答案 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重述你当然已经知道但问题的一部分:
即使没有直接冲突,代表团的策略并不总是直接的 运行时错误并不总是很清楚(请参阅下面的闭包文档链接)
即使用像
这样的方法public getSendEmail() {
println "email for ${name}"
}
您可以使用以下语法:
myObject sendEmail
Groovy假设一个属性getter调用并完成剩下的工作。 这有助于删除dsl语法中的()。
当您使用闭包时,使用它来引用您在闭包中处理的对象 (即信息的接收者)。
示例:
[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 */