Experiment #1: Functional Interactors - adrielcafe/gwent-wallpapers GitHub Wiki

In short, Interactors (a.k.a Use Cases) in Clean Architecture are reusable chunks of code responsible for making the UI layer interact with the Data layer.

Interactors are located at the Domain layer and usually contains a single public function. Let's talk about two common approaches to build an Interactor (using classes and interfaces), and a third one (the functional way).

I'll use Koin in the examples below, but you can get the same result in any DI library.

The class approach

This is the most common way to build an Interactor: we inject the repository into the class and interact with it. The example below shows how to build an Interactor with a class and how to use it in the UI layer:

// Domain layer
class IsFavoriteInteractor(private val repository: FavoriteRepository) {

    operator fun invoke(wallpaper: Wallpaper): Boolean =
        repository.isFavorite(wallpaper)
}

// DI
val domainModule = module {
    single {
        IsFavoriteInteractor(repository = get())
    }
}

// UI layer
class WallpaperViewModel(private val isFavorite: IsFavoriteInteractor) : ViewModel() {

    fun ifFavoriteDoSomething(wallpaper: Wallpaper) {
        if (isFavorite(wallpaper)) {
            // Do something
        }
    }
}

As you can see, we can use operator fun invoke to treat the injected Interactor as a function, pretty cool.

But something bothers me with this approach: sometimes the Interactor is just a proxy, i.e. it only calls the Repository and does nothing else, like the example above. That looks like a code smell to me.

The interface approach

It's also possible to represent an Interactor with an interface. In this case, we can move its implementation to the Repository itself, avoiding the "proxy" issue.

// Domain layer
interface IsFavoriteInteractor {

    fun isFavorite(wallpaper: Wallpaper): Boolean
}

// Data layer
class FavoriteRepository : IsFavoriteInteractor {

    fun isFavorite(wallpaper: Wallpaper): Boolean =
        // Do something
}

// DI
val dataModule = module {
    single<IsFavoriteInteractor> {
        get<FavoriteRepository>()
    }
}

// UI layer
class WallpaperViewModel(private val isFavoriteInteractor: IsFavoriteInteractor) : ViewModel() {

    fun ifFavoriteDoSomething(wallpaper: Wallpaper) {
        if (isFavoriteInteractor.isFavorite(wallpaper)) {
            // Do something
        }
    }
}

A good thing about this approach is that the Repository can implement multiple Interactors, but there's a catch: the Interactor's public function can't be operator fun invoke or have a generic name like execute because will cause the Conflicting overloads error at compile time.

If we choose this approach, we'll loose the ability to treat the injected Interactor as a function. It's not a problem for many developers, but it also bothers me.

The functional approach

My goal in this experiment was to have Interactors without the "proxy" problem and the ability to treat them as functions. In Kotlin, function are first-class citizens, so why not use them, or more precisely lambdas, instead of classes or interfaces?

Using a lambda like (Wallpaper) -> Boolean to represent an Interactor is strange at first sight, but we ca use a typealias to easily identify them: typealias IsFavoriteInteractor = (Wallpaper) -> Boolean.

Let's see the full implementation:

// Domain layer
typealias IsFavoriteInteractor = (Wallpaper) -> Boolean

// Data layer
class FavoriteRepository {

    val isFavorite: IsFavoriteInteractor = { wallpaper ->
        // Do something
    }
}

// DI
val dataModule = module {
    single(named<Interactor.IsFavorite>()) {
        get<FavoriteRepository>().isFavorite
    }
}

// UI layer
class WallpaperViewModel(private val isFavorite: IsFavoriteInteractor) : ViewModel() {

    fun ifFavoriteDoSomething(wallpaper: Wallpaper) {
        if (isFavorite(wallpaper)) {
            // Do something
        }
    }
}

Full implementation: favorites.kt, FavoriteRepository.kt, module.kt, WallpapersViewModel.kt

About each layer:

  • Domain layer: here we declare our Interactors as usual
  • Data layer: like the interface approach, but the Interactor is implemented with lambda
  • DI: since the typealias do not introduce new types, it's not possible to use them to differentiate functions with the same signature at compile time. We must name our definitions, in Koin this is called qualifier (a.k.a tags). Tip: you can use a sealed class or enum to group our qualifiers.
  • UI layer: like the class approach, but now we're really injecting a function!

Conclusion

The functional approach works very well, it has everything I was looking for. But it's not perfect, so far I've encountered the following issues:

  • It's not possible to use default arguments or named arguments with lambdas
  • We must use qualifiers to identify definitions, an extra step not required by other approaches

If you have an idea how to improve it, please open an issue and share our thoughts.