Uso de la arquitectura Clean

Este texto trato de dar una visión introductoria a la programación en Android, por lo tanto, el diseño de arquitecturas de software queda algo alejado de sus objetivos. No obstante, pensamos que puede ser interesante comentar algunos conceptos y tratar de seguir unos ejemplos con una arquitectura adecuada.

A medida que una aplicación crece comprobarás que cada vez resulta más difícil de mantener. Si no seguimos unas reglas claras en el desarrollo, el caos está garantizado. Por ejemplo, un error típico en Android suele ser dar demasiadas responsabilidades a las actividades. MainActivity puede llegar a tener cientos, o incluso miles, de líneas de código. Si estas responsabilidades las separamos en varias clases, el código será más fácil de entender, tendrá menos errores y será más reutilizable.

Para estructurar las clases de la aplicación nos vamos a inspirar en la arquitectura Clean[1]. Aunque de forma muy simplificada, no vamos a realizar inyección de dependencias, usar patrones como modelo-vista-controlador, ni otros temas que complicarían en exceso la aplicación. En una primera aproximación y dado el tamaño de la aplicación, creo que sería excesivo.
Clean no es una arquitectura tal cual, si no una serie de guías y buenas prácticas en el desarrollo de software. Fue definida por Rober C Martin (Uncle Bob) en su charla “Architecture the lost years”, donde exponía una serie de problemas y el alto acoplamiento de los desarrollos de software tanto a los modelos de datos como a la interfaz.

Clean define una serie de capas y otorga una responsabilidad a cada una, pero no entra en profundidad en los detalles de implementación y cómo se deben resolver los problemas.

El objetivo es escribir software que esté lo menos acoplado posible a nuestro modelo de datos, a la representación de este y al framework que estemos usando. Esto va a incrementar la estabilidad de nuestro código ya que va a hacer más fácil cambiar las partes dependientes del sistema. Va a facilitar la portabilidad a otros entornos (como iOS o Web) dado que gran parte del código es independiente del framework. También va a permitir postergar decisiones de implementación, como por ejemplo la persistencia o el uso de red. Podemos hacer una primera versión de nuestro software que guarde los datos de forma local y de una forma sencilla cambiarlo a online. O elegir el framework de persistencia cuando nos sea necesario y no antes.

Aunque dentro de Clean existe variantes, en la aplicación Mis Lugares vamos a organizar las clases en 4 capas:

Capa de Modelo.

También se utiliza el nombre de Dominio o Lógica de Negocio. Está formada por las clases que representan la lógica interna de la aplicación y cómo representamos los datos con los que vamos a trabajar. Muchas clases de esta capa se conocen como POJO (Plain Old Java Object) al tratarse de clases Java puras. Ejemplos de POJO serían las clases Lugar o GeoPunto. No es conveniente que en estas clases se utilicen APIs externos. Si abres las clases que hemos indicado, podrás comprobar que no necesitan ningún import.

Capa de Datos.

En esta capa estarían las clases encargadas de guardar de forma permanente los datos y cómo acceder a ellos. Suelen representar bases de datos, servicios Web, preferencias, ficheros JSON… También es conocida como capa de almacenamiento o persistencia.

Capa de Casos de Uso.

Los casos de uso son clases que van a definir las operaciones que el usuario puede realizar con nuestra aplicación. Esta capa no sería estrictamente necesaria (por ejemplo, en Asteroides no la vamos a utilizar), pero resulta muy interesante para tener enumeradas las diferentes acciones que vamos a implementar. Además, va a permitir quitar mucha responsabilidad a las actividades. Los casos de uso también se conocen como interactors.

Capa de Presentación.

Representa la interfaz de usuario, por lo que está formada por las actividades, fragments, vistas y otros elementos con los que interactúa el usuario.

Una de las características más importantes de esta arquitectura es la regla de dependencia entre capas. Para representar las dependencias se suelen usar un diagrama en forma de círculos concéntricos. Las capas más internas son aquellas que están más cercanas a nuestra lógica de dominio, no deben depender de las capas más externas del software, aquellas que están más cerca a los agentes externos como el framework, o el interfaz de usuario.

Por ejemplo, la clase Lugar que pertenecería a la capa de Modelo, va a poder ser utilizada por el resto de las capas y no puede usar otras clases que no sean de su capa. Por el contrario, una actividad perteneciente a la capa de presentación no debería usarse por el resto de las capas y puede usar cualquier capa interior.

Nota: En la literatura la capa de casos de uso es más interna que la de datos. En esta implementación se ha realizado al revés. Reamente no vamos a seguir la Arquitectura Clean de forma estricta. En nuestra implementación la capa de Usos de Datos va a tener dependencias con la capa de Presentación, cosa que habría que evitar.

Organizando las clases en paquetes.

Cuando trabajas con muchos ficheros suele ser muy práctico organizarlos en carpetas. Podrías tener todos los ficheros de una asignatura en una misma carpeta, pero seguramente prefieras crear una carpeta para cada unidad o una carpeta por tipo de fichero (transparencias, ejercicios, …).

El número de clases de un proyecto Android también puede ser muy elevado, por lo que resulta complicado localizarlas. Para resolver este problema, resulta frecuente organizar las clases en diferentes paquetes. Podemos usar diferentes criterios, por ejemplo, por módulos del proyecto (como autentificación, visualización, mapas…) Otro criterio podría ser por entidades (como lugares, usuarios…) También podemos utilizar la función de la clase (como actividades, fragments, adaptadores…). En este ejercicio utilizaremos como criterio la capa de la arquitectura (en concreto modelo, datos, casos de uso y presentación). La organización propuesta se muestra debajo. Pero eres libre de usar nombres en inglés o definir tu propio criterio. El paquete donde se encuentra cada clase no afectará a los ejercicios.


 

Ejercicio: Organizar las clases en paquetes

1.     Pulsa con el botón derecho sobre com.example.mislugares y selecciona New / Package. Escribe presentacion, se creará com.example.mislugares.presentacion

2.     Arrastra todas las clases terminadas en Activity a este paquete. El proceso de refactorización se realiza de forma bastante automática. Aunque en algunos casos tendrás que realizar algún pequeño ajuste, como hacer público algún método. Añade también en este paquete fragments y adaptadores.

3.     Repite este proceso para los paquetes datos, modelo y casos_uso. Para ver donde se sitúa cada clase puedes usar la imagen anterior.

Ejercicio: Casos de Uso para lugares

1.     Crea la clase CasosUsoLugar  con el siguiente código: NOTA: Si has hecho el ejercicio anterior dentro del paquete.

public class CasosUsoLugar {
   private Activity actividad;
   private RepositorioLugares lugares;

   public CasosUsoLugar(Activity actividad, RepositorioLugares lugares) {
      this.actividad = actividad;
      this.lugares = lugares;
   }
// OPERACIONES BÁSICAS
   public void mostrar(int pos) {
      Intent i = new Intent(actividad, VistaLugarActivity.class);
      i.putExtra("pos", pos);
      actividad.startActivity(i);
   }
}  
class CasosUsoLugar(val actividad: Activity,
                    val lugares: RepositorioLugares) {
// OPERACIONES BÁSICAS
   fun mostrar(pos: Int) {
      val i = Intent(actividad, VistaLugarActivity::class.java)
      i.putExtra("pos", pos);
      actividad.startActivity(i);
   }
} 

Dentro de esta clase vamos a añadir diferentes funciones que ejecutarán distintos casos de uso referentes a un lugar. Por ejemplo, compartir un lugar, borrarlo, … De momento solo añadimos un caso de uso, mostar(pos), que arrancará una actividad mostrando la información del lugar  según su posición en el RecyclerView.
Se han añadido dos propiedades a la clase. La primera, actividad, nos va a permitir extraer el contexto o lanzar otras actividades.

Nota: No se cumple la regla de dependencias de la arquitectura Clean, se añade porque va a simplificar el código. La segunda, lugares, nos va a permitir acceder al repositorio de los lugares.

La segunda, lugares, nos va a permitir acceder al repositorio de los lugares.

2.    Añade en MainActivity las siguiente propiedades:

private RepositorioLugares lugares;
private CasosUsoLugar usoLugar;

@Override protected void onCreate(Bundle savedInstanceState) {
   …
   lugares = ((Aplicacion) getApplication()).lugares;
   usoLugar = new CasosUsoLugar(this, lugares);
} 
lateinit var lugares: RepositorioLugares 
lateinit var usoLugar: CasosUsoLugar

override fun onCreate(savedInstanceState: Bundle?) {
   …
   lugares = (application as Aplicacion).lugares
   usoLugar = CasosUsoLugar(this, lugares) 
}
 

3.    Cada vez que queramos ejecutar este caso de uso usaremos el código:

usoLugar.mostrar(pos) 

Este código será usado en el en siguiente ejercicio. Para evitar que de error puedes comentar el contenido de la función mostrar().

Práctica: Casos de Uso para arrancar actividades
 

1. Crea la clase CasosUsoActividades dentro del paquete casos_uso.

2. Crea la función lanzarAcerdaDe(). Ha de contener el código necesario para arrancar AcerdaDeActivity.

3. Añade en MainActivity el código necesario para usar este caso de uso.

4. En esta clase también se puedes añadir otros casos de uso como lanzarPreferencias() o lanzarMapa(), para abrir las actividades adecuadas.

 

Una vez que completes la aplicación Mis Lugares las clases para casos de uso acaban conteniendo decenas de métodos. De esta forma, va a ser muy sencillo saber qué hace cada método, donde está y las dependencias que necesita. De lo contrario, todo este código acabaría en las actividades, que contendría cientos de líneas de código, siendo muy difíciles de mantener.

[1] https://devexperto.com/clean-architecture-android/