From monolithic to clean
This week I published the talk I had given on how I evolved from a Monolithic architecture to a Clean Architecture based on Fernando Cejas proposal but with a few differences in the domain layer which I would like to explain. I am not saying these are better approaches but just some tweaks that made the data flow and the architecture clearer to me, so take them with a grain of salt.
The following differences between my implementation and Fernando’s differ in a simple implementation detail. To Fernando, as I can see, a use case instance by itself represents not only a feature of the application but also a feature with concrete values. This way, each use case has its values passed through the constructor following the Command Pattern to execute it.
We can all agree that this approach may seem to make sense at first but when implemented in some situations, it is not clear about how things should be done. But before I explain my implementation differences, let me first comment on some of his:
Comment #1
In his implementation of the UserDetailsActivity
, the userId is passed to the presenter through a UserModule
(with Dagger 2) and then to the constructor of the use case which doesn’t seem natural. This works well in this particular use case but it is not clear on how would we perform an action in a list of items. For instance how could this be achieved in an activity where we have the option to Like a comment in a list of comments? Maybe through the instantiation of a LikeCommentUseCase
object and passing it to the presenter via method call instead of its constructor?
Comment #2
In the domain layer each use case knows which thread will subscribe the data and which will observe its result. But why is it so? Presentation layers (like Android) have threading concerns and not domain layers.
Comment #3
Mapping the list of users from the domain model to the presentation model is done on the UI thread instead a worker thread. This is because the UseCase
class doesn’t expose its observable although I think that in the current implementation it makes sense not to do so.
Therefore, I would suggest a little difference on his implementation of a use case. Each use case is a simple method contained in an operations related class than in a class of its own (ex: UserService
which would contain getListUsers()
, getUserDetails()
, etc). Why? Let’s see some of my arguments:
Difference in #1
With a UserService
I only need to invoke the following method in an activity presenter.loadUserDetails(userId)
and then the presenter would invoke userService.loadUserDetails(userId)
which seems to me not only clearer but also easier to follow the data flow. Similarly, if we need to like a comment we would only need to invoke in the presenter, postService.likeComment(postId)
.
Difference in #2
The domain layer doesn’t need to know that there are subscribers to its data. Instead, it should only provide the necessary common domain logic by communicating with the repositories as well as with other use cases if necessary. Subscriptions and unsubscriptions are to me a presentation problem (Android) and should be treated there (due to the UI lifecycle) as well as threading manipulation, I therefore moved them to the presenters. So each service (ex: userService) only exposes operations that return Observable.
public class PostService implements IPostService{
IPostRepository mRepository;
public PostService(IPostRepository mRepository) {
this.mRepository = mRepository;
}
@Override
public Observable<Post> getPostById(int postId) {
return mRepository.getPostById(postId);
}
@Override
public Observable<List<Comment>> getPostComments(int postId) {
return mRepository.getPostComments(postId);
}
}
Difference in #3
By accessing the observable through a service on the presenter, we could map the data still in the IO thread instead of the main thread:
public class HomePresenter implements IHomePresenter{
private IHomeView mView;
private IPostService mService;
private IMapper<Post,PostView> mPostMapper;
private IMapper<Comment,CommentView> mCommentMapper;
public HomePresenter(IHomeView mView, IPostService mService, IMapper<Post, PostView> mPostMapper, IMapper<Comment, CommentView> mCommentMapper) {
this.mView = mView;
this.mService = mService;
this.mPostMapper = mPostMapper;
this.mCommentMapper = mCommentMapper;
}
public void loadPost(final int postId){
mService.getPostById(postId)
.subscribeOn(Schedulers.io())
.map(new Func1<Post, PostView>() {
@Override
public PostView call(Post post) {
return mPostMapper.transform(post);
}
})
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<PostView>() {
@Override
public void onCompleted() {}
@Override
public void onError(Throwable e) {
mView.onError(e.getLocalizedMessage());
}
@Override
public void onNext(PostView postView) {
mView.onPostLoaded(postView);
}
});
}
}
Designing and architecting software systems is all about tradeoffs. I honestly think that with my approaches I made it clearer and with a better separation of concerns, resulting in a loss of a considerable amount of abstraction compared to Fernando’s approach.
Keep in mind that my presentation focused solely on the architecture side, and not on concepts like offline-online data sync, dependency injection, testing, etc. Therefore, the repositories I added to my github don’t have those concepts implemented although I would recommend you to see Fernando’s Clean Architecture repository if that is what you are looking for.
To conclude, I would like to thank great developers like Romain Piel, Jake Wharton, Hannes Dorfmann and Israel Ferrer whose blogposts helped me to achieve this evolution and also many others responsible for moving the community forward, especially, Fernando Cejas for writing that blog post which helped me change the way I architecture my apps.