When using dependency injection frameworks like Spring or Micronaut, you
will often have services, that depend on services, …. While back in the
days, annotating fields with @Inject
or @Autowired
was acceptable, it
felt out of favour, because you would never know what a service really
depends on. So nowadays injection is usually done via the constructor.
But this also means, that you will find yourself often fiddling around
with constructors and orders of arguments. You also will find yourself
falling back to the help of your IDE. But if you have the slightest
sense of order, you will also find, that the generated code does not
keep your order of things: e.g. IntelliJ always adds new fields to the
end of the c'tor - and also the assignment to the field inside the
c'tor. So you either delete the whole c'tor and recreate again or you
will end up using more IDE features to re-arrange your code.
Given that usually those c'tors are only used by DI automatically and
maybe in your tests, can't we do better and just let the compiler
generate the c'tor for us -- instead of moving words around with your
editors?
Groovy comes with
TupleConstructor
,
which can create a c'tor with explicit arguments (in contrast to
MapConstructor
,
which is used to construct via passed in maps to make it look like
Groovy would support named arguments). The defaults of TupleConstructor
are very un-strict, though. By default, it creates a c'tor for each
additional property where the default is null
. Usually not what we
want with DI, because it does not know which c'tor to use now or might
use the wrong one and we end up in the place, that we wanted to avoid
when going with c'tor based injection.
But luckily TupleConstructor
can be configured to high degree. These
settings will give you a pretty good default:
@groovy.transform.TupleConstructor(
includeFields = true, // add fields to the c'tor
defaults = false // do not generate versions of the c'tor with null assignments
)
So now instead of writing this every time, you can make this a new
annotation to easily use (and still be able to override the nuances, if
you have to), with AnnotationCollector
. E.g.
@groovy.transform.TupleConstructor(
includeFields = true, // add fields to the c'tor
defaults = false // do not generate versions of the c'tor with null assignments
)
@groovy.transform.AnnotationCollector
@interface ServiceConstructor {}
Now you can use this annotation instead:
@Service
@ServiceConstructor(
excludes = ['message'] // if you have transients, add them here
)
class MyService {
protected final HelloService helloService
transient protected String message
@PostConstruct
void init() {
message = helloService.sayHello("World")
}
}
This will generate the c'tor you expect: MyService(HelloService
helloService)
. And if you need your FormatService
too, you just add
it as a field -- at the place you like. And the c'tor will honour your
order in the code.
One downside though is the loss of making your IDE show the usages of
the c'tor -- but then, this is often only useful for tests. In that
case you can still use your IDE to generate the c'tor, maybe adjust,
find the usages and fix them and then delete the c'tor again. There
sadly seems not to be a flag to make TupleConstructor
throw, if there
are existing c'tors. It will by default just silently not generate
a c'tor, which is not the worst.