A maneira moderna de executar o tratamento de erros…

116

Venho ponderando esse problema há algum tempo e me vejo encontrando continuamente ressalvas e contradições, por isso espero que alguém possa concluir o seguinte:

Favorece exceções sobre códigos de erro

Até onde sei, de trabalhar na indústria por quatro anos, ler 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 interfaces ou abstrações para reduzir o acoplamento. Nós 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? A implementação poderia lançar 10 exceções diferentes, ou poderia lançar nenhuma. Quando captamos 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 desenvolvedores afirmem que certos métodos lançam certas exceções (Java, por exemplo, usa a palavra-chave throws .) Do ponto de vista do código de chamada, isso parece bom - sabemos explicitamente quais exceções precisamos capturar.

Mas - isso parece sugerir um ...

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? Não há como, em um nível de interface, saber quais exceções uma implementação pode querer lançar.

Então ...

Para 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 nos vícios dos códigos de erro), existe outra alternativa? Qual é o estado atual da arte do tratamento de erros que atende aos requisitos das melhores práticas, conforme descrito acima, mas não depende de chamar o código para verificar o valor de retorno dos códigos de erro?

    
por RichK 03.05.2012 / 12:35
fonte

11 respostas

31

Antes de mais nada, discordo dessa afirmação:

Favour exceptions over error codes

Este não é sempre o caso: por exemplo, dê uma olhada no Objective-C (com o framework 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, NSException class, etc.

No entanto, é verdade que muitas interfaces vazam suas abstrações com as exceções lançadas. Acredito que isso não seja culpa do estilo "exceção" de 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 mantiver 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 elas devem: elas são parte do contrato definido por essa interface: Esse método faz A ou falha com B ou C.

Por exemplo, se uma classe for um XML Parser, uma parte de seu design deve ser para indicar que o arquivo XML fornecido está simplesmente errado. Em Java, você normalmente faz isso declarando as exceções que espera encontrar e adicionando-as à parte throws 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 do não tratado.

Tudo se resume a uma coisa: Bom design de interface. Se você projetar sua interface bem o suficiente, nenhuma quantidade de exceções deve assombrá-lo. Caso contrário, não são apenas exceções que o incomodariam.

Além disso, acho que os criadores de Java tinham motivos de segurança muito strongs 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 o tratamento de erros e simplesmente não incluem recursos de lançamento. Lá, uma 'exceção' do tipo é automaticamente levantada quando uma pós-condição para uma rotina não é satisfeita.

    
por 03.05.2012 / 13:23
fonte
27

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 tomada por Haskell, onde os erros podem ser sinalizados através de tipos de dados abstratos com múltiplos construtores (pense em enums discriminados, ou ponteiros nulos, mas seguros e com a possibilidade de adicionar açúcar sintático ou funções auxiliares para fazer com que o fluxo de código pareça bom).

func x = do
    a <- operationThatMightFail 10
    b <- operationThatMightFail 20
    c <- operationThatMightFail 30
    return (a + b + c)

operationThatMightfail é uma função que retorna um valor envolvido em um Maybe. Ele funciona como um ponteiro anulável, mas a notação garante que a coisa toda seja avaliada como nula se alguma das opções a, b ou c falhar. (e o compilador protege você de fazer um NullPointerException acidental)

Outra possibilidade é passar um objeto manipulador de erro como um argumento extra para cada função que você chamar. Esse manipulador de erro tem um método para cada possível "exceção" que pode ser sinalizada pela função à qual você a transfere 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. / p>

O Common LISP faz isso e torna viável ter suporte sintático (argumentos implícitos) e ter as funções integradas seguindo esse protocolo.

    
por 03.05.2012 / 17:53
fonte
8

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 declarar que as implementações devem mapear seu modelo de exceção interno para essa especificação, capturando, convertendo e reutilizando exceções se necessário. . Se você quer uma interface "prefeito", esse é o caminho a percorrer.

Na prática, normalmente é suficiente especificar exceções que são logicamente parte da interface e que um cliente pode querer capturar e fazer algo a respeito. É geralmente entendido que pode haver outras exceções quando erros de baixo nível acontecem ou um bug se manifesta, e que um cliente só pode manipular geralmente mostrando uma mensagem de erro e / ou desligando o aplicativo. Pelo menos a exceção ainda pode conter informações que ajudem a diagnosticar o problema.

Na verdade, com códigos de erro, praticamente a mesma coisa acaba acontecendo, apenas de uma forma mais implícita, e com muito mais probabilidade de as informações se perderem e o aplicativo acabar em um estado inconsistente.

    
por 03.05.2012 / 12:50
fonte
5

Muita coisa boa aqui, gostaria apenas 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 "algo que não posso resolver aqui, preciso sair com outra pessoa para descobrir o que fazer." Um usuário que digitar uma entrada inválida não é uma exceção (que deve ser tratada localmente pela entrada perguntando novamente, etc.).

Outro caso degenerativo de uso de exceção que eu vi são pessoas cuja primeira resposta é "lançar uma exceção". Isso quase sempre é feito sem escrever a captura (regra geral: escreva primeiro a captura, 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.

Eu não sou anti-exceções, mas elas parecem ser singletons de alguns anos atrás: usadas 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.

    
por 05.05.2012 / 16:51
fonte
4

Leaky abstraction

Why should an interface specify which exceptions can be thrown? What if the implementation doesn't need to throw an exception, or needs to throw other exceptions? There's no way, at an interface level, to know which exceptions an implementation may want to throw.

Não. As especificações de exceção estão no mesmo intervalo que os tipos de retorno e argumento - elas fazem parte da interface. Se você não puder obedecer a essa especificação, não implemente a interface. Se você nunca jogar, tudo bem. Não há vazamento em especificar exceções em uma interface.

Códigos de erro estão além do esperado. Eles são terríveis Você tem que lembrar manualmente de verificar e propagá-los, a cada vez, para cada chamada. Isso viola DRY, para começar, e massivamente explode seu código de tratamento de erros. Essa repetição é um problema muito maior do que qualquer um enfrentado por exceções. Você nunca pode ignorar silenciosamente uma exceção, mas as pessoas podem ignorar silenciosamente os códigos de retorno - definitivamente uma coisa ruim.

    
por 03.05.2012 / 18:37
fonte
2

Bem, o tratamento de exceções pode ter sua própria implementação de interface. Dependendo do tipo da exceção lançada, execute as etapas desejadas.

A solução para seu problema de design é ter duas implementações de interface / abstração. Um para a funcionalidade e outro para o tratamento 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 do construtor string vs. string.

    
por 02.05.2012 / 14:35
fonte
2

As exceções IM-muito-HO devem ser julgadas caso a caso, pois ao quebrar o fluxo de controle elas aumentarão a complexidade real e percebida de 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 o seu fluxo de controle, se você quiser lançar exceções através de limites de chamadas, considere o seguinte:

Permitir que um candidato interrompa seu fluxo de controle pode não fornecer nenhum benefício real, e pode não haver uma maneira significativa de lidar com a exceção. Para um exemplo direto, se alguém estiver implementando o padrão Observable (em uma linguagem como C # onde você tem eventos em todos os lugares e não há throws explícito na definição), não há razão real para deixar o Observer quebrar seu fluxo de controle se falhas, e nenhuma maneira significativa de lidar com suas coisas (é claro, um bom vizinho não deve jogar quando 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 depois de subir 3-6 quadros de pilha, uma exceção não detectada é provável que acabe em uma seção de código que:

  • é muito abstrato para lidar com a exceção de qualquer maneira significativa, mesmo se a exceção em si for alterada;
  • está executando uma função genérica (não poderia se importar menos com o motivo de sua falha, como uma bomba de mensagens ou a observação);
  • é específico, mas com uma responsabilidade diferente, e realmente não deve se preocupar;

Considerando o acima, 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 que.

Eu diria que, em seguida, torna-se uma questão de gosto e conveniência: seu foco principal é graciosamente recuperar seu estado no chamador e no receptor depois de uma "exceção", portanto, se você tem muita experiência em mover códigos de erro em torno de (vindo de um fundo C), ou se você está trabalhando em um ambiente onde as exceções podem se tornar malignas (C ++), eu não acredito que jogar coisas ao redor é tão importante para uma OOP agradável e limpa que você não pode confiar em seus antigos padrões, se você estiver desconfortável com isso. Especialmente se isso 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 fracasso, e não o porquê. O callee lança, alguém muito próximo acima (2-3 quadros) captura uma versão upcasted, e a exceção real é sempre afundada em um manipulador de erro especializado (mesmo se apenas rastreio) - é onde o AOP seria útil, porque esses manipuladores provavelmente são horizontais.

    
por 03.05.2012 / 13:28
fonte
1

Favour exceptions over error codes

  • Ambos devem coexistir.

  • Retorna o código de erro quando você prevê determinado comportamento.

  • Retorna a exceção quando você não previu algum comportamento.

  • 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. Eu não uso códigos de erro para depurar o sistema corrompido.

Coding to interfaces not implementations

Isso pode ser específico para o JAVA, mas quando eu declaro minhas interfaces eu não especifico quais exceções podem ser lançadas por uma implementação daquela interface, simplesmente não faz sentido.

When we catch an exception surely we're making assumptions about the implementation?

Isso depende inteiramente de você. Você pode tentar pegar um tipo muito específico de exceção e, em seguida, pegar um Exception mais geral. Por que não permitir que a exceção se propague para cima da pilha e, em seguida, lidar com isso? Alternativamente, você pode observar a programação de aspectos em que o tratamento de exceções se torna um aspecto "conectável".

What if the implementation doesn't need to throw an exception, or needs to throw other exceptions?

Eu não entendo porque é um problema para você. Sim, você pode ter uma implementação que nunca falha ou lança exceções e você pode ter uma implementação diferente que falha constantemente e lança uma exceção. Se esse for o caso, não especifique exceções na interface e seu problema será resolvido.

Isso mudaria alguma coisa se, em vez de exceção, sua implementação retornasse um objeto de resultado? Esse objeto conteria o resultado de sua ação junto com quaisquer erros / falhas, se houver. Você pode então interrogar esse objeto.

    
por 04.05.2012 / 00:40
fonte
1

Leaky abstraction

Why should an interface specify which exceptions can be thrown? What if the implementation doesn't need to throw an exception, or needs to throw other exceptions? There's no way, at an interface level, to know which exceptions an implementation may want to throw.

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 preocuparia com a causa exata do erro - ele reagiria da mesma forma a qualquer falha, exceto possíveis relato do erro (seja um diálogo de erro ou algum tipo de log); e esse relatório seria feito ortogonalmente ao código que chamava o procedimento de falha. Por exemplo, esse código poderia passar o erro para outra parte do código que saiba como relatar erros específicos (por exemplo, formatar uma sequência de mensagens), possivelmente anexando algumas informações de contexto.

É claro que, em alguns casos, é necessário para anexar semânticas específicas a erros e reagir de maneira diferente com base no erro que ocorreu. Tais 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.

    
por 04.05.2012 / 02:35
fonte
1

Acho que as exceções permitem escrever um código mais estruturado e conciso para relatar e tratar erros: usar códigos de erro exige a verificação de retorno valores após cada chamada e decidir o que fazer no caso de um 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 pedaço de código pode lançar quais exceções (a menos que elas sejam declaradas na assinatura do método como em Java), usando exceções estamos introduzindo dependências implícitas muito complexas entre partes diferentes do código, que é contra o princípio de minimizar as dependências.

Resumindo:

  • Acho que as exceções permitem um código mais limpo e uma abordagem mais agressiva ao teste e à depuração, pois as exceções não identificadas são muito mais visíveis e difíceis de serem ignoradas do que os códigos de erro (falha em breve).
  • Por outro lado, erros de exceção não detectados que não são descobertos durante o teste podem aparecer em um ambiente de produção na forma de uma falha. Em certas circunstâncias, esse comportamento não é aceitável e, nesse caso, acho que usar códigos de erro é uma abordagem mais robusta.
por 05.05.2012 / 00:23
fonte
-1

Com um ou outro lado direito.

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 tratamento de erros adequado parece repugnante (verdade de todos os mecanismos).

    
por 22.12.2014 / 02:53
fonte