kotlin hack: transparently replace class with interface
While working on an experiment using Kotlin recently, I ran into the following issue: I had a class — let’s call it FooInteractor
— that I wanted to be able to replace with an implementation that did nothing in some cases.
I had other classes that referenced it, which I did not want to have to change. I also had several places where FooInteractor
was instanced directly, via FooInteractor(…)
which I also did not want to have to change.
So I had the following challenge: Can I somehow only change FooInteractor
, without touching any other file, and still allow it to be easily replaced?
As an example, let’s use the following definition for FooInteractor
:
class FooInteractor(private val repository: FooRepository) {
fun call(val someArg: String) { /* ... */ }
}
Mocking frameworks?
This use-case also comes up quite often in tests, and there the amazing Mockito framework, or some similar tool is usually employed to automagically create an instance of FooInteractor
that has no behavior (or that we can attach custom behavior to).
But, in this case, I wanted to avoid having to add a dependency to mockito for just this simple use case.
Solution part 1: Introducing an interface
The first step was introducing an interface to replace the concrete implementation:
interface FooInteractor {
fun call(someArg: String)
}
class FooInteractorImpl(private val repository: FooRepository) : FooInteractor {
override fun call(someArg: String) { /* ... */ }
}
By renaming FooInteractor
to FooInteractorImpl
and introducing an interface with exactly the same signature for its functions, all of the code that referenced the FooInteractor
type transparently started using this interface.
…and I could now do this:
val emptyFoo = object : FooInteractor {
override fun call(someArg: String) {
println("Empty foo called with $someArg")
}
}
And I could use this emptyFoo
anywhere that takes a FooInteractor
.
But, there was still an issue: what about places in the codebase that called the FooInteractor
constructors?
Solution part 2: Faking the original class' constructor
Anywhere on my codebase where a FooInteractor
was created via
val fooInteractor = FooInteractor(repository = someRepository)
was now failing, because FooInteractor
just became an interface.
To solve this transparently, we can make use of the Invoke operator.
The invoke operator allows writing an expression such as someObject(…)
as an alias of someObject.invoke(…)
. And funnily enough, constructors look exactly like that!
So yeah, a bit unexpectedly, we can use the invoke operator to make our new interface into a factory of implementation instances, transparently:
interface FooInteractor {
companion object {
operator fun invoke(repository: FooRepository) =
FooInteractorImpl(repository)
}
fun call(someArg: String)
}
And our example code will now work, even though it is now calling the invoke
function on the interactor interface, rather than the constructor directly:
val fooInteractor = FooInteractor(repository = someRepository)
And boom, done! We just introduced an interface by only changing the FooInteractor
class, and the rest of the codebase remains 100% unchanged.
Here’s the full example, if you want to experiment with it:
#!/usr/bin/env kscript
// Save this as example.kts and use https://github.com/holgerbrandl/kscript to run it
class FooRepository() { /* ... */ }
interface FooInteractor {
companion object {
operator fun invoke(repository: FooRepository) =
FooInteractorImpl(repository)
}
fun call(someArg: String)
}
class FooInteractorImpl(private val repository: FooRepository) : FooInteractor {
override fun call(someArg: String) {
println("Real foo called with $someArg")
}
}
val emptyFoo = object : FooInteractor {
override fun call(someArg: String) {
println("Empty foo called with $someArg")
}
}
val someRepository = FooRepository()
val realFoo = FooInteractor(repository = someRepository)
emptyFoo.call("Hello")
realFoo.call("World")
And the expected output:
Empty foo called with Hello Real foo called with World