flipper83

Inside Rosie - Domain and use cases

In the previous Inside Rosie post, we discussed the implementation and considerations of the presentation layer in our framework. We explained how Rosie implements MVP and how this part of the framework should be used. We are going to see how Rosie contributes to domain layer definition and why it has been implemented.

When we started designing Rosie, one of our main concerns was how we could improve the business logic layer design; even when the business rules implemented are not so strong. Most of the applications implemented consume an API, save information and draw the info on the screen. The set of operations is minimal but we thought this layer was really important and could be really helpful in the development if we designed a strong and flexible core.

The first design concern was designing our domain without coupling it to the presentation or networking layer. Designing our domain, with low coupling gives us the benefit of starting to work early on the features without concerning ourselves with implementation details, even if we don’t have the API documentation or the screen design; sometimes we receive screen designs but the backend is not ready yet. By creating a strong separation between our domain and the screen or networking layer we can start developing different tasks at the same time giving a fake implementation when needed. When the API is deployed, we just need to update our implementation, replacing the fake one with the real API client. As all the implementation details are abstract, this also helps us to write tests in our app easily, moreover, our software is easier to extend as our domain model is based on abstractions which can be replaced when needed without modifying our business rules.

One of the key points in designing our domain is the interactors definition (interactor is the term defined by Uncle Bob in his Clean Architecture articles), henceforth we will use the term “use cases” instead of interactor. We prefer the term “use cases” instead of interactor in Rosie because we think the semantic is stronger and more related with the component goal. Use cases define all the operation sets the domain layer can perform. In the Rosie example, GetCharacters use case provide the user interface layer with a list of characters. As you can see, the name used for this use case is descriptive and reveals intention. This will help us define and share easily what a developer can implement based on the domain layer.

Use cases should be named with a verb. A use case performs an action over our domain that we want to execute. GetCharacters, GetCharacterDetails, GetComicSeriesPage are some examples. The goal is to make our software descriptive. The use case does not lie, it has a descriptive name telling us what it proposes and what we can expect from the use case. Imagine a new developer joining our team; with a quick review of the package or packages that contains all use cases, this developer can take a picture of all the operations our software can and can’t implement. With this approach, we reduce the software complexity from the domain layer user point of view while reducing duplicity.

Imagine that this new developer is unlucky and the use case LikeComic does not exist and needs to be implemented. In this case, we can use the second use cases property. The use cases must describe the software. Use case works like a recipe and should not implement complex domain logic, instead, it should delegate this logic to rich models. Rich Models can carry out operations over the domain. For example, we can have an object that sorts the data, or calculates the average… and we run this from our use case instead of through the code in the use cases. The idea is to simplify our use cases as much as possible, always trying to improve readability. Getting back to the LikeComic example, the new developer can check out GetComicById and develop LikeComic using it as an example. If GetComicById obtains a comic by id from the data layer and returns it, LikeComic could obtain a comic by id, run the like method from the Comic model that likes the comic, update the data layer and return it to the view layer.

A common question we are asked is if a use case could have more than one method. There isn’t an easy answer. A use case models an action over the software and the response depends on the software’s granularity. Imagine that the GetComicById has a method getComic(String id) but our domain model has a method with a composite id getComic(String serieId, String comicId), these could be two valid methods for our software and it is not mandatory to have two classes, GetComicById and GetComicBySerieIdAndComicId. Sometimes we forget which implementation detail in the software could be converted into definition details by itself. A really clear example of this, and commonly found in a lot of apps, is to show old expired information on a list until the new information has been loaded. If we review the data layer definition, the data cache information must be hidden by the domain layer, the origin of the data is not important but in this case the data origin is a software requisite, and a use case could have more than one method, for example: GetComic could have the getComics() method, which would be the normal method and getComicFastIncludeOld() returns what we have cached.

All the while we have been working on different Clean Architecture implementations we have considered many solutions for how to run and work with use cases. Uncle Bob, in his definition, does not enter in depth about these use cases; he only talks about using a Command Pattern implementation. We thought about this a lot; you can find a simple implementation with a command pattern in the android-architecture project using callback. We have fixed the problem with a RX implementation for the use cases or other different implementations. After much consideration, in Rosie, we went with what we believed conformed more to what we found day-to-day in our applications.

Most of the applications we develop, go to the network, make a call, store information on disk and draw the info. As a worst case, they make two API calls. When we considered this, we decided that readability of the code would be the most important for us. We liked that everybody could read, understand and extend it easily. Another consideration we had in mind about developing the Android app was to get out of the UI thread as soon as possible because some operations that could seem light in performance, like sorting a list or running a stringtokenizer, could break the UI performance. To fix this problem we created UseCaseHandler and all use cases could be run outside the UI thread and make the code easier all Use Cases inherit from RosieUseCase. The code to run a use case is:

 createUseCaseCall(getCharacterDetails)
    .args(characterKey)
    .useCaseName("ById")

In our daily development, we execute all code in use case synchronously . For example, if we get a Comic and we save it to disk, we do it synchronously; this is arguably slower, but if we compare the time spent against the code’s readability and the complexity of extending the code, we prefer this solution. If the writing is to be postponed to another thread, it is not difficult and can be done where necessary. It is true that in some cases we need to make and compose 5 API calls (I don’t find this problem with real frequency) in this use case we resolve it using Promises or RxJava or whatever can help us fix it easily, but we try not to resolve problems that we do not have.

Defining a use case with Rosie is as easy as extending from RosieUseCase and noting with @UseCase all those methods we want to define as a use case and want to be managed by the Use case handler.

public class GetCharacterDetails extends RosieUseCase {

  private final CharactersRepository charactersRepository;

  @Inject public GetCharacterDetails(CharactersRepository charactersRepository) {
    this.charactersRepository = charactersRepository;
  }

  @UseCase public void getCharacterDetails(String characterKey) throws Exception {
    Character character = charactersRepository.getByKey(characterKey);
    notifySuccess(character);
  }
}

To run a use case, all we need to do is run it from a presenter. The UseCaseHandler will search for a method in the use case class, GetCharactersDetails in the example, that matches with the same arguments passed in args and noted with @UseCase, and run it. We can define an onSuccess method, and in the same way receive messages in the method noted with @Success and match the same arguments. We can make more than one notifySuccess with different signatures in the use case, for example: to disable a loading and after that loading data, or notify first with old data from the cache and then notify with new data from the API.

  private void loadCharacterDetails() {
    createUseCaseCall(getCharacterDetails)
    .args(characterKey)
    .onSuccess(new OnSuccessCallback() {
      @Success public void onCharacterDetailsLoaded(Character character) {
        hideLoading();
        CharacterDetailViewModel characterDetailViewModel =
            mapper.mapCharacterToCharacterDetailViewModel(character);
        getView().showCharacterDetail(characterDetailViewModel);
      }
    }).execute();
  }

If we want to invoke a use case from a presenter, we need to create a UseCaseCall and need a strong reference, because the use case handler only works with weak references. An easy way to keep a strong reference is to save a useCaseCall as a field in your class.

UseCaseCall useCaseCall = new UseCaseCall(useCase, useCaseHandler);  

As we said, a use case could have more than one method, for this reason, we can have two methods with @UseCase and the same signature. To fix this problem in Rosie, we define the property name in the use cases.

@UseCase (name = "ById")
public void getCharacterDetails(String characterKey) throws Exception {  
    Character character = charactersRepository.getByKey(characterKey);
    notifySuccess(character);
}

And to invoke it:

    createUseCaseCall(getCharacterDetails)
    .args(characterKey)
    .useCaseName("ById")

If we look at the examples in depth, you can detect that there is nothing related to dealing with errors. Rosie is designed to develop a happy path, so we shouldn’t worry about the errors except those that are important by definition. If we need our use case to notify an error, we can do it using the method notifyError as the next code shows:

  @UseCase public void login(String user, String password) throws Exception {
    try {
        User user = session.login(user, password);
        notifySuccess(user);
    } catch (InvalidCredentialsException invalidCredentialsException) {
        notifyError(new InvalidCrentialsError());
    }
  }

The error handler is generic for the app. Any exception that happens inside a use case and is not handled must to be converted into an Error. Rosie allows us to choose to map between exceptions and errors. For this we need to define the ErrorFactory.

public class MarvelErrorFactory extends ErrorFactory {

  @Inject public MarvelErrorFactory() {
  }

  @Override public Error create(Exception exception) {
    Throwable targetException = exception;
    if (exception instanceof InvocationTargetException) {
      targetException = ((InvocationTargetException) exception).getTargetException();
    }


    if (targetException instanceof MarvelApiException) {
      MarvelApiException marvelApiException = (MarvelApiException) targetException;
      if (marvelApiException.getCause() instanceof UnknownHostException) {
        return new ConnectionError();
      }
    }
    return new UnknownError();
  }
}

With this, we unify all errors. All network errors will be mapped and we can draw them all together. This allows us to manage all generic errors together, or to unify errors that will be drawn the same. For this to work perfectly we need to provide our factory within the ErrorHandler.

  @Provides public ErrorHandler providesErrorHandler(MarvelErrorFactory errorFactory) {
    return new ErrorHandler(errorFactory);
  }

This should make the value that we want give to the use cases clear and how we wanted to detach ourselves from running the software and programming the happy path over the Android framework. In the next posts, we will talk about how it works with the data layer, from local to API requests and how to isolate from that.

If you want to see more in-depth code examples from the post, you can check out the Rosie sample code. All feedback and comments are always welcome.

Subscribe to Karumi Blog

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!