Okay, so I just started a new Android project and wanted to try implementing the Clean Architecture by Uncle Bob. I have a nice beginning using RxJava and stuff from GitHub samples & boilerplates and Fernando Cerjas' blog (like this article), but still have some questions on how to implement some UseCases.
Should an Entity have fields that are another Entity (in my example, User
having a List<Messages>
field)?
Or should the Presenter combine UseCases to build a ViewModel mapped on multiple Entities (then how to you code the mapper?)?
Or should the Presenter have a ViewModel associated to each UseCase/Entity, and create some kind of "wait for all data to onNext" to call the view.show() for each ViewModel?
Basically, should UseCases only return Entities? Can an Entity be composed of other entities (as in a field of the class)? Are Entities only dumb datamodels POJOs? How to you represent 'join SQL' queries?
As an example, let's take a simple users/messages app.
I want to implement two views: UserList
and UserDetails
:
UserList
displays a list of Users
UserDetails
displays a user's information and its latest messages.UserList
is pretty straightforward, and I can see how to code the associated UseCase and layers (code below).
My problem is with the UserDetails
screen.
How should I code my GetUserInfoUseCase
if I want all the data to be passed at the view at the same time (like building a ViewModel composed of a User class, with a field List)? What should be the return value of the GetUserInfoUseCase
?
Should I code a Observable<User> GetUserInfoUseCase
and a Observable<List<Message>> GetUserLatestMessages
and merge them somehow in my presenter? If yes, how can I manage this, as I don't have the Observables in my Presenter (I'm passing only an Observer as my UseCases parameters)?
public abstract class User {
public abstract long id();
public abstract String name();
...
}
public abstract class Message {
public abstract long id();
public abstract long senderId();
public abstract String text();
public abstract long timstamp();
...
}
public class GetUsersUseCase extends UseCaseObservableWithParameter<Boolean, List<User>, UsersRepository> {
@Inject
public GetUsersUseCase(UsersRepository UsersRepository,
@Named("Thread") Scheduler threadScheduler,
@Named("PostExecution") Scheduler postExecutionScheduler) {
super(usersRepository, threadScheduler, postExecutionScheduler);
}
@Override
protected Observable<List<User>> buildObservable(Boolean forceRefresh) {
if(forceRefresh)
repository.invalidateCache();
return repository.getUsers();
}
}
public class UsersPresenter extends BasePresenter<UsersContract.View> implements UsersContract.Presenter {
@Inject
GetUsersUseCase mGetUsersUseCase;
@Inject
UserViewModelMapper mUserMapper;
@Inject
public UsersPresenter() {
}
@Override
public void attachView(UsersContract.View mvpView) {
super.attachView(mvpView);
}
@Override
public void detachView() {
super.detachView();
mGetUsersUseCase.unsubscribe();
}
@Override
public void fetchUsers(boolean forceRefresh) {
getMvpView().showProgress();
mGetUsersUseCase.execute(forceRefresh, new DisposableObserver<List<User>>() {
@Override
public void onNext(List<User> users) {
getMvpView().hideProgress();
getMvpView().showUsers(mUsersMapper.mapUsersToViewModels(users));
}
@Override
public void onComplete() {
}
@Override
public void onError(Throwable e) {
getMvpView().hideProgress();
getMvpView().showErrorMessage(e.getMessage());
}
});
}
}
public abstract class UseCaseObservableWithParameter<REQUEST_DATA, RESPONSE_DATA, REPOSITORY> extends UseCase<Observable, REQUEST_DATA, RESPONSE_DATA, REPOSITORY> {
public UseCaseObservableWithParameter(REPOSITORY repository, Scheduler threadScheduler, Scheduler postExecutionScheduler) {
super(repository, threadScheduler, postExecutionScheduler);
}
protected abstract Observable<RESPONSE_DATA> buildObservable(REQUEST_DATA requestData);
public void execute(REQUEST_DATA requestData, DisposableObserver<RESPONSE_DATA> useCaseSubscriber) {
this.disposable.add(
this.buildObservable(requestData)
.subscribeOn(threadScheduler)
.observeOn(postExecutionScheduler)
.subscribeWith(useCaseSubscriber)
);
}
}
public abstract class UseCase<OBSERVABLE, REQUEST_DATA, RESPONSE_DATA, REPOSITORY> {
protected final REPOSITORY repository;
protected final Scheduler threadScheduler;
protected final Scheduler postExecutionScheduler;
protected CompositeDisposable disposable = new CompositeDisposable();
public UseCase(REPOSITORY repository,
@Named("Thread") Scheduler threadScheduler,
@Named("PostExecution") Scheduler postExecutionScheduler) {
Timber.d("UseCase CTOR");
this.repository = repository;
this.threadScheduler = threadScheduler;
this.postExecutionScheduler = postExecutionScheduler;
}
protected abstract OBSERVABLE buildObservable(REQUEST_DATA requestData);
public boolean isUnsubscribed() {
return disposable.size() == 0;
}
public void unsubscribe() {
if (!isUnsubscribed()) {
disposable.clear();
}
}
}
The Clean Architecture Entities — Describe the enterprise business rules or the business objects of the app, encapsulating the most general and high-level rules. They are the least likely to change when there are external changes. Use Cases — The app-specific business rules.
Clean architecture vs. The logical layers of this style are as follows: Presentation layer ( accounts for the presentation to the user) Business logic layer (contains the business rules) Data access layer (processes the communication with the database)
The main rule of clean architecture is that code dependencies can only move from the outer levels inward. Code on the inner layers can have no knowledge of functions on the outer layers. The variables, functions and classes (any entities) that exist in the outer layers can not be mentioned in the more inward levels.
A use case diagram shows the interaction between the system and entities external to the system. These external entities are referred to as actors. Actors represent roles which may include human users, external hardware or other systems.
Quite a lot questions within a single question. let me try to consolidate what I think I understood are ur key questions
Can Entities reference each other? the answer would be: YES. Also in Clean Architecture u can create a domain model where entities are interconnected
What should be returned from a UseCase? Answer: UseCases define input DTOs (Data transfer objects) and output DTOs which are most convenient for the use case. in his book uncle bob writes that entities should not be passed to use cases or returned from use cases
What is the role of the presenter then? Answer: ideally a presenter is converting data only. It converts data which is most convenient for one layer into data which is most convenient for the other layer.
hope this guidance helps u to answer ur detailed questions
More details and examples you can find in my recent posts: https://plainionist.github.io/Implementing-Clean-Architecture-UseCases/ and https://plainionist.github.io/Implementing-Clean-Architecture-Controller-Presenter/
Basically, you want to push your "instrumental" aware code as far as possible (on the circle).
Use cases are very close to the model and contain a lot of business logic - you want this layer very clean to be able to do quick and easy unit tests. So, this layer shouldn't know anything about storage.
But the fun part is when Room enters the room :) Room makes it so easy to have model-like objects that you can use around and IMO it's a grey area should you use Room annotated classes for your model or not.
If you think about Room objects as Data Layer objects, then you should map them to your business objects before reaching use cases. If you use Room as a built-in mapper of DAO to model objects, then IMO you can use them in your use cases, although clean purists probably would not agree on this.
My pragmatic advice would be - if your model has a complex structure built in from multiple entities then have a dedicated model class for it and map entities to it. If you have something like an Address, IMO just go with the Room entity.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With