Ambuj Jauhari
Ambuj Jauhari

Reputation: 1299

Default Values in groovy @Builder AST

I am new to groovy and i am just trying to learn here. I have a simple pojo as below, i am trying to have a builder pattern here. Now the class is annotated with @Builder annotaion.

@Builder
class Invert {
      String color = 'Green'
      String code
}

In my main class if create a object of Invert class, variable color is always null.

Invert in = new Invert.builder().code('0000').build()

Object is created with code as 0000 but color as null. Is it expected, if so is there any workaround for this or i am missing something here ?

Upvotes: 0

Views: 2483

Answers (2)

pczeus
pczeus

Reputation: 7868

The Builder API expects you to initialize each property using the builder syntax. By not specifying the value for the 'color' property, you are in effect silently specifying that the value should be null, thus overriding your 'Green' default value.

You can adjust your code to something like the following, which is a simple groovy script I ran to test:

import groovy.transform.builder.*
import groovy.transform.*

@Canonical
@Builder
class Invert {
    String color
    String code
}

def invert = new Invert().builder().code('000').color('Green').build()
println invert

Not that I explicitly set the color attribute, using the Builder syntax, with results:

Invert(Green, 000)

Also notice the @Canonical annotation used. This is a nice annotation that combines several annotations:

@EqualsAndHashCode, @ToString and @TupleConstructor

Besides providing a nice toString() output, which you can see in the output of running the script, the TupleConstructor is (in my opinion) much more preferable to using the @Builder type pattern.

With the @TupleConstructor, you can simple construct your Invert object like this:

def invert = new Invert(code:'000', color:'Green')

In addition, the TupleConstructor allows you to keep your default value you had set for the color and if you don't set it in the Constructor, it will not be overridden:

import groovy.transform.*

@Canonical
class Invert {
    String color = 'Green'
    String code
}

def invert = new Invert(code:'000')
println invert

With results:

Invert(Green, 000)

So, if it were me, I would remove the @Builder and use @Canonical instead as a best practice. For more information on @Canonical, see:

http://docs.groovy-lang.org/next/html/gapi/groovy/transform/Canonical.html

Upvotes: 0

Emmanuel Rosa
Emmanuel Rosa

Reputation: 9895

First, let me show you what's going on. I'll start with the Invert class:

@groovy.transform.builder.Builder
class Invert {
      String color = 'Green'
      String code
}

So that's the same class from your example, with the only difference being a fully-qualified name for @Builder. But check out what happens when Groovy compiles the code (this is the relevant code from the AST viewer in the Groovy console).

Invert class

@groovy.transform.builder.Builder
public class Invert implements groovy.lang.GroovyObject extends java.lang.Object { 

    private java.lang.String color 
    private java.lang.String code 
    ...

    public Invert() {
        color = 'Green'
        metaClass = /*BytecodeExpression*/
    }

    public static Invert$InvertBuilder builder() {
        return new Invert$InvertBuilder()
    }

    ...
}

Nothing surprising, although note that the color is set to green in the constructor.

Invert$InvertBuilder class

public static class Invert$InvertBuilder implements groovy.lang.GroovyObject extends java.lang.Object { 

    private java.lang.String color 
    private java.lang.String code 
    ...

    public Invert$InvertBuilder color(java.lang.String color) {
        this .color = color 
        return this 
    }

    public Invert$InvertBuilder code(java.lang.String code) {
        this .code = code 
        return this 
    }

    public Invert build() {
        Invert _theInvert = new Invert()
        _theInvert .color = color 
        _theInvert .code = code 
        return _theInvert 
    }    
    ...

}

The Invert$InvertBuilder class is created by the @Builder AST. It's what provides the fluent API.

The problem

Did you catch the source of the problem? The builder contains its own color and code fields. Then, when build() is called:

  1. An instance of Invert is created. At this point, the instance's color is green.
  2. The builder applies its own color and code fields to the Invert instance. At this point the color is changed to null because that's the default in the builder.

The solution

To solve this issue with a builder, use the ExternalStrategy to define your own builder class, in which you can set the color as you'd like. Here's a working example:

class Invert {
      String color
      String code

      static InvertBuilder builder() {
          new InvertBuilder()
      }
}

@groovy.transform.builder.Builder(builderStrategy=groovy.transform.builder.ExternalStrategy, forClass=Invert)
class InvertBuilder {
    InvertBuilder() {
        color = 'Green'
    }
}

Invert invert = Invert.builder().code('0000').build()

assert invert.color == 'Green'
assert invert.code == '0000'

Alternatives

Groovy provides some alternative building methods you may be interested in.

Invert invert1 = new Invert(code: '0000', color: 'Blue') // Using the Map-based constructor

Invert invert2 = new Invert().with { // using Object.with(Closure)
    code = '0000'
    color = 'Blue'

    return delegate
}

Upvotes: 2

Related Questions