Android

6 jul, 2017

Como o Kotlin pode nos ajudar no tratamento de erros

Publicidade

A Google anunciou no I/O deste ano que Kotlin é agora uma linguagem oficial para desenvolvimento Android, o que pegou alguns de nós de surpresa. Existe uma série de diferenças entre Kotlin e o velho Java e é importante que tenhamos a compreensão de como podemos resolver velhos problemas de forma diferente e de uma maneira muito melhor. A ideia não é dissecar a linguagem neste artigo, mas ter um pequeno vislumbre de como a linguagem é rica, expressiva e com recursos interessantes até mesmo para tratarmos uma exceção de forma mais limpa.

Em Java todas as classes herdam direta ou indiretamente de Object. Em Kotlin, a raiz de todas as classes é o tipo Any. A novidade é que em Kotlin temos também a classe Nothing que, por sua vez, é subclasse de todas as classes.

Seu propósito está muito bem explicado por Lorenzo Quiroli no seu artigo Nothing (else) matters in Kotlin e se você ainda não leu, recomendo que o faça antes de continuar. Logo, minha intenção aqui é simplesmente mostrar o uso do Nothing para falharmos, por assim dizer, gracefully e de quebra, é claro, deixar nosso código mais limpo.

Pensemos na situação em que queremos exibir o nome de um usuário e seu nome está armazenado em uma propriedade name de um objeto User que podemos ter ou não está guardado em um dado repositório. Suponhamos que a exibição desse nome na tela é fundamental e caso ele não exista, queremos disparar uma exceção. Vamos começar com um pouco de código de como poderíamos fazer isso:

fun getUser(): User? {
   ...
}
 
val user: User? = getUser()
if (user != null) {
   println("Hello, ${user.name}")
} else {
   throw IllegalStateException("Nenhum usuário encontrado!")
}

Note que a função getUser() retorna o tipo User?, o que significa que ela pode retornar um objeto do tipo User ou null (e sim, null tem um tipo em Kotlin).

Em seguida, precisamos fazer a checagem do objeto retornado e, caso ele exista, exibimos na tela; caso contrário, disparamos a nossa exceção.

Mas enfim, isto é Kotlin; portanto, deveríamos ser menos verbosos, certo? Então, vamos eliminar esses null-checks explícitos:

val user: User = getUser() ?: throw IllegalStateException("Nenhum usuário encontrado!")
println("Hello, ${user.name}")

Melhor agora que usamos o elvis operator ?:, sem ifs e duas linhas de código apenas. Mas o que o elvis operator faz? Se getUser() retornar um valor, então a value user irá referenciar esse valor; caso contrário, dispare a exceção e estamos “bem”. Ou seja, ele proporciona um fallback value na expressão ou, no caso em questão, dispara a exceção para a execução.

Agora, vamos tornar nosso tratamento de erro mais genérico. Sabemos que a função getUser() pode retornar null, mas isso pode ocorrer por motivos distintos. Pode ter ocorrido um erro na nossa query no banco de dados, a internet ficou intermitente na hora, a deserialização foi feita com um  json mal formado, enfim, algo deu muito errado. Podemos, então, querer lançar nossa exceção com uma mensagem diferente, dependendo do erro que efetivamente ocorreu. Vamos, então, criar uma outra função chamada fail, que vai ter como única responsabilidade disparar uma exceção com essa mensagem variável de acordo com a situação:

fun fail(message: String) {
   throw IllegalStateException(message)
}
….
val user: User = getUser() ?: fail("Nenhum usuário encontrado!")
println("Hello, ${user.name}")

Aparentemente ficou do jeito que queríamos, mas o código acima tem um problema: ele não compila. Temos um erro de type mismatch ali porque a value user espera por um objeto do tipo User e a função fail retorna o tipo Unit (que é o equivalente ao void no Java).

Lembrando que qualquer função declarada em Kotlin que não tenha tipo de retorno explícito, implicitamente retornará Unit, como é o caso da função fail acima. A saber:

fun fail(message: String) {
   throw IllegalStateException(message)
}
 
É equivalente a:
 
fun fail(message: String): Unit {
   throw IllegalStateException(message)
}

Então, como resolver isso de forma genérica de uma maneira que o compilador fique feliz? Basta declarar Nothing como o tipo de retorno da nossa função fail:

throw IllegalStateException(message)
}
….
val user: User = getUser() ?: fail("Nenhum usuário encontrado!")
println("Hello, ${user.name}")

Mas por que assim funciona? Porque, como dito anteriormente, o tipo Nothing é subclasse de todas as outras classes, logo o retorno da função fail acima será inferido pelo compilador como sendo do tipo User.

Se a função getUser() acima, então, retornar null, a saída que teremos será algo do tipo:

Exception in thread "main" java.lang.IllegalStateException: Nenhum usuário encontrado!
	at com.whatever.MainTestKt.fail(MainTest.kt:13)
	at com.whatever.MainTestKt.main(MainTest.kt:4)

Chegamos onde queríamos, mas às vezes queremos algo mais simples, sem essa stacktrace toda, mostrando só a mensagem de erro. Normalmente, não desejaríamos isso, pois só com a mensagem de erro perdemos insumo para debugar e rastrear o problema, mas existem casos e casos. Um destes em que precisei fazer isso foi no tratamento de erros em uma ferramenta de linha de comando que implementei em Kotlin. Nela, em determinadas situações, só o que quero é que a execução pare e a mensagem de erro seja exibida. Uma forma de se fazer isso é implementando a interface Thread.UncaughtExceptionHandler:

fun fail(message: String): Nothing {
   val throwable = Throwable(message)
   Thread.setDefaultUncaughtExceptionHandler { t, e -> System.err.println(e.message) }
   throw throwable
}
….
val user: User = getUser() ?: fail("Nenhum usuário encontrado!")
println("Hello, ${user.name}")

A classe Thread do Java recebe como único argumento do seu método Thread.setDefaultUncaughtExceptionHandler uma implementação da interface Thread.UncaughtExceptionHandler. Esta interface possui um nome especial no Java 8 – SAM interface (ou Single Abstract Method interface), nome que se dá às chamadas functional interfaces, que são interfaces que possuem um único método. Assim como no Java 8, em Kotlin, funções que recebem como argumento SAM interfaces podem ser substituídas por lambdas, desde que sua assinatura seja a mesma do método único da interface declarada em Java. A interface Thread.UncaughtExceptionHandler, em Java é, então, declarada assim:

@FunctionaInterface
public interface UncaughtExceptionHandler {
	void uncaughtException(Thread t, Throwable e);
}

Logo, em Kotlin, podemos chamá-la usando lambdas! Assim:

Thread.setDefaultUncaughtExceptionHandler ({ t, e -> //... })

Outra novidade é que, em Kotlin, funções que recebem lambdas como último parâmetro (no caso aqui o único) podem receber o corpo do lambda do lado de fora dos parênteses, de forma que fique assim:

Thread.setDefaultUncaughtExceptionHandler () { t, e -> //... }

E como no caso aqui a função lambda não é só o último parâmetro como o único, podemos omitir completamente os parênteses, assim:

Thread.setDefaultUncaughtExceptionHandler { t, e -> //... }

Bem melhor, não?

De volta ao nosso exemplo, qualquer linha de código depois de fail não será executada e agora a saída que teremos se o retorno da função getUser() for null será somente a mensagem “Nenhum usuário encontrado!”.

Como podemos ver, Kotlin possui um bocado de syntactic sugars e paradigmas funcionais que observamos nas linguagens funcionais mais modernas. Com isso tudo, temos um ferramental poderoso para reduzirmos nosso código para o que realmente interessa. Mas isso quer dizer que devemos abandonar o Java? Depende. O Java, de fato, tem suas limitações, mas não devemos esquecer que ele é a fundação de todas as outras linguagens que executam em cima da JVM e continua evoluindo, embora a passos curtos, afinal existe a preocupação com a retrocompatibilidade. Então, eu acredito que é importante para qualquer um que não tenha uma base um pouco mais sólida no que acontece por trás dos panos, que continue fazendo tudo do velho jeito Java de ser, no qual tudo é muito explícito e burocrático para depois sim, desenhar seu próprio paralelo e fazer a escolha certa para o seu caso, entendendo de fato o que está ganhando.

Ficou alguma dúvida ou tem algo a dizer sobre o artigo? Aproveite os campos abaixo. Até a próxima!

***

Artigo original publicado em: https://www.concrete.com.br/2017/06/21/kotlin-no-tratamento-de-erros/