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