Recentemente reli o livro Código Limpo – Robert Cecil Martin, e um assunto me chamou atenção: – Boas práticas com exceções!
Não sei você leitor, mas quando comecei a programar há alguns anos atrás, tive dificuldade com exceções. Não no âmbito de funcionamento, mas sim em saber quais as melhores práticas, assunto o qual abordarei logo abaixo com base no livro citado acima:
O que é interessante sobre exceções é que elas definem um escopo dentro de sua aplicação. Quando executamos uma instrução no try, assumimos que pode ser cancelada a qualquer momento e, então, devemos continuar no catch de forma que estabilize a aplicação após um comportamento inesperado.
Ok. Até ai tudo numa boa. Mas o que usar? Exceções verificadas (Exception) ou não verificadas (RuntimeException)?
Considere uma hierarquia de N chamadas e digamos que um método de nível mais baixo fosse refatorado para lançar uma exceção verificada, a assinatura do método deverá adicionar uma instrução throws. Ai que está, cada método chamador do nosso método refatorado deverá ser alterado para capturar a nova exceção ou anexar uma nova instrução trows. Neste ponto fica claro a quebra de encapsulamento, pois funções no caminho de um lançamento (throw) devem enxergar os detalhes daquela implementação de nível mais baixo. Podemos ver que uma simples alteração é propagada por todo o sistema e o resultado é uma cascata de alterações.
Exceções verificadas podem ser úteis se você está desenvolvendo uma biblioteca crítica, mas em geral os custos da depência superam as vantagens.
Suas exceções devem fornece o contexto, ou seja, a identificação do erro. Crie mensagens de erro informativa e passe junto à exceção.
Há algumas formas de identificar erros: Pela origem – vieram desse ou daquele componente? – ou pelo Tipo – são falhas de dispositivos, de redes, ou erro de programação? –
ConnectionPort port = new ConnectionPort(); try { port.open(); } catch (DeviceResponseException e) { reportPortError(e); logger.log("Device response exception", e); } catch (ATM1212UnlockedException e) { reportPortError(e); logger.log("Unlock exception", e); } catch (GMXError e) { reportPortError(e); logger.log("Device response exception", e); } finally { ... }
O que fazemos é padrão, registramos o erro e nos certificamos que podemos prosseguir. Há muita duplicação de código e a tarefa é a mesma independente da exceção, logo podemos simplificar nosso código consideravelmente:
LocalPort port = new LocalPort(8081); try { port.open(); } catch (PortDeviceFailure e) { reportError(e) logger.log(e.getMessage(), e); } finally { ... }
Pegamos a API e encapsulamos em uma classe LocalPort que é um wrapper (“empacotador”), que usamos para capturar e traduzir as exceções lançadas por ConnectionPort:
public class LocalPort { private ConnectionPort innerPort; public LocalPort(int portNumber) { this.innerPort = new ConnectionPort(portNumber); } public void open() { try { port.open(); } catch (DeviceResponseException e) { new PortDeviceFalure(e); } catch (ATM1212UnlockedException e) { new PortDeviceFalure(e); } catch (GMXError e) { new PortDeviceFalure(e); } finally { ... } } }
Se seguirmos as dicas passadas acima, acabaremos com um código bem dividido entre tratamento de erro e lógica de negócio, o que ajuda a tornar o algoritimo limpo. Empacote suas APIs, lance suas próprias exceções e defina um controlador capaz de lidar com qualquer processamento abortado. Na grande maioria, esta é uma ótima abordagem, mas há situações nas quais talvez você não queira lidar com as exeções.
E este será assunto para um próximo post relacionado a exceções.