After a bug I recently introduced to production was fixed, I set out to write about what happened as a sort of post-mortem exercise. That ends up being covered mostly in the last section of this post, but found some other best practice stuff buried in a readme file I had written a while back that was also relevant, so just posting that all together, ‘cause why not.
In Grails services specifically I have found that it is typically better to be a bit more Java like when possible. Groovy lets you just def
everything, or even omit types declarations altogether and that can be great at times. I don’t tend to spend much time fighting with the compiler in Groovy. In Grails the service layer is where a lot of your business logic is supposed to live. Methods here tend to be reused or are set up to be used by multiple controllers or whatever. Being a bit more explicit with defining the method signature can be very helpful, when used with @CompileStatic
this constitutes a sort of test that automatically runs at compile time as well.
Here is what that looks like.
Include types
Whenever possible specify the return type of service methods and the types of arguments they expect.
❌ Bad
def isThisThingLegit(thing, user)
✅ Good
Boolean isThisThingLegit(Thing thing, AppUser user)
Naming
Good naming is just generally good practice - this is not Groovy or Grails specific of course - but given that Java programs are often overly or painfully verbose in their naming, I have seen developers in Grails sometimes over-correct, naming things to way to vaguely. Mostly refering to my past self here…
The naming stuff could probably go without saying except that Grails sits in this spot where a lot of the people I have worked alongside in Grails projects came either from either a Java background, a Rails background, or a JavaScript background, and those camps seem to clash a bit awkardly when it comes to working in Grails together.
❌ Bad
Boolean allGood(Thing arg1, Map params)
✅ Better at least
Boolean isThisThingLegit(Thing thing, Map propertiesToCheck)
Map params
as an argument to a service method is something I have frequently seen, and drives me a bit crazy now. I know the root of this is in part because Grails controllers use that for request parameters, and just passing that whole thing along to a service method can be tempting, but the type of Map
and name of params
gives the person working on the service method no useful information about what should be in that map. It’s even worse if no type is specified at all, which Grails totally allows!
In this method params could literally be anything 😢
def sendAnEmail(body, params) {...}
To be fair so could body
, but at least the service class name or the method name might give a hint as to what that would be.
Adding a type can inform or contextualize the name, but don’t stop at that, naming arguments well can help a lot!
def sendAnEmail(String body, String[] recipients) {...}
Good naming is at least as much art as science.
Avoid problems with inferred returns
The last statement of a method is inferred as a return
value in Groovy.
This can bite you. Hard. But some static typing along with @CompileStatic
or @TypeChecked
or @GrailsCompileStatic
- discussed a bit more here - can save you some serious headaches
Be explicit with your return statements when its not obvious.
There’s no need to use return
if the method is only one or two lines. But be aware of how the implicit return works
// Here is a casting error waiting to happen
Document doSomethingToTheDocument(Document doc, Map stuffToAppendMaybe) {
...
doc.prop = 'foo'
if (weDidAnything) validateTheThingsYouDidWereOkay(doc) // if this returns a boolean... things go boom
else doc.flag = bar
}
// You can avoid the casting error by having a generic return type
// - def is just an Object - but the method having no defined return type
// in the method signature leaves it vulnerable to other issues
def doSomethingToTheDocument(Document doc, Map stuffToAppendMaybe) {
...
}
Use void
methods when you don’t need anything returned.
Becuase Groovy’s return is implicit you have to be explicit about wanting no return. This is a stark difference with JavaScript, for example. There, if you forget to add a return the default is you get nothing. Coffeescript was more like Groovy in this regard and a few others, and though I liked Coffeescript, it’s implicit return was a contentious thing, and I have become a bit less fond of it in Groovy over time too. But a thing Groovy has that overcomes the biggest problem of the implicit return is the void
specifying void in the signature avoids unintentional casting errors in addition to any unnecessary casting overhead.
void doSomethingToTheDocument(Document doc, Map stuffToAppendMaybe) {
...
}
Multiple method signatures
Taking advantage of static typing in Grails is often surprisingly painless and has many benefits, but it isn’t completely without tradeoffs. One benefit of less strict method signatures is that you don’t need repeated methods with subtley different signatures to deal with different argument types.
Date parseToDate(date) {...}
Inside that one method you can handle many cases - wether date was already a Date
, if it was a String
, or if it was a Long
- as well as if it were null
.
If you sepecify a default value you can even avoid defensive null checking!
Date parseToDate(date='') {...}
However, if you want to move to
Date parseToDate(String date) {...}
you would also need
Date parseToDate(Date date) {...}
and
Date parseToDate(Long date) {...}
to handle those same range of cases.
Here is the rub, if you left it at just those though you could have a problem. In fact, this is where my code did have a problem. If your incoming date is null
Groovy will not know what to do will throw an error
groovy.lang.GroovyRuntimeException: Ambiguous method overloading for method ...
Cannot resolve which method to invoke for [null] due to overlapping prototypes between:
[class java.lang.String]
[class java.util.Date]
To prevent this you can add one more method.
Date parseToDate(Object date) {return null}
The ambibuty is replaced with a specificity cascade. The null situation falls into the less specific signature, and the others go to the one more fitting for their specified types.
So is the static typing here worth it?
And here we have one of those classic “it depends” scenarios.
Factors:
- How important are the performance benefits of
@CompileStatic
for your case? - How many other pieces of code will invoke this method?
- If a lot, a strictly defined method signature sure goes a long way to a predictable and stable API.
- If just one or two and this is a private method - probably NBD either way - do what feels better for those specific one or two spots.
- How many args does your method accept and how much variation in the types of its args are there.
Date myNewDate = incomingJsonDate ? parseToDate(incomingJsonDate) : null
might be better than adding the extra parseToDate signature to handle the null case if you are only invoking the method in a few places. The logic is right on the line where the method is being invoked, no need to go looking or having to reason about java method signature fallbacks etc.- However, if your method has more than one arg, and any of them might be null this approach falls apart pretty quick.
- If you have multiple args each that could be varying types and or null… well this is where dynamic languages can shine. Yes, It may also be the case you need to rethink some abstractions. To much ambiguity can be a form of code smell. On the other hand, embracing the dynamism of a language can be very powerful and practical in the right situations.