Estou ponderando esse problema há um tempo e me vejo continuamente encontrando advertências e contradições, por isso espero que alguém possa produzir uma conclusão para o seguinte:
Favorecer exceções sobre códigos de erro
Pelo que sei, ao trabalhar na indústria há quatro anos, lendo livros e blogs etc., a melhor prática atual para lidar com erros é lançar exceções, em vez de retornar códigos de erro (não necessariamente um código de erro, mas um tipo que representa um erro).
Mas - para mim isso parece contradizer ...
Codificação para interfaces, não implementações
Codificamos para interfaces ou abstrações para reduzir o acoplamento. Não sabemos, ou queremos saber, o tipo específico e a implementação de uma interface. Então, como podemos saber quais exceções devemos procurar capturar? A implementação pode gerar 10 exceções diferentes ou nenhuma. Quando pegamos uma exceção, certamente estamos fazendo suposições sobre a implementação?
A menos que - a interface tenha ...
Especificações de exceção
Algumas linguagens permitem que os desenvolvedores afirmem que certos métodos lançam certas exceções (Java, por exemplo, usa a throws
palavra - chave.) Do ponto de vista do código de chamada, isso parece bom - sabemos explicitamente quais exceções precisamos capturar.
Mas - isso parece sugerir uma ...
Abstração com vazamento
Por que uma interface deve especificar quais exceções podem ser lançadas? E se a implementação não precisar lançar uma exceção ou precisar lançar outras exceções? No nível da interface, não há como saber quais exceções uma implementação pode querer lançar.
Assim...
Concluir
Por que as exceções são preferidas quando parecem (aos meus olhos) contradizer as melhores práticas de software? E, se os códigos de erro são tão ruins (e eu não preciso ser vendido pelos vícios dos códigos de erro), existe outra alternativa? Qual é o estado da arte atual (ou em breve) para tratamento de erros que atende aos requisitos das melhores práticas descritas acima, mas não depende de código de chamada que verifique o valor de retorno dos códigos de erro?
Respostas:
Antes de tudo, discordo desta afirmação:
Nem sempre é esse o caso: por exemplo, dê uma olhada no Objective-C (com a estrutura da Foundation). Lá, o NSError é a maneira preferida de lidar com erros, apesar da existência do que um desenvolvedor Java chamaria de verdadeiras exceções: @try, @catch, @throw, classe NSException etc.
No entanto, é verdade que muitas interfaces vazam suas abstrações com as exceções lançadas. É minha convicção que isso não é culpa do estilo "exceção" da propagação / manipulação de erros. Em geral, acredito que o melhor conselho sobre o tratamento de erros é este:
Lide com o erro / exceção no nível mais baixo possível, período
Eu acho que se alguém seguir essa regra, a quantidade de "vazamento" das abstrações pode ser muito limitada e contida.
Se as exceções lançadas por um método devem fazer parte de sua declaração, acredito que deveriam: fazem parte do contrato definido por esta interface: Este método faz A ou falha com B ou C.
Por exemplo, se uma classe é um Analisador de XML, uma parte de seu design deve indicar que o arquivo XML fornecido está completamente errado. Em Java, você normalmente o faz declarando as exceções que espera encontrar e adicionando-as à
throws
parte da declaração do método. Por outro lado, se um dos algoritmos de análise falhar, não há razão para passar essa exceção acima sem tratamento.Tudo se resume a uma coisa: bom design de interface. Se você projetar sua interface bem o suficiente, nenhuma quantidade de exceções deverá assombrá-lo. Caso contrário, não são apenas as exceções que o incomodariam.
Além disso, acho que os criadores de Java tinham razões de segurança muito fortes para incluir exceções a uma declaração / definição de método.
Uma última coisa: algumas linguagens, Eiffel, por exemplo, têm outros mecanismos para tratamento de erros e simplesmente não incluem recursos de lançamento. Lá, uma 'exceção' de classificação é gerada automaticamente quando uma pós-condição para uma rotina não é satisfeita.
fonte
goto
são muito diferentes. Por exemplo, as exceções sempre vão na mesma direção - na pilha de chamadas. Em segundo lugar, proteger-se de exceções surpresas é exatamente a mesma prática que o DRY - por exemplo, em C ++, se você usa o RAII para garantir a limpeza, ele garante a limpeza em todos os casos, não apenas na exceção, mas também em todo o fluxo de controle normal. Isso é infinitamente mais confiável.try/finally
realiza algo um pouco semelhante. Quando você garante a limpeza adequadamente, não precisa considerar as exceções como um caso especial.Gostaria apenas de observar que exceções e códigos de erro não são a única maneira de lidar com erros e caminhos de código alternativos.
Fora do topo da mente, você pode ter uma abordagem como a adotada por Haskell, na qual os erros podem ser sinalizados por meio de tipos de dados abstratos com vários construtores (pense em enumerações discriminadas ou ponteiros nulos, mas tipicamente seguros e com a possibilidade de adicionar sintaxe). funções sugar ou helper para que o fluxo do código pareça bom).
operationThatMightfail é uma função que retorna um valor envolvido em um Maybe. Funciona como um ponteiro anulável, mas a notação não garante que a coisa toda seja avaliada como nula se algum de a, b ou c falhar. (e o compilador protege você contra uma NullPointerException acidental)
Outra possibilidade é passar um objeto manipulador de erros como um argumento extra para todas as funções que você chama. Esse manipulador de erros possui um método para cada "exceção" possível que pode ser sinalizada pela função para a qual você a passa e pode ser usado por essa função para tratar as exceções onde elas ocorrem, sem necessariamente ter que retroceder a pilha por meio de exceções.
O LISP comum faz isso e torna possível ter suporte sintático (argumentos implícitos) e ter as funções internas seguindo este protocolo.
fonte
Maybe
é.Sim, exceções podem causar abstrações com vazamento. Mas os códigos de erro não são ainda piores a esse respeito?
Uma maneira de lidar com esse problema é fazer com que a interface especifique exatamente quais exceções podem ser lançadas sob quais circunstâncias e declare que as implementações devem mapear seu modelo de exceção interno para essa especificação, capturando, convertendo e lançando exceções, se necessário. Se você deseja uma interface "perfeita", esse é o caminho a seguir.
Na prática, geralmente é suficiente especificar exceções que fazem parte da interface e que um cliente pode querer capturar e fazer alguma coisa. É geralmente entendido que pode haver outras exceções quando erros de baixo nível acontecem ou um erro se manifesta, e com o qual um cliente só pode lidar geralmente mostrando uma mensagem de erro e / ou desligando o aplicativo. Pelo menos a exceção ainda pode conter informações que ajudam a diagnosticar o problema.
De fato, com os códigos de erro, praticamente a mesma coisa acaba acontecendo, apenas de uma maneira mais implícita e com muito mais probabilidade de as informações serem perdidas e o aplicativo acabar em um estado inconsistente.
fonte
getSQLState
(genérico) egetErrorCode
(específico do fornecedor). Agora, se só ele tinha subclasses apropriadas ...Muitas coisas boas aqui, gostaria de acrescentar que todos devemos ter cuidado com o código que usa exceções como parte do fluxo de controle normal. Às vezes, as pessoas entram nessa armadilha onde qualquer coisa que não seja o caso usual se torna uma exceção. Eu até vi uma exceção usada como uma condição de terminação de loop.
Exceções significam que "algo que eu não posso lidar aqui aconteceu, precisa procurar alguém para descobrir o que fazer". Um usuário digitando entrada inválida não é uma exceção (que deve ser tratada localmente pela entrada, solicitando novamente etc.).
Outro caso degenerado de uso de exceção que já vi são pessoas cuja primeira resposta é "lançar uma exceção". Isso quase sempre é feito sem escrever a captura (regra geral: escreva a captura primeiro e depois a instrução throw). Em grandes aplicações, isso se torna problemático quando uma exceção não capturada surge das regiões inferiores e explode o programa.
Não sou anti-exceção, mas eles parecem singletons de alguns anos atrás: usados com muita frequência e de forma inadequada. Eles são perfeitos para o uso pretendido, mas esse caso não é tão amplo quanto alguns pensam.
fonte
Não. As especificações de exceção estão no mesmo bloco que os tipos de retorno e argumento - elas fazem parte da interface. Se você não puder estar em conformidade com essa especificação, não implemente a interface. Se você nunca jogar, tudo bem. Não há nada vazando sobre a especificação de exceções em uma interface.
Os códigos de erro estão além do ruim. Eles são terríveis. Você deve se lembrar de verificar e propagar manualmente, todas as vezes, para todas as chamadas. Isso viola o DRY, para começar, e explode massivamente o seu código de tratamento de erros. Essa repetição é um problema muito maior do que qualquer outro enfrentado por exceções. Você nunca pode ignorar silenciosamente uma exceção, mas as pessoas ignoram silenciosamente os códigos de retorno - definitivamente uma coisa ruim.
fonte
O tratamento de exceção de poço pode ter sua própria implementação de interface. Dependendo do tipo de exceção lançada, execute as etapas desejadas.
A solução para o seu problema de design é ter duas implementações de interface / abstração. Um para a funcionalidade e outro para manipulação de exceções. E, dependendo do tipo de exceção capturada, chame a classe de tipo de exceção apropriada.
A implementação de códigos de erro é uma maneira ortodoxa de lidar com exceções. É como o uso de string vs. construtor de strings.
fonte
As exceções de IM-muito-HO devem ser julgadas caso a caso, porque, ao interromper o fluxo de controle, elas aumentarão a complexidade real e percebida do seu código, em muitos casos desnecessariamente. Deixando de lado a discussão relacionada ao lançamento de exceções dentro de suas funções - o que pode realmente melhorar seu fluxo de controle, se você quiser examinar exceções através dos limites de chamada, considere o seguinte:
Permitir que um chamado interrompa seu fluxo de controle pode não fornecer nenhum benefício real e não pode haver uma maneira significativa de lidar com a exceção. Para um exemplo direto, se alguém estiver implementando o padrão Observável (em um idioma como C # onde você tem eventos em todos os lugares e não é explícito
throws
na definição), não há motivo real para permitir que o Observador interrompa o fluxo de controle se ele travar, e nenhuma maneira significativa de lidar com suas coisas (é claro, um bom vizinho não deve jogar ao observar, mas ninguém é perfeito).A observação acima pode ser estendida a qualquer interface fracamente acoplada (como você apontou); Eu acho que é realmente uma norma que, após aumentar de 3 a 6 quadros de pilha, uma exceção não detectada provavelmente acabe em uma seção de código que:
Considerando o exposto, decorar interfaces com
throws
semântica é apenas um ganho funcional marginal, porque muitos chamadores através de contratos de interface só se importariam se você falhasse, não por quê.Eu diria que isso se torna uma questão de gosto e conveniência: seu foco principal é recuperar graciosamente seu estado no chamador e no chamado após uma "exceção"; portanto, se você tiver muita experiência em mover códigos de erro (próximos de um plano de fundo C), ou se você estiver trabalhando em um ambiente em que as exceções podem se tornar más (C ++), não acredito que jogar coisas ao redor seja tão importante para uma OOP agradável e limpa que você não possa confiar no seu antigo padrões se você estiver desconfortável com isso. Especialmente se levar à quebra do SoC.
De uma perspectiva teórica, acho que uma maneira SoC-kosher de lidar com exceções pode ser derivada diretamente da observação de que na maioria das vezes o chamador direto só se importa com o que você falhou, e não por quê. O chamado, alguém muito próximo acima (2-3 quadros) captura uma versão otimizada, e a exceção real sempre é afundada em um manipulador de erros especializado (mesmo que apenas seja rastreado) - é aqui que a AOP seria útil, porque esses manipuladores provavelmente serão horizontais.
fonte
Ambos devem coexistir.
Retorne o código de erro quando você antecipar determinado comportamento.
Retorne a exceção quando você não antecipou algum comportamento.
Os códigos de erro são normalmente associados a uma única mensagem, quando o tipo de exceção permanece, mas uma mensagem pode variar
A exceção tem um rastreamento de pilha, quando o código de erro não. Não uso códigos de erro para depurar o sistema danificado.
Isso pode ser específico para o JAVA, mas quando declaro minhas interfaces, não especifico quais exceções podem ser lançadas por uma implementação dessa interface, simplesmente não faz sentido.
Isso depende inteiramente de você. Você pode tentar capturar um tipo muito específico de exceção e depois capturar um tipo mais geral
Exception
. Por que não deixar a exceção propagar a pilha e depois lidar com isso? Como alternativa, você pode observar a programação de aspectos em que o tratamento de exceções se torna um aspecto "conectável".Não entendo por que isso é um problema para você. Sim, você pode ter uma implementação que nunca falha ou gera exceções e uma implementação diferente que falha constantemente e gera exceção. Se for esse o caso, não especifique nenhuma exceção na interface e seu problema será resolvido.
Mudaria alguma coisa se, em vez de exceção, sua implementação retornasse um objeto de resultado? Este objeto conteria o resultado da sua ação, juntamente com quaisquer erros / falhas, se houver. Você pode então interrogar esse objeto.
fonte
Na minha experiência, o código que recebe o erro (seja por exceção, código de erro ou qualquer outra coisa) normalmente não se importa com a causa exata do erro - reagiria da mesma maneira a qualquer falha, exceto por um possível relatório do erro (seja um diálogo de erro ou algum tipo de log); e esse relatório seria feito ortogonalmente ao código que chamou o procedimento com falha. Por exemplo, esse código pode passar o erro para outro trecho de código que sabe relatar erros específicos (por exemplo, formatar uma sequência de mensagens), possivelmente anexando algumas informações de contexto.
Obviamente, em alguns casos, é necessário anexar semânticas específicas a erros e reagir de maneira diferente com base em qual erro ocorreu. Esses casos devem ser documentados na especificação da interface. No entanto, a interface ainda pode se reservar o direito de lançar outras exceções sem significado específico.
fonte
Acho que as exceções permitem escrever um código mais estruturado e conciso para relatar e manipular erros: o uso de códigos de erro requer a verificação de valores de retorno após cada chamada e a decisão do que fazer em caso de resultado inesperado.
Por outro lado, concordo que as exceções revelam detalhes de implementação que devem estar ocultos no código que invoca uma interface. Como não é possível saber a priori qual parte do código pode lançar quais exceções (a menos que sejam declaradas na assinatura do método como em Java), usando exceções, estamos introduzindo dependências implícitas muito complexas entre diferentes partes do código, o que é contra o princípio de minimizar dependências.
Resumindo:
fonte
catch Exception
ou mesmoThrowable
ou equivalente).Tendência à direita.
Não pode ser ignorado, deve ser tratado, é completamente transparente. E se você usar o tipo de erro canhoto correto, ele transmitirá todas as mesmas informações que uma exceção java.
Desvantagem? Código com manipulação de erro adequada parece nojento (verdadeiro para todos os mecanismos).
fonte