Capa de dominio

La capa de dominio es una capa opcional que se encuentra entre la capa de la IU y la de datos.

Cuando está incluida, la capa opcional de dominio brinda dependencias a la capa de la IU y depende de la capa de datos.
Figura 1: La función de la capa de dominio en la arquitectura de la app

La capa de dominio es responsable de encapsular la lógica empresarial compleja o la lógica empresarial simple que varios ViewModels reutilizan. Esta capa es opcional porque no todas las apps tendrán estos requisitos. Solo debes usarla cuando sea necesario; por ejemplo, para administrar la complejidad o favorecer la reutilización.

Una capa de dominio brinda los siguientes beneficios:

  • Evita la duplicación de código.
  • Mejora la legibilidad en las clases que usan las de la capa de dominio.
  • Mejora la capacidad de prueba de la app.
  • Evita las clases grandes, ya que te permite dividir las responsabilidades.

Para que estas clases sean simples y livianas, cada caso de uso solo debe tener responsabilidad sobre una sola funcionalidad y no deben contener datos mutables. Como alternativa, debes controlar los datos mutables de la capa de la IU o de la de datos.

Convenciones de nombres en esta guía

En esta guía, los casos de uso se nombran según la acción única de la que son responsables. La convención es la siguiente:

verbo en tiempo presente + sustantivo/qué (opcional) + UseCase.

Por ejemplo, FormatDateUseCase, LogOutUserUseCase, GetLatestNewsWithAuthorsUseCase o MakeLoginRequestUseCase.

Dependencias

En una arquitectura típica de la app, las clases de casos de uso se ajustan entre ViewModels de la capa de IU y repositorios de la capa de datos. Eso significa que las clases de caso de uso suelen depender de las clases de repositorio y se comunican con la capa de la IU de la misma manera que lo hacen los repositorios: mediante devoluciones de llamada (para Java) o corrutinas (para Kotlin). Para obtener más información, consulta la página sobre la capa de datos.

Por ejemplo, en tu app, puedes tener una clase de caso de uso que obtenga datos de un repositorio de noticias y un repositorio de autores, y los combine como se muestra a continuación:

class GetLatestNewsWithAuthorsUseCase(
  private val newsRepository: NewsRepository,
  private val authorsRepository: AuthorsRepository
) { /* ... */ }

Debido a que los casos de uso contienen lógica reutilizable, también se pueden usar en otros casos. Es normal tener varios niveles de casos de uso en la capa de dominio. Por ejemplo, el caso de uso que se define en el siguiente ejemplo puede usar el caso de uso FormatDateUseCase si varias clases de la capa de la IU dependen de las zonas horarias para mostrar el mensaje apropiado en la pantalla:

class GetLatestNewsWithAuthorsUseCase(
  private val newsRepository: NewsRepository,
  private val authorsRepository: AuthorsRepository,
  private val formatDateUseCase: FormatDateUseCase
) { /* ... */ }
GetLatestNewsWithAuthorsUseCase depende de las clases de repositorio de la capa de datos, pero también depende de FormatDataUseCase, otra clase de caso de uso que también está en la capa de dominio.
Figura 2: Ejemplo del gráfico de dependencia para un caso de uso que depende de otros casos.

Casos de uso de llamadas en Kotlin

En Kotlin, si defines la función invoke() con el modificador operator, puedes hacer instancias de la clase de caso de uso se puedan llamar como funciones. Observa el siguiente ejemplo:

class FormatDateUseCase(userRepository: UserRepository) {

    private val formatter = SimpleDateFormat(
        userRepository.getPreferredDateFormat(),
        userRepository.getPreferredLocale()
    )

    operator fun invoke(date: Date): String {
        return formatter.format(date)
    }
}

En este ejemplo, el método invoke() en FormatDateUseCase te permite llamar a instancias de la clase como si fueran funciones. El método invoke() no está restringido a ninguna firma específica. Además, puede tomar cualquier cantidad de parámetros y mostrar cualquier tipo. También puedes sobrecargar invoke() con diferentes firmas en la clase. Deberías llamar al caso de uso del ejemplo anterior de la siguiente manera:

class MyViewModel(formatDateUseCase: FormatDateUseCase) : ViewModel() {
    init {
        val today = Calendar.getInstance()
        val todaysDate = formatDateUseCase(today)
        /* ... */
    }
}

Para obtener más información sobre el operador invoke(), consulta la documentación de Kotlin.

Ciclo de vida

Los casos de uso no tienen su propio ciclo de vida. En su lugar, se limitan a la clase que los usa. Esto significa que puedes llamar a los casos de uso de clases en la capa de la IU, de servicios o de la clase Application. Debido a que los casos de uso no deberían contener datos mutables, debes crear una instancia nueva para la clase del caso de uso cada vez que la pases como una dependencia.

Subprocesos

Los casos de uso de la capa de dominio deben ser seguros para el subproceso principal; en otras palabras, deben ser seguros para llamar desde el subproceso principal. Si las clases de caso de uso realizan operaciones de bloqueo de larga duración, son responsables de trasladar esa lógica al subproceso correspondiente. Sin embargo, antes de hacer eso, comprueba si esas operaciones de bloqueo se ubicarían mejor en otras capas de la jerarquía. Por lo general, los cálculos complejos ocurren en la capa de datos para fomentar la reutilización o el almacenamiento en caché. Por ejemplo, se recomienda ubicar una operación que requiere un uso intensivo de recursos en una lista grande en la capa de datos que en la capa de dominio si el resultado debe almacenarse en caché a fin de volver a usarlo en varias pantallas de la app.

En el siguiente ejemplo, se muestra un caso de uso en el que el trabajo se realiza en un subproceso en segundo plano:

class MyUseCase(
    private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {

    suspend operator fun invoke(...) = withContext(defaultDispatcher) {
        // Long-running blocking operations happen on a background thread.
    }
}

Tareas comunes

En esta sección, se describe cómo realizar tareas comunes en la capa de dominio.

Lógica empresarial simple reutilizable

Se recomienda encapsular la lógica empresarial repetible presente en la capa de la IU en una clase del caso de uso. Esto facilita el uso de cualquier cambio en todos los lugares en los que se usa la lógica. También te permite probar la lógica de forma aislada.

Considera el ejemplo de FormatDateUseCase que se describió antes. Si tus requisitos empresariales sobre el formato de fecha cambia en el futuro, solo necesitas modificar el código en un lugar centralizado.

Combinación de repositorios

En una app de noticias, es posible que tengas las clases NewsRepository y AuthorsRepository, que controlan las operaciones de datos de noticias y autores, respectivamente. La clase Article que NewsRepository expone solo contiene el nombre del autor, pero quieres mostrar más información sobre este en la pantalla. La información del autor se puede obtener de AuthorsRepository.

GetLatestNewsWithAuthorsUseCase depende de dos clases de repositorios diferentes de la capa de datos: NewsRepository y AuthorsRepository.
Figura 3: Gráfico de dependencia para un caso de uso que combina datos de varios repositorios

Como la lógica implica varios repositorios y puede volverse compleja, debes crear una clase GetLatestNewsWithAuthorsUseCase para abstraer la lógica del ViewModel y hacerla más legible. Esto también hace que la lógica sea más fácil de probar en aislamiento y que se pueda reutilizar en diferentes partes de la app.

/**
 * This use case fetches the latest news and the associated author.
 */
class GetLatestNewsWithAuthorsUseCase(
  private val newsRepository: NewsRepository,
  private val authorsRepository: AuthorsRepository,
  private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
    suspend operator fun invoke(): List<ArticleWithAuthor> =
        withContext(defaultDispatcher) {
            val news = newsRepository.fetchLatestNews()
            val result: MutableList<ArticleWithAuthor> = mutableListOf()
            // This is not parallelized, the use case is linearly slow.
            for (article in news) {
                // The repository exposes suspend functions
                val author = authorsRepository.getAuthor(article.authorId)
                result.add(ArticleWithAuthor(article, author))
            }
            result
        }
}

La lógica asigna todos los elementos de la lista news, por lo tanto, aunque la capa de datos sea segura para el subproceso principal, este trabajo no debería bloquearlo porque no sabes cuántos elementos procesará. Es por eso que, en el caso de uso, se traslada el trabajo a un subproceso en segundo plano mediante el despachador predeterminado.

Otros procesos de compra de consumible

Además de la capa de la IU, la capa de dominio puede reutilizarse en otras clases, como en la de servicios y la de Application. Además, si otras plataformas, como TV o Wear, comparten la base de código con la app para dispositivos móviles, su capa de la IU también puede reutilizar casos de uso a fin de obtener todos los beneficios que se mencionaron antes de la capa de dominio.

Restricción del acceso a la capa de datos

Otra consideración que debes tener en cuenta cuando implementas la capa de dominio es si aún debes permitir el acceso directo a la capa de datos desde la capa de la IU o forzar todo a través de la capa de dominio.

La capa de la IU no puede acceder a la capa de datos directamente, sino que debe pasar por la capa de dominio.
Figura 4. Gráfico de dependencia en el que se muestra la denegación de acceso a la capa de datos de la capa de la IU.

Una de las ventajas de esta restricción es que impide que la IU omita la lógica de la capa de dominio, por ejemplo, si realizas un registro de estadísticas en cada solicitud de acceso a la capa de datos.

Sin embargo, la desventaja potencialmente significativa es que te obliga a agregar casos prácticos, incluso cuando solo son llamadas simples a la capa de datos, lo que puede agregar complejidad con pocos beneficios.

Un buen enfoque es agregar casos de uso solo cuando sea necesario. Si descubres que la capa de la IU accede a los datos mediante casos de uso casi de manera exclusiva, puede ser útil solo acceder a los datos de esta manera.

En última instancia, la decisión de restringir el acceso a la capa de datos depende de tu base de código individual y de si prefieres establecer reglas estrictas o utilizar un enfoque más flexible.

Pruebas

Si quieres probar la capa del dominio, aplica la guía de pruebas generales. Para otras pruebas de la IU, los desarrolladores suelen usar repositorios falsos. También se recomienda usar repositorios falsos cuando se prueba la capa del dominio.

Ejemplos

En los siguientes ejemplos de Google, se demuestra el uso de la capa de dominio. Explóralos para ver esta guía en práctica: