De onde se comunicar ou lidar com situações de erro? – Método Tradicional
Em um aplicativo, existem situações de erro que podem ser consideradas esperadas e, no entanto, existem outras que são anômalas.
Como desenvolvedores, devemos levar em consideração o máximo “razoável” de cada uma dessas situações e tratá-las como o usuário espera.
Veremos alguns exemplos simples usando Kotlin com programação de objetos mais tradicional e como eles podem ser resolvidos, pois muitas vezes até em exemplos simples a forma de resolvê-lo pode gerar algum problema adicional que não tínhamos.
Os exemplos que veremos pressupõem uma aplicação usando Clean Architecture, Hexagonal ou similar.
Conteudo
Lista de resultados não encontrada
É bastante comum no desenvolvimento de qualquer tipo de aplicação que em algum momento tenhamos que mostrar uma lista onde possamos pesquisar.
Agora imagine que o usuário realiza uma busca na lista e em nossa fonte de dados não há dados que se enquadrem na combinação de filtros feita pelo usuário.
Esta é uma situação de falha ou erro, mas esperada pelo usuário e se encaixa em um fluxo de trabalho normal.
O que um usuário espera é que a lista resultante esteja vazia, mas não que nosso software falhe por esse motivo.
No máximo, você poderia esperar uma mensagem, mas o resultado da operação por não retornar resultados é tão óbvio que a mensagem geralmente é demais.
Se estivéssemos desenvolvendo uma API, o código de resposta http deveria ser 200 e o corpo uma lista vazia.
Sendo uma situação esperada em nosso domínio ou negócio, não devemos classificar este cenário como um erro no domínio de forma alguma.
Tratando o problema
Ao lidar com o problema, temos duas opções:
- Retornar nulo do repositório
class MovieRepository (val context: Application): MovieRepository { val baseAddress = context.getString(R.string.base_address) var allMovies:MutableList<Movie> = mutableListOf<Movie>() ... override fun getAllByText (text: String): List<Movie>?{ if (allMovies.count{ it.title.contains(text) } > 0){ return allMovies.filter{ it.title.contains(text) } } else { return null; } } ... }
Então, em algum ponto da cadeia, devemos converter esse nulo em uma lista vazia, que é o que o usuário espera.
Essa opção é a que eu menos gosto, existe muita literatura sobre o problema de retorno de null.
Retornar null não é uma boa ideia porque me força nas camadas acima a me proteger contra null sempre que preciso interagir com o resultado.
Além disso, como é um erro que só é detectado na execução, se eu esquecer de me proteger em qualquer caso, não o detectarei em tempo de compilação.
- Retorna uma lista vazia do repositório
class MovieRepository (val context: Application): MovieRepository { val baseAddress = context.getString(R.string.base_address) var allMovies:MutableList<Movie> = mutableListOf<Movie>() ... override fun getAllByText (text: String): List<Movie>{ return allMovies.filter{ it.title.contains(text) } } ... }
Esta é a opção que costumo utilizar, é a mais simples e não me gera problemas adicionais.
Evitamos o problema de ter que lidar com nulos e é a opção mais natural.
Neste caso é a opção mais simples porque a função de filtro, se não houver dados, já retorna uma lista vazia. Mas se este não for o caso, eu geralmente gero uma lista vazia à mão para não retornar nulo em nenhum caso.
Resultado simple no encontrado
Outro caso bastante comum é ter que gerenciar quando um recurso simples não existe.
Por exemplo, ao desenvolver uma API, se eles solicitarem um recurso com um identificador e ele não existir.
Esta é uma situação de erro anômala que não se enquadra em um fluxo normal ou sem falha.
Ao desenvolver uma API, se este cenário ocorrer, devemos comunicar um erro ao usuário, retornar um código de erro http 404 é geralmente o mais adequado.
Em um aplicativo padrão é mais complicado saber sobre esse problema além de um problema no código porque o normal é que o usuário solicite um recurso de nós escolhendo-o previamente em uma lista, caso em que o aplicativo não deve criar, mas mostrar uma mensagem para o do utilizador.
Mas onde devemos gerar o erro? E como devemos tratá-lo?
Tratando o problema
Ao lidar com o problema, também temos várias opções neste caso:
- Retornar nulo do repositório e lançar exceção do domínio
Neste cenário, retornaríamos null do repositório caso o recurso não existisse.
class MovieRepository (val context: Application): MovieRepository { val baseAddress = context.getString(R.string.base_address) var allMovies:MutableList<Movie> = mutableListOf<Movie>() ... override fun getById(id: Long): Movie? { val movie = getAll().firstOrNull { it.id == id } return movie } ... }
FirstOrNull retorna null se os dados não existirem de acordo com o filtro.
E depois verificamos no caso de uso ou serviço de aplicação se é nulo e nesse caso invocamos a função subscrita correspondente para comunicar o erro à camada de apresentação no thread de interface.
class GetMovieByIdUseCase (private val movieRepository: MovieRepository, executor: Executor): UseCase(executor) { ... fun run() { try { val movie = movieRepository.getById(id) if (movie == null){ uiExecute {onMovieNotFoundError()} }else { uiExecute { onMovieLoaded(movie!!) } } } catch (ex: Exception) { uiExecute {onConnectionError()} } } }
Essa opção pode parecer fazer sentido porque estamos gerenciando quando um cenário de erro ocorre no domínio por meio do caso de uso ou serviço de aplicativo.
Mas a desvantagem é que isso nos força a retornar nulo do repositório e eu prefiro não retornar nulo em nenhum caso.
- Levante a exceção de domínio do repositório e trate-a no caso de uso
Nesse cenário, lançamos uma exceção do repositório.
class MovieRepository (val context: Application): MovieRepository { val baseAddress = context.getString(R.string.base_address) var allMovies:MutableList<Movie> = mutableListOf<Movie>() ... override fun getById (id: Long) = getAll().firstOrNull{it.id == id} ?: throw MovieNotFoundException() } ... }
E, no caso de uso, coletamos a exceção e, nesse caso, invocamos a função assinada correspondente para comunicar o erro à camada de apresentação no thread de interface.
class GetMovieByIdUseCase (private val movieRepository: MovieRepository, executor: Executor): UseCase(executor) { ... fun run() { try { val movie = movieRepository.getById(id) uiExecute {onMovieLoaded(movie)} } catch (ex: MovieNotFoundException) { uiExecute {onMovieNotFoundError()} } catch (ex: Exception) { uiExecute {onConnectionError()} } } }
Resolvendo o problema desta forma, podemos evitar lidar com nulo em qualquer ponto do aplicativo onde o repositório é usado.
Esta abordagem mantém as responsabilidades que teoricamente um repositório deveria ter, como adaptador, que é a conversão de uma linguagem de infraestrutura para uma linguagem de domínio, uma vez que a exceção seria definida no domínio.
O uso de exceções pode ser complicado porque, historicamente, elas foram mal utilizadas e abusadas para modificar o fluxo de execução, criando exceções para modelar casos que não são excepcionais.
Em alguns contextos, como importações em massa em que casos excepcionais podem ocorrer com frequência, o uso de exceções pode ser um problema de desempenho porque são mais caras.
Além disso, o uso de exceções é um problema se você trabalhar com simultaneidade, pois as exceções não cruzam os encadeamentos, elas vivem apenas no encadeamento onde são criadas e você precisa usar retornos de chamada ou algum padrão de design do tipo Observer para comunicar a exceção entre os encadeamentos.
Outro problema adicional com o uso de exceções é que ele não deixa claro para o cliente quais situações excepcionais podem ocorrer.
Conclusões
Neste artigo, vimos como podemos gerenciar situações normais ou de não falha em um aplicativo usando Clean Architecture, Hexagonal ou semelhante por meio de conceitos de programação orientada a objetos, como listas vazias ou exceções, evitando o retorno de null do repositório.
Em um aplicativo de usuário não muito complexo, podem ser opções válidas, mas as exceções têm algumas limitações que vimos.
É possível tratar os casos excepcionais com uma abordagem de programação funcional sem o uso de exceções, podemos usar um valor de retorno do tipo opção ou do repositório que contém o objeto retornado em caso de sucesso ou falha.
Como poderíamos implementá-lo desta forma, deixo para uma segunda parte que isso já está um pouco longo 🙂