Como fazer tratamentos de erros elegantes

A maioria das aplicações, para evitar dizer todas, dependem de uma série de fatores que podem dar errado. Robert C. Martin, exemplifica, com arquivos não encontrados, banco de dados fora do ar.

Por isso é natural que tenhamos algum fluxo alternativo caso as coisas dêem errado. Lá na epóca do C era comum retornar um código de erro ou sucesso no final da função, fazendo com que as chamadas as funções que prevem certos erros serem sucedidas por ifs/elses para criar o fluxo alternativo. Criando esse tipo de coisa:

int retorno = save(usuario);    if(retorno==0){         print("Usuário salvo com sucesso");     }

Hoje em dia, a maioria das linguagens modernas possibilita o tratamento de erros pela criação de Exceptions evitando assim sujar o código com lógica de negócio e tratamento de erros. Robert C. Martin também cita, que a o escopo try-catch-finally podem ser usados como uma espécie de transação de banco de dados, onde em caso de exceções o catch deve fazer os "rollbacks" necessários para manter a aplicação consistente. Mas para fazer isso é necessário isolar os try-catch em pontos "globais", lançando a exceção para cima ao invés de tratar em cada método.

Além disso, temos o beneficio de centralizarmos o caminho infeliz em um ponto, ao invés de termos que ir em cada classe para entender qual o caminho a ser seguido no caso de falha.

Nos projetos em que trabalhei, a regra para lançar ou não a exceção para cima era sempre definida pela frase "O método foo pode saber como tratar a exceção Bar para qualquer sistema/classe que o venha a chamar?". Um exemplo é o de banco indisponível. Qual o conhecimento da camada de dados para resolver este problema? Se ela é capaz de se conectar a outro banco de dados ou recuperar o mesmo dado de outra forma, faz sentido ela tratar a própria execeção, mas fora isso seria muito estranho se ela tratasse a exceção e retornasse valores padrões para TODAS as aplicações que o chamarem.

No caso do java, pode ser interessante converter as exceções a uma exceção do projeto inves de lançar a Exception de frameworks e libs de terceiros, pois o uso de "checked exceptions" força o programador a importar a Exception da biblioteca para declarar o throws e depois para o catch, isso faz com que seja doloroso trocar de vendor.

Também é necessário evitar os escopos de try-catch no meio dos métodos, métodos são escritos para fazerem uma e somente uma ação, se além de fazer essa ação, ele também trata erros, ele está claramente fazendo duas coisas, não? Por isso Martin defende que o try deve estar na primeira linha do método, demonstrando que a função dele é tratar erros.

Em Clean Code também é citado que não se deve retornar valores nulls para evitar lançar erros. Pois esses objetos nulls passam a ser "retorno de flags de erros" da mesma forma que faziamos no C, além de não facilitar o entendimento do código: usuario = gestaoUsuario.getUsuarioPorNome("Elias Granja"); if(usuario == null){ ... } else

Lançando exceções, além de ficar mais claro que o usuário não foi encontrado, também é possível "bater" os olhos e entender o fluxo dos caminhos felizes e infelizes: try{ usuario = gestaoUsuario.getUsuarioPorNome("Elias Granja"); }catch(UsuarioNaoEncontradoException e){ ... }

Seguindo essas regras definimos de maneira simples os fluxos da aplicação, além de separarmos de forma elegante o algoritmo com as regras de negócio do tratamento de erro.