使用Groovy实现Domain-Specific Languages 一

2022-07-10 12:53:30 浏览数 (2)

本文翻译自官方文档

1. Command chains 链式调用

Groovy lets you omit parentheses around the arguments of a method call for top-level statements. "command chain" feature extends this by allowing us to chain such parentheses-free method calls, requiring neither parentheses around arguments, nor dots between the chained calls. The general idea is that a call like a b c d will actually be equivalent to a(b).c(d). This also works with multiple arguments, closure arguments, and even named arguments. Furthermore, such command chains can also appear on the right-hand side of assignments. Let’s have a look at some examples supported by this new syntax:

Groovy 允许函数调用的时候不写括号,"链式调用"特性又允许函数调用不写点(.),两者配置,使得函数调用的时候既不需要写括号,又不需要写点(.)。比如说a b c d等价于a(b).c(d)。这个特性同样适用多参数、闭包参数、命名参数的情况。此外,这个调用链也可以放在赋值语句的右边。下面是一些例子:

代码语言:text复制
// equivalent to: turn(left).then(right)
turn left then right

// equivalent to: take(2.pills).of(chloroquinine).after(6.hours)
take 2.pills of chloroquinine after 6.hours

// equivalent to: paint(wall).with(red, green).and(yellow)
paint wall with red, green and yellow

// with named parameters too
// equivalent to: check(that: margarita).tastes(good)
check that: margarita tastes good

// with closures as parameters
// equivalent to: given({}).when({}).then({})
given { } when { } then { }

It is also possible to use methods in the chain which take no arguments, but in that case, the parentheses are needed:

在没有参数的时候,括号不能省略:

代码语言:text复制
// equivalent to: select(all).unique().from(names)
select all unique() from names

If your command chain contains an odd number of elements, the chain will be composed of method / arguments, and will finish by a final property access:

如果链式调用包含基数个元素,那么这个链式调用的结尾就是一个获取属性的操作:

代码语言:text复制
// equivalent to: take(3).cookies
// and also this: take(3).getCookies()
take 3 cookies

这里就揭示了一个规律,链式调用正常由偶数个元素组成,奇数位置是函数名,偶数位置是参数a b c d等于a(b).c(d),上面那个例子paint wall with red, green and yellowred, green中间是逗号隔开,所以是一个整体,算一个,另外没有参数的时候,括号不能省略,也是为了遵从这一规律。

This command chain approach opens up interesting possibilities in terms of the much wider range of DSLs which can now be written in Groovy.

链式调用为使用Groovy实现DSLs提供了非常有趣的可能性。

The above examples illustrate using a command chain based DSL but not how to create one. There are various strategies that you can use, but to illustrate creating such a DSL, we will show a couple of examples - first using maps and Closures:

上面的例子只是展示了基于链式调用的于DSL,但是没说怎么实现。接下来将我们使用maps和Closures,来实现这种DSL:

代码语言:text复制
show = { println it }
square_root = { Math.sqrt(it) }

def please(action) {
  [the: { what ->
    [of: { n -> action(what(n)) }]
  }]
}

// equivalent to: please(show).the(square_root).of(100)
please show the square_root of 100
// ==> 10.0

map的语法可以参考这里,简单来说对于一个map:def colors = [red: '#FF0000', green: '#00FF00', blue: '#0000FF'],有两种方法获取key的值colors['red']colors.green

As a second example, consider how you might write a DSL for simplifying one of your existing APIs. Maybe you need to put this code in front of customers, business analysts or testers who might be not hard-core Java developers. We’ll use the Splitter from the Google Guava libraries project as it already has a nice Fluent API. Here is how we might use it out of the box:

第二个例子,对于一些已经已有的函数,我们以Google Guava libraries中的Splitter为例子,我们一般会把代码写成这样:

代码语言:text复制
@Grab('com.google.guava:guava:r09')
import com.google.common.base.*
def result = Splitter.on(',').trimResults(CharMatcher.is('_' as char)).split("_a ,_b_ ,c__").iterator().toList()

It reads fairly well for a Java developer but if that is not your target audience or you have many such statements to write, it could be considered a little verbose. Again, there are many options for writing a DSL. We’ll keep it simple with Maps and Closures. We’ll first write a helper method:

对一个Java程序员来说,这个代码写得挺漂亮,但是有一说一,确实有点冗长。有很多方法可以把他改造成DSL,但是这里我们将以Maps 和 Closures为例来简化他,定义一个帮助函数:

代码语言:text复制
@Grab('com.google.guava:guava:r09')
import com.google.common.base.*
def split(string) {
  [on: { sep ->
    [trimming: { trimChar ->
      Splitter.on(sep).trimResults(CharMatcher.is(trimChar as char)).split(string).iterator().toList()
    }]
  }]
}

now instead of this line from our original example:

有了这个帮助函数,原来这段代码:

代码语言:text复制
def result = Splitter.on(',').trimResults(CharMatcher.is('_' as char)).split("_a ,_b_ ,c__").iterator().toList()

we can write this:

就可以写成这个样子:

代码语言:text复制
def result = split "_a ,_b_ ,c__" on ',' trimming '_'

2. Operator overloading 操作符重载

Various operators in Groovy are mapped onto regular method calls on objects.

This allows you to provide your own Java or Groovy objects which can take advantage of operator overloading. The following table describes the operators supported in Groovy and the methods they map to.

在Groovy语言中,很多操作符都映射到常规的方法调用,这就可以让你自己定义的 Java或者Groovy类也能使用操作符。

Operator

Method

a b

a.plus(b)

a - b

a.minus(b)

a * b

a.multiply(b)

a ** b

a.power(b)

a / b

a.div(b)

a % b

a.mod(b)

a | b

a.or(b)

a & b

a.and(b)

a ^ b

a.xor(b)

a or a

a.next()

a-- or --a

a.previous()

a[b]

a.getAt(b)

a[b] = c

a.putAt(b, c)

a << b

a.leftShift(b)

a >> b

a.rightShift(b)

a >>> b

a.rightShiftUnsigned(b)

switch(a) { case(b) : }

b.isCase(a)

if(a)

a.asBoolean()

~a

a.bitwiseNegate()

-a

a.negative()

a

a.positive()

a as b

a.asType(b)

a == b

a.equals(b)

a != b

! a.equals(b)

a <=> b

a.compareTo(b)

a > b

a.compareTo(b) > 0

a >= b

a.compareTo(b) >= 0

a < b

a.compareTo(b) < 0

a <= b

a.compareTo(b) <= 0

3. Script base classes 脚本基类

3.1 The Script class 脚本类

Groovy scripts are always compiled to classes. For example, a script as simple as:

Groovy脚本通常被编译成类,比如下面这个脚本:

代码语言:text复制
println 'Hello from Groovy'

is compiled to a class extending the abstract groovy.lang.Script class. This class contains a single abstract method called run. When a script is compiled, then its body will become the run method, while the other methods found in the script are found in the implementing class. The Script class provides base support for integration with your application through the Binding object, as illustrated in this example:

当一个脚本被编译的时候,它将被编译成一个继承 groovy.lang.Script (这个类是abstract 的,它有一个虚函数run)的类,脚本的内容就会变成run函数的内容,脚本中别的函数就会变成这个类的成员函数。这个类可以通过Binding对象,在你的应用程序和脚本之间进行交互,比如:

代码语言:text复制
def binding = new Binding()             //1 a binding is used to share data between the script and the calling class
def shell = new GroovyShell(binding)    //2 a GroovyShell can be used with this binding
binding.setVariable('x',1)              //3 input variables are set from the calling class inside the binding
binding.setVariable('y',3)
shell.evaluate 'z=2*x y'                //4 then the script is evaluated
assert binding.getVariable('z') == 5    //5 and the `z` variable has been "exported" into the binding
  1. binding可以用来共享数据
  2. GroovyShell接受binding参数
  3. 通过binding向脚本传递变量
  4. 执行脚本
  5. 脚本可以通过binding暴露数据

This is a very practical way to share data between the caller and the script, however it may be insufficient or not practical in some cases. For that purpose, Groovy allows you to set your own base script class. A base script class has to extend groovy.lang.Script and be a single abstract method type:

这是在调用者和脚本之间传递数据最基础的做法,但是不够灵活。在Groovy调用脚本的时候,还可以使用自定义的脚本类:

代码语言:text复制
abstract class MyBaseClass extends Script {
    String name
    public void greet() { println "Hello, $name!" }
}

Then the custom script base class can be declared in the compiler configuration, for example:

然后通过CompilerConfiguration对象指定自定义脚本基类:

代码语言:text复制
def config = new CompilerConfiguration()                                //1
config.scriptBaseClass = 'MyBaseClass'                                  //2
def shell = new GroovyShell(this.class.classLoader, config)             //3
shell.evaluate """
    setName 'Judith'                                                    //4
    greet()
"""
  1. 定义一个CompilerConfiguration
  2. 指定脚本类
  3. 创建GroovyShell
  4. 现在脚本可以直接设置name属性,调用greet函数

3.2. The @BaseScript annotation @BaseScript注解

As an alternative, it is also possible to use the @BaseScript annotation directly into a script:

另外,可以直接在脚本里使用@BaseScript注解:

代码语言:text复制
import groovy.transform.BaseScript

@BaseScript MyBaseClass baseScript
setName 'Judith'
greet()

where @BaseScript should annotate a variable which type is the class of the base script. Alternatively, you can set the base script class as a member of the @BaseScript annotation itself:

但是@BaseScript只能标注一个变量,所以还可以写成这样:

代码语言:text复制
@BaseScript(MyBaseClass)
import groovy.transform.BaseScript

setName 'Judith'
greet()

这里需要注意:@BaseScript(MyBaseClass)经过我的测试,一定要放在文件的第一页。 另外通过使用这个注解,IDEA里面是可以支持代码提示的。

3.3. Alternate abstract method 虚函数的替身

We have seen that the base script class is a single abstract method type that needs to implement the run method. The run method is executed by the script engine automatically. In some circumstances it may be interesting to have a base class which implements the run method, but provides an alternative abstract method to be used for the script body. For example, the base script run method might perform some initialization before the run method is executed. This is possible by doing this:

通过上面的例子,我们可以看到,脚本引擎会自动执行脚本类的run函数,但是有时候,我们希望自定义的脚本基类能够通过实现run函数,从而进行一些初始化操作,这其实也是可以的,只要重新定义一个虚函数:

代码语言:text复制
abstract class MyBaseClass extends Script {
    int count
    abstract void scriptBody()                              //1
    def run() {
        count                                               //2
        scriptBody()                                        //3
        count                                               //4
    }
}
  1. 脚本基类重新定义唯一一个虚函数
  2. run函数在运行脚本内容之前可以做点别的事情
  3. run函数调用scriptBody,从而执行脚本类容
  4. 返回自定义内容,而不是脚本的内容

If you execute this code:

代码语言:text复制
def result = shell.evaluate """
    println 'Ok'
"""
assert result == 1

Then you will see that the script is executed, but the result of the evaluation is 1 as returned by the run method of the base class. It is even clearer if you use parse instead of evaluate, because it would allow you to execute the run method several times on the same script instance:

代码语言:text复制
def script = shell.parse("println 'Ok'")
assert script.run() == 1
assert script.run() == 2

运行第一段代码,返回结果是1,如果使用parse多次运行脚本,返回结果就是1、2、3……

0 人点赞