Projete boas APIs com as Exceptions do Java
Como você pode tirar proveito das Exceptions do Java para escrever boas APIs
No último post vimos por que as Exceptions do Java são como são a partir dos objetivos traçados por seus designers. Lá o foco foi no fluxo de execução, aqui trato de outro aspecto que considero importante: o da definição de APIs.
Mas o que é uma API mesmo?
Mais um termo que de tão (mal) usado perdeu o sentido: é importante lembrar então. Curto e grosso, uma API é:
A interface do seu programa que irá expor parte de suas funcionalidades a outros programadores
Repare que não usei a palavra “sistema” mas sim “programa”. Quando você projeta uma classe pública que possuirá métodos públicos, pensados para que outros programadores a usem (sem precisar alterar seu código fonte), você está criando uma API (mesmo que você seja este outro programador).
O que é uma boa API?
É aquela que nos diz exatamente o que será feito. Tudo começa a partir do nome escolhido pelo programador para a funcionalidade exposta, que deverá mostrar de forma evidente a sua intenção.
Mas na essência qualquer mecanismo de integração, por expor uma funcionalidade é, na prática, uma API (pense profundamente sobre isto).
Um bom nome de método ou classe 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. Observe a função abaixo:
int cadastrarPessoa(String cpf, 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, já com todos os atributos ali definidos para que possa ser feita uma validação mais rica e uma implementação mais fácil de ser extendida. Fácil de melhorar então:
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 pergunte: tá claro isto na definição do método?). Se houver um erro, ela poderia simplesmente nos retornar um valor negativo: “-1: sem cpf, -2: sem nome, -3: sem sobrenome” e por aí vai. O valor de retorno terá então duplo sentido: o nã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:
// tratamento quando não tem cpf
break;
case -2:
// nome não foi fornecido
break;
case -48: ... você quer MESMO causar este sofrimento?
}
Sofrimento eterno que poderia ser aliviado incluindo algumas constantes, mas ainda seria um sofrimento (ainda 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. Aqui as exceptions do Java começam a se mostrar interessantes. Como seria uma terceira versão da nossa API?
void cadastrar(Pessoa pessoa) throws Validacao
Não preciso mais retornar um valor inteiro: se a persistência for bem sucedida, o próprio método já vai preencher um atributo “id” do objeto que passei como parâmetro com a chave primária do banco de dados. 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.
try {
servicePessoa.cadastrar(pessoa);
} catch (Validacao erro) {
// aqui faço o tratamento. Talvez retornar um código 400 numa API REST?
// talvez seja possível, com base nos detalhes da exceção, algum
// tratamento que corrija o objeto e tente novamente?
}
void cadastrar(Pessoa pessoa) throws Validacao, JDBCException
O objetivo deste exemplo é meramente didático, e contexto, lembre-se: é tudo!
Antes eu sabia que objetos inválidos não seriam persistidos (é a condição para a execução): agora também sei que uma falha no banco de dados relacional pode ocorrer. Novamente, contexto é tudo.
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.
Há quem use declarar sempre suas classes de exceção como derivadas de RuntimeException para que transações sempre sofram rollback, mas isto é assunto para outro post (fica aqui apenas a dica "a la Fermat")
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.
Nossa API agora diz muito:
- A intenção (persistir um objeto Pessoa) usando uma granularidade apropriada de parâmetros e retorno (o "void" pode ser pensado como retorno)
- As restrições/limites da API a partir do disparo da exceção Validacao.
- O que pode dar errado.
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. Na prática parte desta robustez vêm também do bom uso do código que escrevemos, cuja intenção, restrições e exceções devem estar explícitas na API que escrevemos.