
Olá Habr!
Todo mundo adora exceções de tempo de execução. Não há melhor maneira de descobrir que algo não foi levado em consideração ao escrever o código. Especialmente - se as exceções estão descartando o aplicativo entre milhões de usuários, e essas notícias chegam em um email de pânico no portal de análise. Sábado de manhã. Quando você estiver em uma viagem pelo campo.
Depois disso, você pensa seriamente no tratamento de erros - e quais são as possibilidades que o Kotlin nos fornece?
O primeiro a vir à mente é tentar pegar. Para mim - uma ótima opção, mas tem dois problemas:
- Afinal, é um código extra (um invólucro forçado ao redor do código não afeta a legibilidade da melhor maneira).
- Nem sempre (especialmente ao usar bibliotecas de terceiros) do bloco de captura é possível receber uma mensagem informativa sobre o que exatamente causou o erro.
Vamos ver o que o try-catch transforma o código ao tentar resolver os problemas acima.
Por exemplo, a função de execução de consulta de rede mais simples
fun makeRequest(request: RequestBody): List<ResponseData>? { val response = httpClient.newCall(request).execute() return if (response.isSuccessful) { val body = response.body()?.string() val json = ObjectMapper().readValue(body, MyCustomResponse::class.java) json?.data } else { null } }
torna-se como
fun makeRequest(request: RequestBody): List<ResponseData>? { try { val response = httpClient.newCall(request).execute() return if (response.isSuccessful) { val body = response.body()?.string() val json = ObjectMapper().readValue(body, MyCustomResponse::class.java) json?.data } else { null } } catch (e: Exception) { log.error("SON YOU DISSAPOINT: ", e.message) return null } }
"Não é tão ruim", alguém pode dizer, "você e seu kotlin querem todo o código", acrescenta (esta é uma citação) - e ele estará ... duas vezes certo. Não, não haverá holivares hoje - todo mundo decide por si mesmo. Pessoalmente, eu governei o código do analisador json auto-escrito, onde a análise de cada campo era envolvida em try-catch, enquanto cada um dos blocos catch estava vazio. Se alguém está satisfeito com este estado de coisas - uma bandeira nas mãos. Eu quero oferecer uma maneira melhor.
A maioria das linguagens de programação funcional digitadas oferece duas classes para lidar com erros e exceções:
Try e
Either . Tente manipular exceções e Ou para manipular erros de lógica de negócios.
A biblioteca Arrow permite que você use essas abstrações com o Kotlin. Assim, você pode reescrever a solicitação acima da seguinte maneira:
fun makeRequest(request: RequestBody): Try<List<ResponseData>> = Try { val response = httpClient.newCall(request).execute() if (response.isSuccessful) { val body = response.body()?.string() val json = ObjectMapper().readValue(body, MyCustomResponse::class.java) json?.data } else { emptyList() } }
Como essa abordagem é diferente do uso de try-catch?
Antes de tudo, qualquer pessoa que leia esse código depois de você (e provavelmente o fará) já poderá entender por assinatura que a execução do código pode levar a um erro - e escrever um código para processá-lo. Além disso, o compilador jurará se isso não for feito.
Em segundo lugar, há flexibilidade na maneira como o erro pode ser tratado.
Inside Try, o erro ou sucesso da execução é representado como as classes Failure e Success, respectivamente. Se quisermos que a função sempre retorne algo com erro, podemos definir o valor padrão:
makeRequest(request).getOrElse { emptyList() }
Se uma manipulação de erro mais complexa for necessária, o fold será útil:
makeRequest(request).fold( {ex ->
Você pode usar a função de recuperação - seu conteúdo será completamente ignorado se Try retornar Sucesso.
makeRequest(request).recover { emptyList() }
Você pode usar para compreensões (emprestadas pelos criadores de Arrow da Scala) se precisar processar o resultado Success usando uma sequência de comandos chamando a fábrica .monad () em Try:
Try.monad().binding { val r = httpclient.makeRequest(request) val data = r.recoverWith { Try.pure(emptyList()) }.bind() val result: MutableList<Data> = data.toMutableList() result.add(Data()) yields(result) }
A opção acima pode ser escrita sem o uso de ligação, mas será lida de uma maneira diferente:
httpcilent.makeRequest(request) .recoverWith { Try.pure(emptyList()) } .flatMap { data -> val result: MutableList<Data> = data.toMutableList() result.add(Data()) Try.pure(result) }
No final, o resultado da função pode ser processado usando quando:
when(response) { is Try.Success -> response.data.toString() is Try.Failure -> response.exception.message }
Assim, usando o Arrow, você pode substituir a construção try-catch longe do ideal por algo flexível e muito conveniente. Uma vantagem adicional de usar o Arrow é que, apesar de a biblioteca se posicionar como funcional, você pode usar abstrações individuais a partir daí (por exemplo, a mesma tentativa) enquanto continua a escrever um bom código OOP antigo. Mas eu aviso: você pode gostar e se envolver, em algumas semanas começará a estudar Haskell e seus colegas em breve deixarão de entender seu raciocínio sobre a estrutura do código.
PS: Vale a pena :)