使用Groovy实现Domain-Specific Languages 二

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

本文翻译自官方文档

上一篇地址

4. Adding properties to numbers 给数字添加属性

In Groovy number types are considered equal to any other types. As such, it is possible to enhance numbers by adding properties or methods to them. This can be very handy when dealing with measurable quantities for example. Details about how existing classes can be enhanced in Groovy are found in the extension modules section or the categories section.

在Groovy语言中,数字类型和其它所有类型地位相同,因此,可以通过为数字添加属性或者函数,来对数字类型进行增强。这个在你处理一个可测量的数据是非常有用。关于增强一个已有类型的内容可以参考这两个地方 extension modules section or the categories section。

An illustration of this can be found in Groovy using the TimeCategory:

下面以 TimeCategory为例:

代码语言:text复制
use(TimeCategory)  {
    println 1.minute.from.now       //1
    println 10.hours.ago

    def someDate = new Date()       //2
    println someDate - 3.months
}
  1. 通过TimeCategory,给Integer添加了一个minute属性。
  2. 类似的,months函数返回一个 groovy.time.DatumDependentDuration类型,支持计算。

Categories are lexically bound, making them a great fit for internal DSLs.

这个给人的感觉就是语言的一部分,用在DSL中就很赞。

5. @DelegatesTo

5.1. Explaining delegation strategy at compile time 什么是编译时委托策略

@groovy.lang.DelegatesTo is a documentation and compile-time annotation aimed at:

  • documenting APIs that use closures as arguments
  • providing type information for the static type checker and compiler

@groovy.lang.DelegatesTo 是一个注解,它的主要用途是:

  • 在将一个闭包作为参数使用时,记录它的API
  • 为静态类型检查以及编译器提供类型信息

The Groovy language is a platform of choice for building DSLs. Using closures, it’s quite easy to create custom control structures, as well as it is simple to create builders. Imagine that you have the following code:

Groovy 语言是构建 DSL 的首选平台。 使用闭包,可以很容易创建自定义控制结构,也很容易创建构建器。假设您有以下代码:

代码语言:text复制
email {
    from 'dsl-guru@mycompany.com'
    to 'john.doe@waitaminute.com'
    subject 'The pope has resigned!'
    body {
        p 'Really, the pope has resigned!'
    }
}

One way of implementing this is using the builder strategy, which implies a method, named email which accepts a closure as an argument. The method may delegate subsequent calls to an object that implements the from, to, subject and body methods. Again, body is a method which accepts a closure as an argument and that uses the builder strategy.

实现这种效果的一种方法是使用构造器策略,定义一个email的函数,它接受一个闭包作为参数。这个函数可以将后续调用委托给实现“from”、“to”、“subject”和“body”方法的对象。同样,body也是一个接受闭包作为参数并使用构建器策略的方法。

Implementing such a builder is usually done the following way:

通常通过一下方式实现构造器:

代码语言:text复制
def email(Closure cl) {
    def email = new EmailSpec()
    def code = cl.rehydrate(email, this, this)
    code.resolveStrategy = Closure.DELEGATE_ONLY
    code()
}

the EmailSpec class implements the from, to, … methods. By calling rehydrate, we’re creating a copy of the closure for which we set the delegate, owner and thisObject values. Setting the owner and the this object is not very important here since we will use the DELEGATE_ONLY strategy which says that the method calls will be resolved only against the delegate of the closure.

首先EmailSpec类实现了 from, to这些函数。接着,通过rehydrate函数,我们创建了原始闭包的一个副本,三个参数分别是: delegate, owner and thisObject,在这个例子中,后面两个参数不重要,只是随便赋个值,因为在第三行我们设置了 DELEGATE_ONLY 策略,它的效果是闭包里面的那些函数统统都在email中找:

代码语言:text复制
class EmailSpec {
    void from(String from) { println "From: $from"}
    void to(String... to) { println "To: $to"}
    void subject(String subject) { println "Subject: $subject"}
    void body(Closure body) {
        def bodySpec = new BodySpec()
        def code = body.rehydrate(bodySpec, this, this)
        code.resolveStrategy = Closure.DELEGATE_ONLY
        code()
    }
}

The EmailSpec class has itself a body method accepting a closure that is cloned and executed. This is what we call the builder pattern in Groovy.

EmailSpec 类本身有一个 body 函数,该函数接受闭包。 这就是我们在 Groovy 中所说的构建器模式。

One of the problems with the code that we’ve shown is that the user of the email method doesn’t have any information about the methods that he’s allowed to call inside the closure. The only possible information is from the method documentation. There are two issues with this: first of all, documentation is not always written, and if it is, it’s not always available (javadoc not downloaded, for example). Second, it doesn’t help IDEs. What would be really interesting, here, is for IDEs to help the developer by suggesting, once they are in the closure body, methods that exist on the email class.

但是这有个问题,就是如果写代码的人不看文档,或者没有文档,他就不知道他能用什么功能,另外就是对于IDE来说,他也没法提供代码提示功能。

Moreover, if the user calls a method in the closure which is not defined by the EmailSpec class, the IDE should at least issue a warning (because it’s very likely that it will break at runtime).

此外,如果用户在闭包中调用了 EmailSpec 类未定义的方法,IDE 至少应该发出警告(因为它很可能会在运行时中断)。

One more problem with the code above is that it is not compatible with static type checking. Type checking would let the user know if a method call is authorized at compile time instead of runtime, but if you try to perform type checking on this code:

上面代码的另一个问题是它与静态类型检查不兼容,类型检查可以让问题在编译时而不是运行时就暴露出来,但是如果您尝试对此代码执行类型检查:

代码语言:text复制
email {
    from 'dsl-guru@mycompany.com'
    to 'john.doe@waitaminute.com'
    subject 'The pope has resigned!'
    body {
        p 'Really, the pope has resigned!'
    }
}

Then the type checker will know that there’s an email method accepting a Closure, but it will complain for every method call inside the closure, because from, for example, is not a method which is defined in the class. Indeed, it’s defined in the EmailSpec class and it has absolutely no hint to help it knowing that the closure delegate will, at runtime, be of type EmailSpec:

类型检查工具知道有个email方法,它接受一个Closure作为参数,这个没问题,但是当他检查闭包内部的函数的时候,他就懵逼了。

代码语言:text复制
@groovy.transform.TypeChecked
void sendEmail() {
    email {
        from 'dsl-guru@mycompany.com'
        to 'john.doe@waitaminute.com'
        subject 'The pope has resigned!'
        body {
            p 'Really, the pope has resigned!'
        }
    }
}

will fail compilation with errors like this one:

最终它有可能会报错:

代码语言:text复制
[Static type checking] - Cannot find matching method MyScript#from(java.lang.String). Please check if the declared type is correct and if the method exists.
 @ line 31, column 21.
                       from 'dsl-guru@mycompany.com'

5.2. @DelegatesTo

For those reasons, Groovy 2.1 introduced a new annotation named @DelegatesTo. The goal of this annotation is to solve both the documentation issue, that will let your IDE know about the expected methods in the closure body, and it will also solve the type checking issue, by giving hints to the compiler about what are the potential receivers of method calls in the closure body.

为了解决这个问题,Groovy 2.1引入了一个新注解@DelegatesTo

The idea is to annotate the Closure parameter of the email method:

email函数的Closure参数注解:

代码语言:text复制
def email(@DelegatesTo(EmailSpec) Closure cl) {
    def email = new EmailSpec()
    def code = cl.rehydrate(email, this, this)
    code.resolveStrategy = Closure.DELEGATE_ONLY
    code()
}

What we’ve done here is telling the compiler (or the IDE) that when the method will be called with a closure, the delegate of this closure will be set to an object of type email. But there is still a problem: the default delegation strategy is not the one which is used in our method. So we will give more information and tell the compiler (or the IDE) that the delegation strategy is also changed:

但是我们在代码中设置了代理策略是DELEGATE_ONLY,所以还需要再注释中添加策略信息:

代码语言:text复制
def email(@DelegatesTo(strategy=Closure.DELEGATE_ONLY, value=EmailSpec) Closure cl) {
    def email = new EmailSpec()
    def code = cl.rehydrate(email, this, this)
    code.resolveStrategy = Closure.DELEGATE_ONLY
    code()
}

Now, both the IDE and the type checker (if you are using @TypeChecked) will be aware of the delegate and the delegation strategy. This is very nice because it will both allow the IDE to provide smart completion, but it will also remove errors at compile time that exist only because the behaviour of the program is normally only known at runtime!

这就很完美。

The following code will now pass compilation:

代码语言:text复制
@TypeChecked
void doEmail() {
    email {
        from 'dsl-guru@mycompany.com'
        to 'john.doe@waitaminute.com'
        subject 'The pope has resigned!'
        body {
            p 'Really, the pope has resigned!'
        }
    }
}

5.3. DelegatesTo modes 代理模式

@DelegatesTo supports multiple modes that we will describe with examples in this section.

下面介绍一下代理模式

5.3.1. Simple delegation 简单代理

In this mode, the only mandatory parameter is the value which says to which class we delegate calls. Nothing more. We’re telling the compiler that the type of the delegate will always be of the type documented by @DelegatesTo (note that it can be a subclass, but if it is, the methods defined by the subclass will not be visible to the type checker).

再这种模式下,我们告诉编译器我们只会再我们注解的类型里面解析(即使是注解类型的子类型,子类型里面定义的内容也是不可以见的)

但是我觉得这个地方说得不对,也可能是我没理解不对。

代码语言:text复制
void body(@DelegatesTo(BodySpec) Closure cl) {
    // ...
}
5.3.2. Delegation strategy 代理策略

In this mode, you must specify both the delegate class and a delegation strategy. This must be used if the closure will not be called with the default delegation strategy, which is Closure.OWNER_FIRST.

在这种模式下,可以额外执行代理策略,默认的策略是:Closure.OWNER_FIRST

代码语言:text复制
void body(@DelegatesTo(strategy=Closure.DELEGATE_ONLY, value=BodySpec) Closure cl) {
    // ...
}
5.3.3. Delegate to parameter 代理变量

In this variant, we will tell the compiler that we are delegating to another parameter of the method. Take the following code:

在这里,我打算直接代理一个变量

上面的例子,都是写死了被代理的对象类型,现在希望我们提供什么类型,就代理什么类型

代码语言:text复制
def exec(Object target, Closure code) {
   def clone = code.rehydrate(target, this, this)
   clone()
}

Here, the delegate which will be used is not created inside the exec method. In fact, we take an argument of the method and delegate to it. Usage may look like this:

在这里,我们要代理的对象不是在exec函数里面创建的,而是通过参数传进来的:

代码语言:text复制
def email = new Email()
exec(email) {
   from '...'
   to '...'
   send()
}

Each of the method calls are delegated to the email parameter. This is a widely used pattern which is also supported by @DelegatesTo using a companion annotation:

这也是@DelegatesTo另外一个很常用的一种用法:

代码语言:text复制
def exec(@DelegatesTo.Target Object target, @DelegatesTo Closure code) {
   def clone = code.rehydrate(target, this, this)
   clone()
}

A closure is annotated with @DelegatesTo, but this time, without specifying any class. Instead, we’re annotating another parameter with @DelegatesTo.Target. The type of the delegate is then determined at compile time. One could think that we are using the parameter type, which in this case is Object but this is not true. Take this code:

closure还是用@DelegatesTo标注,但是另外参数使用 @DelegatesTo.Target注解,这样就行了:

代码语言:text复制
class Greeter {
   void sayHello() { println 'Hello' }
}
def greeter = new Greeter()
exec(greeter) {
   sayHello()
}

Remember that this works out of the box without having to annotate with @DelegatesTo. However, to make the IDE aware of the delegate type, or the type checker aware of it, we need to add @DelegatesTo. And in this case, it will know that the Greeter variable is of type Greeter, so it will not report errors on the sayHello method even if the exec method doesn’t explicitly define the target as of type Greeter. This is a very powerful feature, because it prevents you from writing multiple versions of the same exec method for different receiver types!

其实这里不用 @DelegatesTo标注也是可以的,但是这里加上 @DelegatesTo可以给IDE提供额外的信息,这真是一个非常牛逼特性。

In this mode, the @DelegatesTo annotation also supports the strategy parameter that we’ve described upper.

在这个模式中, @DelegatesTo 注解依然支持 strategy

5.3.4. Multiple closures 多个闭包的情况

In the previous example, the exec method accepted only one closure, but you may have methods that take multiple closures:

上面的例子,只有一个闭包,但是如果有多个闭包的情况:

代码语言:text复制
void fooBarBaz(Closure foo, Closure bar, Closure baz) {
    ...
}

Then nothing prevents you from annotating each closure with @DelegatesTo:

依然可以使用@DelegatesTo标记代理

代码语言:text复制
class Foo { void foo(String msg) { println "Foo ${msg}!" } }
class Bar { void bar(int x) { println "Bar ${x}!" } }
class Baz { void baz(Date d) { println "Baz ${d}!" } }

void fooBarBaz(@DelegatesTo(Foo) Closure foo, @DelegatesTo(Bar) Closure bar, @DelegatesTo(Baz) Closure baz) {
   ...
}

But more importantly, if you have multiple closures and multiple arguments, you can use several targets:

还有更牛逼了,多个代理变量、多个闭包。

代码语言:text复制
void fooBarBaz(
    @DelegatesTo.Target('foo') foo,
    @DelegatesTo.Target('bar') bar,
    @DelegatesTo.Target('baz') baz,

    @DelegatesTo(target='foo') Closure cl1,
    @DelegatesTo(target='bar') Closure cl2,
    @DelegatesTo(target='baz') Closure cl3) {
    cl1.rehydrate(foo, this, this).call()
    cl2.rehydrate(bar, this, this).call()
    cl3.rehydrate(baz, this, this).call()
}

def a = new Foo()
def b = new Bar()
def c = new Baz()
fooBarBaz(
    a, b, c,
    { foo('Hello') },
    { bar(123) },
    { baz(new Date()) }
)

At this point, you may wonder why we don’t use the parameter names as references. The reason is that the information (the parameter name) is not always available (it’s a debug-only information), so it’s a limitation of the JVM.

你可能会觉得奇怪,这里为什么不直接通过指定变量名来建立对应关系,这是JVM的一个限制。

5.3.5. Delegating to a generic type 对泛型的代理

In some situations, it is interesting to instruct the IDE or the compiler that the delegate type will not be a parameter but a generic type. Imagine a configurator that runs on a list of elements:

有时候,对于一个泛型类型,我们怎么让编译器或者IDE知道我们代理的是什么类型呢:

注意这里的策略是Closure.DELEGATE_FIRST,我猜是第一个元素类型的意思

代码语言:text复制
public <T> void configure(List<T> elements, Closure configuration) {
   elements.each { e->
      def clone = configuration.rehydrate(e, this, this)
      clone.resolveStrategy = Closure.DELEGATE_FIRST
      clone.call()
   }
}

Then this method can be called with any list like this:

然后这么使用:

代码语言:text复制
@groovy.transform.ToString
class Realm {
   String name
}
List<Realm> list = []
3.times { list << new Realm() }
configure(list) {
   name = 'My Realm'
}
assert list.every { it.name == 'My Realm' }

To let the type checker and the IDE know that the configure method calls the closure on each element of the list, you need to use @DelegatesTo differently:

代码语言:text复制
public <T> void configure(
    @DelegatesTo.Target List<T> elements,
    @DelegatesTo(strategy=Closure.DELEGATE_FIRST, genericTypeIndex=0) Closure configuration) {
   def clone = configuration.rehydrate(e, this, this)
   clone.resolveStrategy = Closure.DELEGATE_FIRST
   clone.call()
}

@DelegatesTo takes an optional genericTypeIndex argument that tells what is the index of the generic type that will be used as the delegate type. This must be used in conjunction with @DelegatesTo.Target and the index starts at 0. In the example above, that means that the delegate type is resolved against List<T>, and since the generic type at index 0 is T and inferred as a Realm, the type checker infers that the delegate type will be of type Realm.

这里主要是genericTypeIndex参数表示第几个泛型类型,这里第0个,就是Realm

We’re using a genericTypeIndex instead of a placeholder (T) because of JVM limitations.

同样是由于JVM的限制,使用genericTypeIndex而不是T

5.3.6. Delegating to an arbitrary type 代理任意的类型

下面这段有段没懂,感觉可能也用不到,就不翻译了。

It is possible that none of the options above can represent the type you want to delegate to. For example, let’s define a mapper class which is parametrized with an object and defines a map method which returns an object of another type:

代码语言:text复制
class Mapper<T,U> {                             
    final T value                               
    Mapper(T value) { this.value = value }
    U map(Closure<U> producer) {                
        producer.delegate = value
        producer()
    }
}

The mapper class takes two generic type arguments: the source type and the target type

The source object is stored in a final field

The map method asks to convert the source object to a target object

As you can see, the method signature from map does not give any information about what object will be manipulated by the closure. Reading the method body, we know that it will be the value which is of type T, but T is not found in the method signature, so we are facing a case where none of the available options for @DelegatesTo is suitable. For example, if we try to statically compile this code:

代码语言:text复制
def mapper = new Mapper<String,Integer>('Hello')
assert mapper.map { length() } == 5

Then the compiler will fail with:

代码语言:text复制
Static type checking] - Cannot find matching method TestScript0#length()

In that case, you can use the type member of the @DelegatesTo annotation to reference T as a type token:

代码语言:text复制
class Mapper<T,U> {
    final T value
    Mapper(T value) { this.value = value }
    U map(@DelegatesTo(type="T") Closure<U> producer) {  
        producer.delegate = value
        producer()
    }
}

The @DelegatesTo annotation references a generic type which is not found in the method signature

Note that you are not limited to generic type toke

0 人点赞