Projete boas APIs com Java exceptions
Henrique Lobo Weissmann (Kico)
Posted on June 18, 2021
No meu último post falei um pouco sobre um dos meus recursos favoritos do Java que são as exceptions. Lá falei um pouco sobre como as exceções nos ajudam a pensar contratos, mas agora gostaria de ir um pouco além. Agora vou mostrar como tornar suas APIs muito mais ricas usando exceptions.
Mas o que é uma API mesmo?
Não vou dourar a pílula, sendo assim vou definir de uma forma bem direta:
é a parte executável do seu programa que você irá expor a outros programadores para que eles a executem
Repare que não usei a palavra "sistema" aqui, mas sim "programa". Quando você projeta uma classe que possuirá métodos públicos, pensados para que outros programadores a usem, você está criando uma API (mesmo que você seja este outro programador).
Existe algum programa que não crie uma API Kico? Yeap: pense nos scripts que você escreve para automatizar suas tarefas. É aquele tipo de programa que você apenas quer que seja executado. Softwares feitos apenas para uso direto normalmente não expõem uma API.
Ah, mas aí, se outro programa o chamar, eu posso dizer que aquilo ali é uma API? Aí você relativizou a coisa e este post não sai. :)
Falaremos aqui não de APIs REST, mas sim aquela que se manifesta sob a forma de código executável, ou seja, os métodos públicos que você declara em suas classes ou os abstratos em suas interfaces.
O que é uma boa API?
Uma boa API é aquela que nos diz exatamente o que será feito. Tudo começa a partir do nome escolhido pelo programador para aquele método, que deverá expor de forma evidente a intenção daquele código.
Um bom nome já nos deu boa parte do que precisamos, o segundo componente são os parâmetros que a API espera. Idealmente devem ser poucos e com uma granularidade adequada. Vamos começar com um exemplo simples que iremos ir melhorando durante o post (sim, é código real):
int cadastrarPessoa(int id, String nome, String sobrenome, Date dataNascimento)
Reparou como a granularidade está errada? Por que não simplesmente passar um objeto do tipo Pessoa a ser persistido, tal como a versão melhorada que mostrarei a seguir? Ainda não é uma API perfeita, mas é inegavelmente mais simples (e o programador sofre menos no momento em que for digitar seu código):
class Pessoa {
int cadastrarPessoa(Pessoa pessoa) (...)
}
Há outro aspecto na API que deve ser levado em consideração: o tipo de retorno. Nossa versão anterior retorna um valor inteiro, que representa o identificador do registro no banco de dados. Se houver um erro, ela poderia simplesmente nos retornar um valor negativo: "-1: sem id, -2: sem nome, -3: sem sobrenome" e por aí vai. O valor de retorno terá então duplo sentido: o óbvio (retornar o identificador) e identificar um erro (quebra de contrato).
Um cliente da API então escreveria código similar ao exposto a seguir para lidar com erros:
switch (cadastrarPessoa(pessoa) {
case -1:
System.out.println("Opa! Sem o ID que deve ser preenchido antes!");
break;
case -2:
(...)
}
Sofrimento eterno que poderia ser um pouco aliviado incluindo algumas constantes, mas ainda seria um sofrimento eterno. Vimos alguns bons pontos na definição de uma API:
- Um bom nome
- Uma boa definição de parâmetros
- Um valor de retorno que seja significativo (e possua uma única função)
Falta algo: os limites da API, ou seja, as condições para que ela funcione. É muito difícil tornar isto explícito em uma API REST, mas com código executável, especialmente em linguagens que possuam um recurso como as exceptions do Java, não. Como seria uma terceira versão da nossa API?
class PessoaNegocio {
void cadastrar(Pessoa pessoa) throws Validação (...)
}
Não preciso mais retornar um valor inteiro: se a persistência for bem sucedida, o próprio método já vai preencher o atributo "id" do objeto que passei como parâmetro. E se algo der errado? Uma exceção chamada Validação (evite caracteres especiais no seu código) será disparada, e nesta se encontrarão os detalhes a respeito do que deu errado.
A exceção é parte do contrato: ela nos diz algo como:
Ok, vou cadastrar esta pessoa no banco de dados, mas apenas se o objeto tiver valores válidos para todos os atributos.
Nossa API agora tem um limite bem definido: você lê a assinatura do método e sabe que somente objetos válidos, ou seja, aqueles cujo estado interno esteja de acordo com o que se espera, será persistido no banco de dados.
O programador agora pode escrever código ainda mais interessante:
try {
// o fluxo principal fica BEM isolado
negocio.cadastrar(pessoa);
} catch (Validação erroValidacao) {
// eu sei que meu problema é de validação
// talvez eu possa projetar algum comportamento
// de retentativa, ou mesmo informar melhor o
// usuário final a respeito da bobagem que está
// tentando fazer
}
E aqui entra mais um ponto que você deve levar em consideração quando for escrever sua API: os erros que talvez seus clientes não consigam aliviar. Uma falha no seu SGBD. Será que seria legal tentar melhorar um pouco mais nossa API tal como no exemplo abaixo?
void cadastrar(Pessoa pessoa) throws Validação, JDBCException
Antes eu sabia que objetos inválidos não seriam persistidos: agora também sei que uma falha no banco de dados pode ocorrer. Mais do que isto, sei que é um banco de dados relacional (JDBCException). Aqui entra o seu contexto.
Você quer que os usuários da sua API saibam que por trás dos panos está um SGBD relacional? Se sim, ok. Se não, trate internamente estes problemas e dispare uma exceção do tipo RuntimeException ou derivadas. Você estará aqui expondo detalhes de uma camada inferior sem necessidade alguma, e ainda tornando mais difícil a vida dos usuários da sua API.
Agora, se você quer expor este aspecto do sistema, perfeito: há aqui uma delegação de responsabilidade. O cliente da sua API terá de lidar explicitamente com erros provenientes da camada inferior do sistema.
Uma rápida menção ao Groovy
Groovy é uma linguagem que visa desburocratizar o trabalho do desenvolvedor. Uma das formas que faz isto é através do modo como lidamos com exceções do tipo checked.
Enquanto no Java, código que irá chamar um método que dispara uma exceção obrigatoriamente deve envolver a chamada ao método em um bloco catch ou incluir a declaração da exceção no método que o chamará, em Groovy este não é o caso. Então, código Java similar ao exposto abaixo:
try {
negocio.cadastrar(pessoa);
} catch (Validação ex) {
// trato aqui
}
ou
void executaAlgo() throws Validação {
(...)
negocio.cadastra(pessoa);
(...)
}
Em Groovy eu apenas chamo o método e o incluo em um bloco try... catch ou adiciono uma declaração do tipo throws se eu quiser. Ok, ignoro então as exceptions? Não.
Se você vai projetar uma API, tudo o que disse em relação ao Java também se aplica ao Groovy, pois exceptions nos ajudam a explicitar os limites da mesma.
Concluindo
Meu objetivo neste post foi ir além do uso padrão das exceptions como uma ferramenta que nos possibilita escrever código mais robusto. Como puderam ver, elas também nos ajudam a implementar melhores APIs: com contrato melhor explicitado, mais fáceis de usar e que, consequentemente, acabarão por criar sistemas também mais robustos.
Posted on June 18, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.