In my previous post I spoke about threading strategies when implementing Clean Architecture on Android. After tackling that, I wanted to share some thoughts regarding the Use Case interface (a.k.a Interactor) and how that interface is effected by the use of RxJava. In the description of this architecture, the use case should interact with the outer layers through Interface Adapters. In his talk about Clean Architecture, Uncle Bob talks about a request response model as the Interactor’s interface passing through a Boundary interface. So it might look like this:
This is obviously synchronous, so the asynchronous version will look something like:
And if we want to make it more generic:
This is a valid approach, but has a few drawbacks:
- The generic type requires to specify both types.
- No good error handling (though that can be easily fixed by adding an onError method to the Callback class.
- If, for example, we need to pass 2 integers to the execute, we need to create a Request type that holds 2 integers just for the sake of this generic interface.
- If we do not need to pass any parameters (for example, getting a list of items), we need to pass Void type and nulls when executing the request.
- If we don’t care about the response, we are in the same situation as the bullet point above.
RxJava into the mix
With RxJava this asynchronous API can become much simpler:
This solves the error handling and most of the boilerplate code. The thing that it doesn’t solve is the Void and null passing.
Another way is to say that the request will be passed to the constructor of the use case and the execute will be generic so the interface will look like this:
But if one wishes to use dependency injection to pass this use case, that will make the code much harder to reason about (at least to me).
Why an Interface?
In the first iteration of the Use Case interface I was inspired by Fernando Cejas implementation of Clean Architecture on Android. In that implementation, the threading was handled by the UseCase base class. As discussed in the previous post, this approach has several drawbacks so I moved away from having the threading handled in the Use Case. Without that piece of logic, the remaining class is just a simple interface as described above. So why go through all the trouble of defining this abstraction if at the end of the day, the users of these Use Cases are presenters that will use the concrete implementation of the Use Case and will not require a pure reference to a UseCase instance. If that is the case, this smells of unnecessary complexity and abstraction.
You might be wandering by now what the hell have I been writing about in this post if I am not going to even have an interface? That’s a valid question, and my answer will be that even if you don’t have an actual interface, you should still aspire to have some cohesion in your Use Cases public interface. So for example, think of two use cases: one to return a list of items and one to add an item. In this case they will look like this:
So even though the execute method does not return the same Observable and accepts the same parameters, it is still called execute and if it needs to return a response, it will do so in the form of an Observable<T> (it can return void if no response required). And now you can reason about what this Use Case do and if someone wants to see what it does all they need to do is look for the execute method. Having a convention that is simple and easy to follow helps people navigate the code base and understand it and thus making it more maintainable. Enforcing this contract through an explicit interface causes some unnecessary abstraction and adds the need for passing nulls and Void generic params which makes the code less readable and therefore less maintainable.
If you are using Rx in the rest of the app, it is very handy to use it for the Use Cases API. It is asynchronous by nature and makes your API very clean and useful. If not, you can implement your own Observable design pattern to listen on responses from the use case. Either way, you should avoid adding an interface for the sake of having one. That will lead to less readable and less maintainable code.