Concrete Logo
Hamburger button

Como o Kotlin pode nos ajudar no tratamento de erros

  • Blog
  • 21 de Junho de 2017
Share

O anúncio feito pela Google no I/O deste ano de 2017 de que Kotlin é agora uma linguagem oficial para desenvolvimento Android 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 de uma maneira muito melhor. A ideia não é dissecar a linguagem neste post, 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 post 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 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:

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:

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 e 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:

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:

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:

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:

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:

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:

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

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:

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

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 post? Aproveite os campos abaixo. Até a próxima!

É desenvolvedor Android e quer trabalhar em um time fantástico? Clique aqui.