Groovy

Make Your Groovy Objects More Bullet-Proof

Groovy has the Immutable annotation which allows to create immutable classes, which is a prerequisite for creating value objects. Unfortunately, when a class has been annotated with Immutable it’s no longer possible to add your own constructor to verify if provided parameters are not null, which would make our value objects really bullet-proof.

@groovy.transform.Immutable
class Money {
    BigDecimal amount
}

def money = new Money() // we can just instantiate without an amount!
assert money.amount == null

Of course, Money here represents a typical example of a value object — immutable after creation, comparable by value e.g. the amount. In some of my code bases I use value objects a lot and it annoyed me that there was no way to prevent, in this case, the amount from being null — by assigning it null or leaving it to its default value.

Explicit constructors

Normally I could just create a custom constructor, verifying all parameters are valid according to my own business rules e.g. such as checking for nulls:

@groovy.transform.Immutable
class Money {
    BigDecimal amount

    Money(BigDecimal amount) {
        if (amount == null) {
            throw new IllegalArgumentException("Amount can not be null")
        }
        this.amount = amount
    }
}

def money = new Money()
// normally would throw IllegalArgumentException: "Amount can not be null"

Normally having an own constructor would be fine and throw an IllegalArgumentException in above example, but unfortunately the Immutable annotation prevents this.

org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
Script1.groovy: 6: Explicit constructors not allowed for @Immutable class: Money
 @ line 6, column 17.
                   Money(BigDecimal amount) {
                   ^

1 error

Factory method

I had to scratch my itch and deal with it somehow.

Then I would create a factory method instead, doing my custom checks there.

@Immutable
class Money {
    BigDecimal amount

    static Money of(BigDecimal amount) {
        if (amount == null) {
            throw new IllegalArgumentException("Amount can not be null")
        }
        new Money(amount)
    }
}
def money1 = Money.of(3) // ok
def money2 = Money.of() // good, fails with IllegalArgumentException

But one could still bypass my static of factory method and create an invalid Money directly with the default constructor.

def money3 = new Money() // still works, argh!

So what can you do?

The obvious thing is making sure there are no constructors any more to call. E.g. introduce your own private constructor.

@Immutable
class Money {
    BigDecimal amount

    private Money() {
        throw new IllegalArgumentException("Use of() method instead")
    }

    static Money of(...

but…@Immutable prevents you from adding your own constructor in the first place — or overriding the default Map-constructor — remember?

The only option I could think of, besides creating my own custom version of the existing @Immutable transformation, is to do something after it has already modified the class.

And one could do that with…

AST transformations

Bulletproof helps to fill this gap by adding a few AST transformations.

  • The NonNull annotation which modifies every constructor to perform null-checks. Add this to an Immutable class and no null slips past your constructor.
  • The ValueObject meta-annotation which puts both NonNull and Immutable on your class as a convenience to do above step with one annotation.

More details can be found on GitHub, but basically you just add the dependency and use one of the annotations.

NonNull

The NonNull annotation on the class-level triggers an AST transformation which modifies every constructor to perform a null-check.

@Immutable
@tvinke.bulletproof.transform.NonNull
class Person {
    String name
}
new Person() // throws IllegalArgumentException: "Name can not be null"

How does it work?

In the curent initial version, @NonNull can only be applied at the class-level.

The associated AST transformation modifies the class and

  1. adds a “checker” method for each property checking the value for being non-null.
  2. adds an “uber checker” method calling each of above individual checks
  3. modifies each constructor and adds a call to above uber checker method as the last statement

So, something simple as

@NonNull
class Person {
    String name
}

ends up in the compiled version (behaviour-wise) roughly as

@NonNull
class Person {
    String name
    Person(String name) {
        this.name = name
        if (this.name == null) {
            throw new IllegalArgumentException("Name can not be null")
        }
    } 
}

Modifying any existing constructor (however they got there!) and doing the checks as the last statements, right now seemed the most sensible thing to do: iterate any constructor, have them do their own logic first if any and finally as a post-condition make sure the values are not ending up as null.

Value Object

The ValueObject meta-annotation combines the Immutable and NonNull annotations together.

@tvinke.bulletproof.transform.ValueObject
class Money {
    BigDecimal amount
}

new Money(amount: null)
// throws IllegalArgumentException because of NonNull

def money = new Money(2.95)
money.amount = 3.0
// throws regular ReadOnlyPropertyException because of Immutable

By then including this library in my project, I replaced all my @Immutable annotations by @ValueObject and was done with it!

Value Objects all the way!

I had some challenges finding out in which compile phase my AST transforms had to run, to make sure I would see the constructors already added earlier by the Immutable annotation. Some Q and A on Groovy’s Slack channel helped me a lot, including getting some initial feedback on the code.

If you would like to check it out:

Ted Vinke

Ted is a Java software engineer with a passion for Web development and JVM languages and works for First8, a Java Web development company in the Netherlands.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button