Clean architecture es un tema que nunca pasa de moda en el mundo Android, y a partir de los comentarios y preguntas que recibo, me da la sensación de que todavía no está muy claro.

Sé que hay decenas (o probablemente cientos) de artículos relacionados con clean architecture pero aquí he querido dar un enfoque más pragmático / simplista que puede ayudar en tu primera incursión a la clean architecture. Es por eso que voy omitir conceptos que pueden parecer ineludibles para los puristas de la arquitectura.

Mi principal objetivo aquí es que entiendas lo que considero el punto principal (y más complicado) en clean architecture: la inversión de dependencias. Una vez que comprendas esó, puedes ir a otros artículos para rellenar los pequeños huecos que puedan haber quedado fuera.

Clean architecture: ¿por qué debería importarme?

Incluso si decides no utilizar arquitecturas en tus aplicaciones, creo que aprenderlas es realmente interesante, ya que te ayudará a entender conceptos muy importantes de la programación orientada a objetos.

Las arquitecturas permmiten desacoplar diferentes unidades de tu código de manera organizada. De esta forma el código se hace más fácil de entender, modificar y testear.

Pero las arquitecturas complejas, como la clean architecture pura, también pueden tener el efecto contrario: desacoplar el código también significa crear un montón de fronteras, modelos, transformaciones de datos… que pueden terminar aumentando la curva de aprendizaje de tu código hasta un punto en el que no merezca la pena.

Así que, como se debe hacer con todo lo que se aprende, pruébalo en el mundo real y decide qué nivel de complejidad estás dispuesto a introducir. Dependerá del equipo, el tamaño de la aplicación, el tipo de problemas que resuelve…

¡Así que vamos a empezar! En primer lugar, vamos a definir las capas que va a usar nuestra aplicación.

Las capas de clean architecture

Este punto se puede enfocar de maneras muy diferentes. Sin embargo, por simplicidad, me voy a limitar a 5 capas (que ya es lo suficientemente complejo de todas formas 😂):

1. Presentación

Es la capa que interactúa con la interfaz de usuario. Es probable que veas esta capa dividida en dos en otros ejemplos, ya que técnicamente se podría extraer todo menos las clases de la arquitectura a otra capa. Pero en la práctica, casi nunca se le saca partido, y complica las cosas.

Esta capa de presentación por lo general consiste en la interfaz de usuario de Android (activities, fragments, views) y presenters o view models, según el patrón de presentación que decidas utilizar. Si usa MVP, tengo un artículo donde explico en profundidad (y me gustaría escribir uno sobre MVVM en breve).

2. Casos de uso

A veces también se les llama interactors. Se trata principalmente de las acciones que el usuario puede desencadenar. Estos pueden ser acciones activas (el usuario hace clic en un botón) o acciones implícitas (la App navega a una pantalla).

Si quieres ser extra-pragmático, puedes incluso quitarte esta capa. A mí gusta porque por lo general es el punto en el que me cambio de hilo. A partir de este punto, puedo ejecutar todo lo demás en un un hilo secundario y olvidarme de tener cuidado con el hilo de UI. De esta forma, no necesito preguntarme más si algo se está ejecutando en el hilo de interfaz de usuario o un hilo de background.

3. Dominio

También conocida como la lógica de negocio. Estas son las reglas de tu negocio.

Contiene todos los modelos de negocio. Por ejemplo, en una App de películas, podría ser la clase Movie, la clase de Subtitles, etc.

Idealmente, debería ser la capa más grande, aunque es cierto que Apps Android por lo general lo único que hacen es dibujar una API en la pantalla de un teléfono, por lo que la mayor parte de la lógica va a consistir simplemente en la solicitud y la persistencia de datos.

4. Datos

En esta capa se encuentra una definición abstracta de las diferentes fuentes de datos, y la forma en que se debe utilizar. Aquí se suele usar un patrón repositorio que, para una determinada solicitud, es capaz de decidir dónde encontrar la información.

Una App típica podría guardar sus datos localmente y recuperarlos desde la red. Así que esta capa puede comprobar si los datos están en una base de datos local. Si están ahí y no están caducadon, devolverlos como resultado, y de lo contrario pedirlos a la API y guardarlos localmente.

Pero los datos no tienen que provenir solamente de una petición a un API. Es posible, por ejemplo, utilizar los datos de los sensores del dispositivo, o de un BroadcastReceiver (¡aunque la capa de datos nunca debe saber acerca de este concepto! Lo veremos más adelante)

5. Framework

Puedes encontrar esta capa con muchos nombres distintos. Básicamente encapsula la interacción con el framework, por lo que el resto del código puede ser agnóstico y reutilizable en caso de que quieras desarrollar la misma aplicación para otra plataforma (una opción real hoy en día con los proyectos multi-plataforma en Kotlin). Con “framework” no sólo me refiero al framework de Android, sino a cualquier biblioteca externa que queremos ser capaces de reemplazar fácilmente en el futuro.

Por ejemplo, si la capa de datos debe persistir algo, aquí se podría utilizar Room para hacerlo. O si tienes que hacer una petición, se usaría Retrofit. O se puede acceder a los sensores para solicitar alguna información. ¡Lo que sea que necesites!

Esta capa debe ser tan simple como sea posible, ya que toda la lógica debería ser abstraída en la capa de datos.

¡Recuerda! Estas son las capas sugeridas, pero algunas de ellas se pueden combinar. Incluso se puede simplemente tener tres capas: presentación – dominio – framework. Esto probablemente no puede llamarse estrictamente clean architecture, pero sinceramente me dan un poco igual los nombres. Dejaré 5 capas, ya que ayuda a explicar el punto siguiente, que creo que es el importante.

Interacción entre capas

Esta es la parte más difícil de explicar y entender. Voy a tratar de ser lo más claro posible, porque creo que este es también el punto más importante si se quiere entender la clean architecture. Pero no dudes en escribirme si no entiendes algo, y actualizaré el artículo para aclararlo.

Cuando se piensa en una forma lógica de interacción, se diría que la presentación utiliza la capa de casos de uso, que a su vez va a utilizar el dominio para acceder a la capa de datos, la que finalmente va a utilizar el framework para obtener acceso a los datos solicitados. A continuación, estos datos van de vuelta por la estructura de capas hasta llegar a la capa de presentación, que actualiza la interfaz de usuario. Esto sería un gráfico sencillo de lo que está sucediendo:

Como puedes ver, los dos límites del flujo dependen del framework, por lo que requieren el uso de la dependencia de Android, mientras que el resto de las capas sólo requieren Kotlin. Esto es realmente interesante si desea dividir cada capa en un sub-módulo por separado. Si fueses a reutilizar este mismo código para (digamos) una aplicación web, simplemente necesitarías reimplementar las capas de presentación y framework.

Pero no mezcles el flujo de la aplicación con la dirección de las dependencias entre las capas. Si has leído acerca de la clean architecture antes, es probable que vieras este gráfico:

Que es un poco diferente de la imagen anterior. Los nombres también son diferentes, pero vemos esto en un minuto. Básicamente, la clean architecture dice que tenemos capas exteriores e interiores, y que las capas internas no deben saber nada sobre las externas. Esto significa que una clase externa puede tener una dependencia explícita de una clase interna, pero no al revés.

Vamos a recrear el gráfico anterior con nuestras propias capas:

Así que desde la interfaz de usuario hacia la de dominio, todo es bastante simple, ¿verdad? La capa de presentación tiene una dependencia de casos de uso, y es capaz de llamar para iniciar el flujo. A continuación, el caso de uso tiene una dependencia al dominio.

Sin embargo, los problemas aparecen cuando vamos desde el interior hacia el exterior. Por ejemplo, cuando la capa de datos necesita algo del framework. Como se trata de una capa interna, la capa de datos no sabe nada acerca de las capas externas, por lo que ¿cómo puede comunicarse con ellos?

Presta atención, que aquí viene lo importante.

Principio de Inversión de Dependencia

Si has oído hablar de los principios SOLID, puede que hayas leído acerca del principio de inversión de dependencias. Pero, como pasa con muchos de estos conceptos, es posible que no que no te quedara claro cómo aplicarlo a un ejemplo real. La inversión de la dependencia es la “D” de los principios SOLID, y esto es lo que dice:

A. Los módulos de alto nivel no deben depender de los módulos de bajo nivel. Ambos deben depender de abstracciones.
B. Las abstracciones no deben depender de los detalles. Los detalles deben depender de las abstracciones.

Sinceramente esto a mí no me dice mucho mucho en mi trabajo del día a día. Sin embargo, con este ejemplo, te va a ser más fácil entenderlo:

Un ejemplo de inversión de dependencia

Digamos que tenemos un DataRepository en la capa de datos que requiere una RoomDatabase para recuperar unos elementos guardados en un base de datos. El primer enfoque sería tener algo como esto.

Este es nuestro RoomDatabase:

Y el DataRepository utilizaría una instancia de la misma:

¡Pero esto no es posible! La capa de datos no sabe nada de las clases en Framework porque es una capa más interna.

Así que el primer paso es hacer una inversión de control (no mezclar con la inversión de dependencias, no son lo mismo), lo que significa que en lugar de crear instancias de la clase nosotros mismos, dejamos nos vengan dadas del exterior (a través del constructor):

Fácil ¿verdad? Pero aquí es donde tenemos que hacer la inversión de dependencias. En vez de depender de la implementación específica, vamos a depender de una abstracción (una interfaz). Por lo que el módulo de dominio tendrá la siguiente interfaz:

Ahora el DataRepository puede utilizar la interfaz (que se encuentra en su misma capa):

Y ya que la capa de datos puede utilizar la capa de dominio, puede implementar esa interfaz:

El único punto que faltaría es buscar una forma de proporcionar la dependencia al DataRepository. Eso se hace mediante inyección de dependencias. Las capas externas se harán cargo de ella.

No voy a profundizar en la inyección de dependencia en este artículo, ya que no quiero añadir muchos conceptos complejos. tengo un unos cuantos artículos que hablan sobre ello y sobre Dagger, por si quieres ampliar sobre el tema.

Implementando un ejemplo

Lo primero de todo es que puedes encontrar el ejemplo completo en este repositorio. Voy a omitir algunos detalles, así que puedes ir allí a echarle un ojo, hacer fork y jugar un poco con el ejemplo.

Todo esto está muy bien, pero hay que ponerlo en práctica si queremos entenderlo por completo.

Para ello, vamos a crear una App que permitirá solicitar la ubicación actual gracias a un botón y mantener un registro de las ubicaciones previamente solicitadas, mostrándolas en una lista.

Creación de un proyecto de ejemplo

El proyecto consistirá en un conjunto de 5 módulos.

Por simplicidad, sólo voy a crear 4:

  • app: Será el único proyecto que utiliza el framework de Android, que incluirá las capas de presentación y de Framework.

  • usecases: Será un módulo Kotlin (no necesita el framework de Android).

  • dominio: Otro módulo Kotlin.

  • datos: Un módulo Kotlin también.

Se podría hacer perfectamente todo en un único módulo, y utilizar paquetes en su lugar. Pero es más fácil de no violar el flujo de dependencias si se hace así. Así que recomiendo te lo recomiendo si te estás iniciando.

Crea un nuevo proyecto, que creará automáticamente el módulo de aplicación, y luego crea los módulos adicionales de Java:

No hay una opción “Kotlin library”, por lo que tendrás que añadir el soporte para Kotlin después.

La capa de dominio

Necesitamos una clase que representa la ubicación. En Android, ya tenemos una clase Location. Pero recordemos que queremos dejar los detalles de implementación en las capas exteriores.

Imagina que quieres utilizar este código para una aplicación web escrita en KotlinJS. Aquí no tendrías acceso a las clases de Android.

Así que esta es la clase:

Si has leído sobre esto antes, una clean architecture pura tendría una representación del modelo por capa, que en nuestro caso implicaría tener una clase Location en cada capa. A continuación, se usarían transformaciones de datos para convertirlos al pasar de una capa a otra. Eso hace que las capas estén menos acopladas, pero también todo lo más complejo. En este ejemplo, sólo voy a hacerlo cuando sea estrictamente necesario.

Esto es todo lo que necesitas en esta capa para este ejemplo sencillo.

La capa de datos

La capa de datos normalmente se modela usando repositorios que acceden a los datos que neceistamos. Podemos tener algo como esto:

Tiene una función para obtener las ubicaciones pedidas anteriormente, y otra función para solicitar una nueva.

Como se puede ver, esta capa está utilizando la capa de dominio. Una capa exterior puede utilizar las capas internas (pero no a la inversa). Para ello, es necesario agregar una nueva dependencia al build.gradle` del módulo:

El repositorio se va a utilizar un par de orígenes (o sources):

Uno de ellos tiene acceso a las ubicaciones almacenadas, y el otro a la ubicación actual del dispositivo.

Y aquí es donde sucede la magia inversión de dependencias. Estas dos fuentes son interfaces:

Y la capa de datos no sabe (y no necesita saber) cuál es la implementación real de estas interfaces. Las ubicaciones almacenadas y del dispositivo deben ser gestionadas por el framework específico del dispositivo. Una vez más, volviendo al ejemplo de KotlinJS, una aplicación web implementaría esto de forma muy diferente a la de una App para Android.

Ahora, el LocationsRepository puede utilizar estas fuentes sin necesidad de conocer la implementación final:

La capa de Casos de Uso

Esta es por lo general una capa muy simple, que simplemente convierte las acciones del usuario en las interacciones con el resto de capas internas. En nuestro caso, vamos a tener un par de casos de uso:

  • GetLocations: Devuelve las ubicaciones que ya han sido registradas por la aplicación.

  • RequestNewLocation: Le dirá al LocationsRepository que pida la ubicación actual.

Estos casos de uso tendrán una dependencia al LocationRepository:

La capa del Framework

Esta va a ser parte del módulo de app, e implementará principalmente las dependencias que se ofrecen al resto de capas. En nuestro caso particular, será LocationPersistenceSource y DeviceLocationSource.

El primero podría ser implementado con Room, por ejemplo, y el segundo con el LocationManager. Pero con la intención de hacer esta explicación más simple, voy a utilizar implementaciones fake. Podría implementar la solución real en algún momento, pero esto sólo añadiría complejidad a la explicación, por lo que prefiero que nos olvidemos de ello por ahora.

Para la persistencia, voy a usar una implementación en memoria sencilla:

Y un generador aleatorio para el otro:

Piensa en esto en un proyecto real. Gracias a las interfaces, durante la implementación de una nueva funcionalidad, se pueden proporcionar dependencias fake mientras se trabaja en el resto del flujo, y olvidarse de los detalles de implementación hasta el final.

Esto también demuestra que estos detalles de implementación son fácilmente intercambiables. Así podrías hacer que tu App trabaje inicialmente con persistencia en memoria, y luego más adelante usar una base de datos. Se pueden implementar como queramos, y luego reemplazarlos.

O imagina que una nueva biblioteca aparece (como Room 😂) y quieres probarlo y considerar una posible migración. Sólo tienes que implementar la interfaz utilizando la nueva librería, sustituir la dependencia, ¡y ya lo tienes funcionando!

Y, por supuesto, esto también ayuda en los tests, en el que podemos sustituir estos componentes por fakes o mocks.

La capa de presentación

Y ahora podemos implementar la interfaz de usuario. Para este ejemplo, voy a usar MVP, porque este artículo se originó por las preguntas en el artículo original sobre MVP y porque creo que es más fácil de entender que el uso de MVVM con architecture components. Sin embargo, ambos enfoques son muy similares.

En primer lugar, tenemos que escribir el Presenter, el cual recibirá una vista como dependencia (la interfaz del Presenter para interactuar con su vista) y los dos casos de uso:

Todo muy sencillo, dejando a un lado la forma de realizar tareas en segundo plano. He usado corrutinas (de hecho, la librería Anko, para utilizar bg). No estoy seguro de si es la mejor decisión, porque quería mantener este ejemplo tan fácil como sea posible. Así que si no se entiendo bien, indícamelo en los comentarios. Puedes saber más sobre las corrutinas en este artículo que escribí hace algún tiempo.

Por último, el MainActivity. Con el fin de evitar el uso de un inyector de dependencias, he declarado aquí las dependencias:

No recomiendo usar esto para una App grande, ya que se podrían reemplazar las dependencias en los tests de UI, por ejemplo, pero es suficiente para este ejemplo.

Y el resto del código no necesita mucha explicación. Tenemos un RecyclerView y un botón. Cuando se hace clic en el botón, se llama al Presenter para que se solicite una nueva ubicación:

Y cuando el Presenter termina, se llama al método de la View. Esta interfaz la implementa la Activity de esta manera:

Conclusión

¡Esto es todo! Los fundamentos de la clean architecture son de hecho bastante simples.

Sólo es necesario entender cómo funciona la inversión de dependencias, y luego enlazar las capas correctamente. Sólo recuerda no agregar dependencias de los módulos externos a los internos, y tendrás mucha ayuda del IDE para hacer las cosas bien.

Sé que esto es una mucha información de golpe. Mi objetivo es que esto sirve como un punto de entrada para las personas que nunca vieron la clean architecture antes. Así que todavía tienes dudas, házmelo saber en los comentarios y reescribiré este artículo tantas veces como sea necesario.

Y también recuerda que con el fin de hacer este simple artículo, he omitido algunas complejidades que encontrarías  en una clean architecture habitual. Una vez que tengas esto claro, te sugiero que leas otros ejemplos más completos. Siempre me gusta recomendar éste de Fernando Cejas.

El enlace al repositorio de Github está aquí. Puedes ir allí para echar un ojo a los pequeños detalles, y si te gusta, por favor, házmelo saber con una estrella 🙂

Author: Antonio Leiva

Soy un apasionado de Kotlin. Hace ya más de dos años que estudio el lenguaje y su aplicación a Android para ayudarte a ti a aprenderlo de la forma más sencilla posible.