De MVP a Clean II. ¡Hagámoslo Clean!

Entender qué es eso de Clean Architecture no siempre resulta fácil ni intuitivo. En este artículo intentaré explicar los puntos claves de un proyecto Clean; centrándome en clarificar la visión de conjunto, pero sin descuidar esas peculiaridades que tantos quebraderos de cabeza nos dan en el día a día.

Antes de empezar, si no te has leído el artículo anterior, donde hablo sobre el patrón MVP en Android, te recomiendo que le eches un ojo, ya que lo utilizaré como base para hablar del tema que nos ocupa. 😉

¿De qué hablamos cuando hablamos de Clean?

En 2012, Robert C. Martin escribió un artículo en el que plasmó su propia visión de una arquitectura limpia, destacando cinco puntos importantes que siempre debe cumplir:

  1. Independiente de frameworks
  2. Testable
  3. Independiente de la UI
  4. Independiente de la base de datos
  5. Independiente de factores externos

Básicamente nos dice que nuestra lógica de negocio (el dominio) no debería depender ni verse limitada por ninguna librería externa, y que además tendríamos que ser capaces de testear dicha lógica de forma aislada, con independencia de la base de datos, el servidor web o la interfaz de usuario.

Este sería su esquema original de círculos concéntricos adaptado a la semántica y estructura propias de mi proyecto:

Clean Architecture

En este esquema se puede apreciar que las Entidades, que encapsulan la mayoría de la lógica general y de alto nivel, y los Casos de Uso, que se encargan de dirigir el flujo de datos desde y hacia las entidades, serán el centro de nuestro sistema, mientras que las herramientas (como la base de datos, el framework web, la plataforma…), que es el código más propenso al cambio, se mantienen alejadas para que su impacto sea menor.

Para terminar esta pequeña introducción, cabe destacar la famosa regla de dependencia. Esta regla dice que las dependencias a nivel de código fuente sólo pueden apuntar hacia dentro. Nada que se encuentre en un círculo interior puede saber algo sobre lo que hay en un círculo exterior; el motivo es que los círculos exteriores son mecanismos mientras que los interiores son políticas, y no interesa que el código más estable dependa de detalles de implementación.

Convertir los círculos en capas

Como se puede ver en la imagen, la aplicación quedará dividida en cuatro capas:

Android Layers

  • Data: aquí es donde ubico las implementaciones de los DataSources (fuentes de datos) y de Repository.
  • Domain: el paquete interactor contiene los casos de uso de la aplicación; además la entidad, o entidades, que de alguna forma definirá nuestro sistema, también se encuentra en esta capa.
  • Platform: aquí van las clases que dependen de la plataforma (Android); destacan sobre todo las Activities, los Fragments…
  • Presentation: los presenters, que gestionan la lógica de presentación, están aquí, y también las interfaces que representan a las vistas.
  •  

    He destacado las capas con colores para diferenciar aquellas que mantienen alguna dependencia de Android de las que están escritas en Java puro.

    Clean Architecture nos ayuda a esto, a separar responsabilidades, a modularizar nuestro sistema. Las herramientas las llevamos a lugares alejados del núcleo de nuestro proyecto, y así vamos aislando nuestra lógica de negocio (Domain) y nuestra lógica de presentación (Presentation), que podremos testear de forma unitaria, con independencia de cualquier factor externo.

    El lugar sobre el que las demás orbitan

    El dominio es el centro de nuestro sistema, donde debemos ubicar los casos de uso y los objetos que encapsulen las reglas de negocio (por ejemplo modelos ricos).

    El dominio no debe depender de nada que se encuentre en otra capa, siempre debe comunicarse con cualquier elemento externo a través de abstracciones; el motivo es que la parte más estable de nuestro sistema, nuestra lógica de negocio, no puede depender de detalles de implementación (un cambio en nuestro framework web o nuestra UI no debería obligarnos a modificar nuestra lógica de negocio).

    Use Cases

    Como he mencionado antes, los casos de uso son clases que dirigen el flujo de datos desde y hacia las entidades.

    Si las entidades encapsulaban la mayor parte de la lógica de negocio, serán los casos de uso los encargados de manejar la lógica propia de la aplicación. Para comprender esto mejor podemos pensar en los filtros: será el paquete data, a través de la implementación del repository, nuestro proveedor de datos, pero si esos filtros forman parte de la lógica de la aplicación, deberíamos aplicarlos en nuestro caso de uso, una vez recibida la colección de datos.

    El Handler será el callback que nos permitirá establecer la comunicación entre capas. Nos ayudará a cruzar fronteras. Es una interfaz. Recordad que nuestro caso de uso no debe conocer a nadie de una capa superior; no queremos que se vea afectado por aquellas clases más propensas al cambio.

    public interface Handler<T> {
      void handle(T result);
      void error(Exception exception);
    }

    Nuestro UseCase también es una interfaz. Al ejecutarse recibirá un elemento de tipo Handler, al que tendrá que retornar una respuesta, y un genérico que identificará los parámetros que necesite el caso de uso para cumplir su objetivo:

    public interface UseCase<T, P> {
      void execute(Handler<T> handler, P params);
    }

    public class GetMovie implements UseCase<Movie, GetMovie.Params> {

      private MoviesRepository repository;

      public GetMovie(MoviesRepository repository) {
        this.repository = repository;
      }

      @Override
      public void execute(final Handler<Movie> handler, Params params) {
        repository.getMovie(params.getMovieId(), new Handler<Movie>() {
          @Override
          public void handle(Movie movie) {
            handler.handle(movie);
          }
          @Override
          public void error(Exception ignored) {}
        });
      }

      public static final class Params {
        private final int movieId;

        public Params(int movieId) {
          this.movieId = movieId;
        }

        public int getMovieId() {
          return movieId;
        }
      }
    }

    Para completar el flujo ya solo nos falta que nuestro Presenter sea capaz de obtener una instancia del caso de uso que necesita, y que el susodicho caso de uso pueda comunicarse con la capa de datos. El Presenter cumplirá su objetivo a través de una factoría de casos de uso (UseCaseFactory), el UseCase a través de una instancia de Repository; ambas serán proporcionadas por nuestro inyector de dependencias, Dagger.

    Use Case Flow

    Inyección de dependencias

    La inyección de dependencias nació para reducir el acoplamiento entre los componentes (las clases) de nuestro sistema; básicamente es un patrón de diseño en el que se suministran objetos a una clase en lugar de ser la propia clase la que cree dichos objetos.

    Este patrón nos facilita mucho la vida a la hora de intentar cumplir uno de los principios SOLID, el de inversión de dependencias. Según este principio nuestras clases deben depender de abstracciones y no de detalles de implementación, esto las hace más fuertes frente al cambio e independientes de frameworks, además de más fáciles de testear (si por ejemplo creas una instancia dentro de un método, no podrás testear dicho método de forma aislada, ya que no tendrás forma de sustituir el comportamiento de la instancia creada, y cualquier error en el test te hará dudar de qué clase es la culpable).

    Dagger

    Dagger es un framework de inyección de dependencias, que se divide sobre todo en Componentes y Módulos. Los Módulos son las clases que se encargan de proveer las dependencias. Los Componentes son interfaces a las que están ligados uno o varios módulos; estas interfaces, que serán usadas por Dagger para generar el código, actúan como puente entre las dependencias que proveen los módulos y las clases donde serán inyectadas.

    Dagger Graphic

    La gráfica de arriba representa la estructura de componentes y módulos que utilizo para proveer las dependencias. Si nos fijamos destaca un componente, AppComponent, el componente principal al que están ligados los siguientes módulos:

    • AppModule: este módulo se encargará de proporcionar las dependencias únicas (Singleton) que pervivirán a lo largo del ciclo de vida de la aplicación.
    • ActivityBuilder: gracias a la anotación @ContributesAndroidInjector podemos acoplar fácilmente nuestras Activities y/o Fragments al grafo de dependencias, sin olvidarnos de hacer referencia al módulo concreto que nos proveerá la instancia del Presenter.
    • AndroidSupportInjectionModule: clase interna incluida en Dagger 2.10 que nos ayuda a enlazar los tipos propios de Android (Activity, Fragment, Service…).

    Al entrar en el método onCreate() de MoviesApp, se construirá AppComponent, y con él el grafo de dependencias. En tiempo de compilación habremos puesto en conocimiento de Dagger las Activities y/o Fragments que deseamos acoplar al grafo, y estaremos listos para acceder a las dependencias a través de la anotación @Inject:

    public class MoviesRepositoryImpl implements MoviesRepository {

      private MoviesLocalDataSource localDataSource;
      private MoviesRemoteDataSource remoteDataSource;
      private EntityDataMapper entityDataMapper;

      @Inject
      public MoviesRepositoryImpl(MoviesLocalDataSource localDataSource,
        MoviesRemoteDataSource remoteDataSource,
        EntityDataMapper entityDataMapper) {
        this.localDataSource = localDataSource;
        this.remoteDataSource = remoteDataSource;
        this.entityDataMapper = entityDataMapper;
      }

      @Override
      public void getMovies(final Handler<List<Movie>> handler) {
        // Code . . .
      }

      @Override
      public void getMovie(int movieId, final Handler<Movie> handler) {
        // Code . . .
      }
    }

    Conclusión

    Me gustaría terminar este artículo recopilando algunos conceptos importantes:

    • Los casos de uso definen la funcionalidad de tu aplicación.
    • Trabaja con la sensación de estar empujando los detalles de implementación lejos del muro. Deja el uso del framework web o de base de datos para las implementaciones de los data sources, tampoco permitas que tu Presenter sepa si la foto de perfil se mostrará usando Picasso o Glide, esto te ayudará a aislar tú código más estable del más propenso a cambios.
    • La capa Data y la capa Presentation pueden conocer la entidad de Dominio, pero el Dominio no puede conocer las entidades de Data y Presentation. Recuerda la regla de dependencia. Ante la duda, mantén cada entidad en su capa correspondiente y utiliza mappers en el Presenter y el Repository para convertirlas (¿no quieres que por ejemplo cada cambio en el parseo de tu framework web te obligue a tocar el Dominio, verdad?).
    • El flujo Presenter-UseCase-Respository está escrito en Java puro, no está ligado a la plataforma y debería poder testearse de forma unitaria. Si no es así, quizás no estás cumpliendo el principio de inversión de dependencias.

    Código fuente

    Enlaces de referencia