Companion object invoke operator overloading for default constructor argument in generic classes
A real world case in a coroutine test rule implementation
That might've been the worst title I've ever come up with. But it also might be the shortest blogpost I've ever written, so maybe that makes up for it.
TL;DR:
This won't work (the Java equivalent won't compile either):
But if we have this:
We can call that as if it was an empty constructor, thus achieving what we wanted in the first place: a generic class that provides a constructor that assigns a default value to the generic property.
You can read all about overloading the invoke operator on a companion object in this blogpost by Ataul Munim:
The context there is around validation before the object creation. The point of this post is to highlight another interesting scenario where I found this useful.
Coroutines testing
We've been increasingly using coroutines at Blinkist and we wanted to introduce in our codebase a JUnit rule like this one:
But I didn't want to only support TestCoroutineDispatcher
, I wanted to give a way for clients to also use the Unconfined
dispatcher for simple scenarios. In fact, I wanted Unconfined
to be the default, and I still wanted to expose the dispatcher so clients would be able to use it in case they chose to work with the TestCoroutineDispatcher
. I basically agree with Zach Klippenstein here:
I also want to make sure I'm exposing the actual type of the dispatcher I'm using, so when clients are using a TestCoroutineDispatcher
, they can do something like this, for instance:
rule.dispatcher.advanceTimeBy(500)
That's why generics is important here and I don't want to expose the dispatcher as a simple CoroutineDispatcher
, which ultimately would lead to ugly casts in the clients.
So this was my first attempt:
But it won't compile:
Type mismatch.
Required:T
Found:CoroutineDispatcher
There are some discussions about this, and currently there's no ETA for this feature. It took me a while to accept this wasn't possible, but I was lucky to have fresh in my memory something that could help me there. I had recently watched this great talk by Danny Preussler where he mentions overloading the invoke operator on companion objects:
This is how our rule looks like with that trick:
Now anyone wanting to write a simple coroutine test can just go with a CoroutineRule()
, and anyone interested in the power of the TestCoroutineDispatcher
can explicitly pass it in the constructor ✨
I agree with Ataul Munim that this still causes some astonishment, and in the end we actually decided to add a comment there explaining what's going on:
I'm super happy in the end the API looks pretty much what we wanted, so I'm definitely embracing this h̶a̶c̶k̶ peculiar solution and hoping it might prove useful in other situations for other people :)