Invoking UseCases the Kotlin Way

UseCase functions benefiting from Kotlin's operator overloading feature.

Invoking UseCases the Kotlin Way

Awhile back a coworker introduced me to an approach to structuring UseCases with a Kotlin syntactic feature. Since then, I really have grown to appreciate the approach, so I figured I'd share it.

TL;DR

Use the Kotlin invoke operator function for contract-less UseCases.

Background

In a Clean Architecture project, there are often classes called UseCases (Sometimes referred to as Interactors). These classes typically have a single focus and perform an action either related to the application or business logic. For example, there might be a UseCase to login to the application and another one to retrieve a model representing the currently authenticated user. There might even be another UseCase that performs some data calculation that is used in multiple parts of the application.

While it is useful to break code down into these focused and independently testable classes, it poses another issue: enforcing a consistent contract. Some UseCases might return results while others might not. Some might have zero arguments while others might have ten or more. Some might suspend while others are executed synchronously. These variances make it difficult to have a single interface that each UseCase extends. Therefore, forcing the call-site to know explicitly which class it is using.

There have been many approaches to attempt to mitigate this issue. One approach might be to have an interface for every UseCase implementation (Sometimes the UseCase is the interface and the Interactor is the implementation or vice-versa). Another would be to abstract the parameters and return type. Personally, I've found these solutions to be too much work for too little gain. So I have decided to leave them as individually contract-less classes, but as a means of convention, have chosen to call every public UseCase function the same name: execute.

(Foreshadowing: This decision proved to be beneficial later on when implementing the new approach because refactoring in the IDE was simple using the "Find and Replace" feature.)

For instance:

class LoginUseCase(private val webService) {

    suspend fun execute(username: Username, password: Password): LoginResult {
        val result = webService.login(username, password)
        
        return if (result.isSuccessful) {
            val user = result.body()
            
            LoginResult.Success(user)
        } else {
            LoginResult.Failure
        }
    }
}

Which could be invoked like this:

when (result = loginUseCase.execute(username, password)) {
    is LoginResult.Success -> navigator.goToHome(result.user)
    LoginResult.Failure -> view.showLoginError(loginErrorText)
}

Though the UseCases still do not conform to an interface, by convention they follow a similar design. The issue with this is that there is nothing enforcing this convention other than peer review. New approach to the rescue!

New Approach

The approach my coworker introduced me to was to make use of the Kotlin Operator Overloading Feature. This means that every UseCase's public facing function would have the operator modifier and be named invoke instead of execute. Enforcing these functions to be an operator overloaded function is still by convention but then the name is enforced by the compiler and we are given the added benefit of being able to invoke the function implicitly using just parentheses.

So the above UseCase would become:

class LoginUseCase(private val webService) {

    suspend operator fun invoke(username: Username, password: Password): LoginResult {
        val result = webService.login(username, password)
        
        return if (result.isSuccessful) {
            val user = result.body()
            
            LoginResult.Success(user)
        } else {
            LoginResult.Failure
        }
    }
}

This simple change allows a much more readable call-site:

when (result = login(username, password)) {
    is LoginResult.Success -> navigator.goToHome(result.user)
    LoginResult.Failure -> view.showLoginError(loginErrorText)
}

Notice how the UseCase property was named login instead of loginUseCase now. This makes the code read more naturally and allows the UseCase to simply look like a function.

End

This subtle change to a UseCase function provides the following benefits:

  • Enforces a pseudo-contract on a contract-less UseCase
  • Reads more naturally
  • Flexibility in invocation ( login() vs login.invoke())

Kotlin's conciseness always finds new ways to be invoked.