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 anImmutable
class and nonull
slips past your constructor. - The
ValueObject
meta-annotation which puts bothNonNull
andImmutable
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
- adds a “checker” method for each property checking the value for being non-null.
- adds an “uber checker” method calling each of above individual checks
- 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:
- Head over to https://github.com/tvinke/bulletproof and let me know what can be improved or suggestions otherwise.
- If you have an issue, use the Issue Tracker.
Reference: | Make Your Groovy Objects More Bullet-Proof from our JCG partner Ted Vinke at the Ted Vinke’s Blog blog. |