Clean architecture and the sinful interactor

Ian Alexander
6 min readSep 8, 2018

Interactors: little, reusable chunks of code that abstract logic from presenters while simplifying your app and making future changes effortless. But are they all they’re cracked up to be? Before I go on, the interactor pattern has a number of variations in Android. For this article, the term relates to enforcing the UI to interact with the data layer via an interactor layer. These interactors are primarily single method classes which look something like the example below.

class GetChats @Inject constructor(
private val myapi: MyApi
) : SingleUseCase<List<Chat>, GetChats.Params>() {
data class Params(val page: Int) override fun execute(params: Params): Single<List<Chat>> {
return myapi.getChats(params.page)
}
}

But how does the idea of interactors measure up when used in a real life project? If you’ve followed the trend and compartmentalised your domain logic into a variety of single function classes, probably something like this.

When you first wrote those elegantly named, concise classes, the structure made sense. Of course x uses y which invokes z and c that is helped by p. But come back to that code in 6 months time, or even worse, come into an interactor structured project as a new developer and I wish you luck in making sense of the spider web of dependencies. You’ll need it.

Yes, several small interactors look great in a medium article or an example project on Github. But apply that same paradigm to a large project and you’ll quickly have hundreds of interactors, which call other interactors, which call even more interactors. When you come to add a new feature or search for a bug, you can quickly fall down a rabbit hole while trying to discover which interactors to change — without duplicating code or creating a bug elsewhere.

The solution? The paradigm we’ve programmed to for the the last 40 years. Object Oriented Programming. Those 10 public interactors which handle building a user profile? Merge them into one class. Reusable classes of logic which group similar behaviour into public and private functions. Your future self will thank you.

Functions marked with (+) are public, whilst functions marked with (—) are private

Reusable classes of common behaviour make our domain logic easier to read, understand, and maintain, but perhaps we are giving too much responsibility to our interactor layer in the first place — the god problem. Do any of your interactors looks like this?

class UpdateUser(
private val myApi: MyApi,
private val myCache: MyCache
private val myDatabase: MyDatabase
) : CompletableUseCase<User>() {

override fun execute(params: User): Completable {
// Update cache
// Update database
// Update api
}
}

class GetUser @Inject constructor(
private val myApi: MyApi,
private val myCache: MyCache
private val myDatabase: MyDatabase
) : SingleUseCase<User, Void>() {

override fun execute(params: Void): Single<User> {
// Check cache for user
// If cache is null check database
// If database is null fetch from api
}
}

This is code which deals with the decision of where to store (or retrieve) data. Sounds a lot like the repository design pattern. By moving data storage decisions into interactors, we are making what can be a simple data api, into a complex set of classes which are hidden throughout our layer of interactors — the software equivalent of hiding a series of needles in a haystack.

How can this be simplified?

class UserRepository(
private val myApi: MyApi,
private val myCache: MyCache
private val myDatabase: MyDatabase
) {

fun updateUser(user: User) {
// Update cache
// Update database
// Update api
}

fun getUser(): Single<User> {
// Check cache for user
// If cache is null check database
// If database is null fetch from api
}
}

With this approach you can simply search in one place — the repository layer — to discover if data is being stored (or retrieved) from the cache, database, or api. And if there is no decision on where the data is fetched (or retrieved), perhaps your UI can call the data store directly — bypassing the repository layer entirely.

So perhaps we are giving interactors too much responsibility, and some of that logic can be split into more appropriate design patterns. But what about interactors that contain no logic? How many of your interactors look like this?

class GetChatList(
private val myRepository: MyRepository
) : SingleUseCase<List<Chat>, GetChats.Params>() {

data class Params(val page: Int)

override fun execute(params: Params): Single<List<Chat>> {
return myRepository.getConversations()
}
}

class GetChat(
private val myapi: MyApi
) : SingleUseCase<Chat>, GetChats.Params>() {

data class Params(val chatId: String)

override fun execute(params: Params): Single<Chat> {
return myapi.getChat(params.chatId)
}
}

One line delegators to the data layer — I’m willing to bet a fair chunk. Have you ever asked yourself what benefit this brings? Your one line api call is hidden behind a descriptive class name? if your method is well named there’s nothing to gain there. Future changes to the data returned from the api can be dealt with in one place, the interactor? YAGNI. Your presenter doesn’t know data is fetched from the api? While I would argue that this is overkill — unless you work on VERY large projects — it’s also the wrong pattern to choose.

Both MVP and MVVM are UI patterns, The M (model) part of the pattern has the responsibility of handling interactions with the domain or data layer and handles mapping data into the correct form for the UI to use. This part of the pattern in usually unnecessary in small to midsize projects . But, if you truly want to remove knowledge of the data layer from your presenter or view model, make use of the M in MVP and MVVM.

class ChatModel(
private val myapi: MyApi
private val myRepository: MyRepository
) {

fun getChatList(page: Int): Single<List<Chat>> {
return myRepository.getConversations(page)
}

fun getChat(chatId: String): Single<Chat> {
return myapi.getChat(chatId)
}
}

In software development, the interactor design pattern was created with the primary intention of encapsulating business logic. The problem with using this pattern in a mobile context, is that mobile apps do not contain much business logic. Since most modern apps run on multiple platforms, true business logic has been moved to backend apis to avoid duplication of code on each platform. Mobile apps — in very simplistic terms — are UI vehicles to allow interactions with the cloud.

The reason interactors have picked up so much traction in Android is that they scratch an itch. However, they are often used to scratch EVERY itch. Originally, we gave all responsibility to the God Activity. To solve the god problem we abstracted these responsibilities to presenters. Presenters became the new god object. To solve this problem, we embraced clean architecture, and abstracted all this responsibility to interactors. Notice a pattern here?

So when you’re writing your next interactor. Look at what responsibility you’re giving to the Interactor. Choosing where to store or retrieve data? Use the Repository pattern. Hiding the data layer from the presenter or view model? Use the M of MVP and MVVM. Making code reusable between presenters? Perhaps that interactor is simpler and easier to maintain as a reusable, object oriented class of common behaviour.

Bonus:

A frequent use of interactors can be to enforce threading on the domain level. However, is this needed? Threading enables us to keep the UI responsive while making blocking calls, therefore, perhaps the best place to perform threading is on the UI layer; where we know if a blocking call is a problem. If you would like to abstract responsibility for threading from presenters and view models, perhaps the best way to do that is to give that responsibility to your Model class — the M in MVP and MVVM.

--

--