Camada de domínios

A camada de domínios é uma camada opcional que fica entre a camada da IU e a camada de dados.

Quando incluída, a camada de domínios opcional oferece dependências para
    a camada da IU e depende da camada de dados.
Figura 1. O papel da camada de domínios na arquitetura do app.

A camada de domínios é responsável por encapsular a lógica de negócios complexa ou simples que é reutilizada por vários ViewModels. Essa camada é opcional, porque nem todos os apps vão ter esses requisitos. Use-a apenas quando necessário, por exemplo, para lidar com a complexidade ou favorecer a reutilização.

Uma camada de domínios oferece estes benefícios:

  • Evita a duplicação de código.
  • Melhora a legibilidade em classes que usam classes da camada de domínios.
  • Melhora a capacidade de teste do app.
  • Evita classes grandes ao permitir a divisão das responsabilidades.

Para manter as classes simples e leves, cada caso de uso precisa ter responsabilidade apenas sobre uma única funcionalidade e não pode conter dados mutáveis. Em vez disso, você precisa gerenciar dados mutáveis na IU ou nas camadas de dados.

Convenções de nomenclatura neste guia

Neste guia, os casos de uso recebem o nome da única ação pela qual são responsáveis. A convenção é esta:

verbo no presente + substantivo/o que (opcional) + UseCase.

Por exemplo, FormatDateUseCase, LogOutUserUseCase, GetLatestNewsWithAuthorsUseCase ou MakeLoginRequestUseCase.

Dependências

Em uma arquitetura de app típica, as classes de caso de uso se encaixam entre ViewModels da camada de IU e repositórios da camada de dados. Isso significa que as classes de caso de uso geralmente dependem das classes de repositório e se comunicam com a camada da interface da mesma maneira que os repositórios: usando callbacks (em Java) ou corrotinas (em Kotlin). Para saber mais sobre isso, consulte a página sobre a camada de dados.

Por exemplo, no app, é possível ter uma classe de caso de uso que busque dados de um repositório de notícias e repositório de autor e os combine:

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

Como os casos de uso contêm lógica reutilizável, eles também podem ser usados por outros casos. É normal haver vários níveis de casos de uso na camada de domínios. Por exemplo, o caso de uso definido no exemplo abaixo pode usar o FormatDateUseCase se várias classes da camada de IU dependem de fusos horários para mostrar a mensagem adequada na tela.

class GetLatestNewsWithAuthorsUseCase(
  private val newsRepository: NewsRepository,
  private val authorsRepository: AuthorsRepository,
  private val formatDateUseCase: FormatDateUseCase
) { /* ... */ }
GetLatestNewsWithAuthorsUseCase depende das classes de repositório da
    camada de dados, mas também depende do FormatDataUseCase, outra classe de caso de uso
    que também está na camada de domínios.
Figura 2. Exemplo de gráfico das dependências para um caso de uso que depende de outros casos de uso.

Casos de uso de chamadas no Kotlin

No Kotlin, você pode fazer com que seja possível chamar as instâncias de classe de caso de uso como funções, definindo a função invoke() com o modificador operator. Veja o exemplo abaixo:

class FormatDateUseCase(userRepository: UserRepository) {

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

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

Nesse exemplo, o método invoke() no FormatDateUseCase possibilita chamar instâncias da classe como se fossem funções. O método invoke() não está restrito a uma assinatura específica. Ele pode usar qualquer número de parâmetros e retornar qualquer tipo. Também é possível sobrecarregar o operador invoke() com assinaturas diferentes na classe. O caso de uso do exemplo acima é chamado desta maneira:

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

Para saber mais sobre o operador invoke(), consulte os documentos do Kotlin (em inglês).

Ciclo de vida

Os casos de uso não têm ciclo de vida próprio. Em vez disso, eles têm escopo definido para a classe que os usa. Isso significa que você pode chamar casos de uso das classes na camada da IU, de serviços ou da própria classe do Application. Como os casos de uso não podem conter dados mutáveis, é preciso criar uma nova instância de uma classe de caso de uso sempre que a transmitir como uma dependência.

Linha de execução

Os casos de uso da camada de domínios precisam ter proteção para linha de execução principal. Em outras palavras, é preciso que seja seguro para eles fazerem chamadas da linha de execução principal. Se as classes de caso de uso realizam operações de bloqueio de longa duração, elas são responsáveis por mover essa lógica para a linha de execução adequada. No entanto, antes de fazer isso, confira se essas operações de bloqueio podem estar melhor posicionadas em outras camadas da hierarquia. Normalmente, cálculos complexos ocorrem na camada de dados para incentivar a reutilização ou o armazenamento em cache. Por exemplo, uma operação com uso intensivo de recursos em uma lista grande fica melhor posicionada na camada de dados do que na camada de domínios quando o resultado precisa ser armazenado em cache para ser reutilizado em várias telas do app.

O exemplo abaixo mostra um caso de uso que realiza o trabalho em uma linha de execução em 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.
    }
}

Tarefas comuns

Esta seção descreve como executar tarefas comuns da camada de domínio.

Lógica de negócios simples e reutilizável

É necessário encapsular a lógica de negócios reproduzível presente na camada de IU em uma classe de caso de uso. Isso facilita a aplicação de mudanças em qualquer lugar que a lógica for usada. Isso também possibilita que você teste a lógica de forma isolada.

Considere o exemplo do FormatDateUseCase descrito anteriormente. Caso seus requisitos comerciais relacionados à formatação de data mudem no futuro, você só vai precisar mudar o código em um local centralizado.

Combinar repositórios

Em um app de notícias, você pode ter as classes NewsRepository e AuthorsRepository, que processam notícias e operações de dados do autor, respectivamente. A classe Article que o NewsRepository expõe contém apenas o nome do autor, mas você quer mostrar mais informações sobre o autor na tela. As informações do autor podem ser encontradas no AuthorsRepository.

GetlatestNewsWithAuthorsUseCase depende de duas classes
    diferentes da camada de dados: NewsRepository e AuthorsRepository.
Figura 3. Gráfico das dependências de um caso de uso que combina dados de vários repositórios.

Como a lógica envolve vários repositórios e pode se tornar complexa, você cria uma classe GetLatestNewsWithAuthorsUseCase para abstrair a lógica do ViewModel e a tornar mais legível. Isso também torna a lógica mais reutilizável e fácil de testar isoladamente em diferentes partes do 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
        }
}

A lógica mapeia todos os itens na lista news. Assim, mesmo que a camada de dados tenha proteção para linha de execução principal, esse trabalho não deve bloquear a linha de execução principal porque você não sabe quantos itens vão ser processados. É por isso que o caso de uso move o trabalho para uma linha de execução em segundo plano usando o agente padrão.

Outros consumidores

Além da camada de IU, a camada de domínios pode ser reutilizada por outras classes, como serviços e a classe do Application. Além disso, se outras plataformas, como TV ou Wear, compartilharem bases de código com o app para dispositivos móveis, a camada de interface também vai poder reutilizar os casos de uso para aproveitar todos os benefícios da camada de domínios mencionados acima.

Restrição de acesso à camada de dados

Outra consideração ao implementar a camada de domínios é se você ainda precisa permitir o acesso direto à camada de dados da IU ou forçar tudo na camada de domínios.

A camada de interface não pode acessar a camada de dados diretamente. É preciso passar pela camada de domínios.
Figura 4. Gráfico de dependência mostrando a camada de IU com acesso negado à camada de dados.

Uma vantagem de fazer essa restrição é que ela impede que a interface ignore a lógica da camada de domínios, por exemplo, quando você executa registros de análise em cada solicitação de acesso à camada de dados.

No entanto, a desvantagem possivelmente significativa é que isso força você a adicionar casos de uso, mesmo quando são apenas chamadas de função simples para a camada de dados, o que pode aumentar a complexidade com pouco benefício.

Uma boa abordagem é adicionar casos de uso somente quando necessário. Se você acha que a camada de interface está acessando dados por casos de uso quase exclusivamente, talvez seja recomendável acessar dados apenas dessa maneira.

Por fim, a decisão de restringir o acesso à camada de dados depende da sua base de código individual e de você preferir regras rígidas ou uma abordagem mais flexível.

Testes

As diretrizes gerais de teste se aplicam ao testar a camada de domínios. Para outros testes de interface, os desenvolvedores geralmente usam repositórios falsos. Essa é uma prática recomendada ao testar a camada do domínios.

Exemplos

Os exemplos do Google abaixo demonstram o uso de uma camada de domínios. Acesse-os para conferir a orientação na prática: