As variáveis de erro são um antipadrão ou um bom design?

43

Para lidar com vários erros possíveis que não devem interromper a execução, tenho uma variável error que os clientes podem verificar e usar para lançar exceções. Isso é um anti-padrão? Existe uma maneira melhor de lidar com isso? Para um exemplo disso em ação, você pode ver a API mysqli do PHP. Suponha que problemas de visibilidade (acessores, escopo público e privado, seja a variável em uma classe ou global?) Sejam tratados corretamente.

    
por Trenton Maki 17.06.2014 / 08:22
fonte

11 respostas

61

Se uma linguagem suporta inerentemente exceções, então é preferível lançar exceções e os clientes podem capturar a exceção se não quiser que ela resulte em uma falha. Na verdade, os clientes do seu código esperam exceções e serão executados em muitos bugs porque eles não verificarão os valores de retorno.

Existem algumas vantagens em usar exceções se você tiver uma opção.

Mensagens

As exceções contêm mensagens de erro legíveis pelo usuário, que podem ser usadas pelos desenvolvedores para depuração ou até mesmo exibidas aos usuários, se desejado. Se o código consumidor não puder manipular a exceção, ele sempre poderá registrá-lo para que os desenvolvedores possam percorrer os logs sem precisar parar em todos os outros rastreios para descobrir qual era o valor de retorno e mapeá-lo em uma tabela para descobrir qual era o valor exceção real.

Com valores de retorno, não há informações adicionais que possam ser fornecidas facilmente. Alguns idiomas suportarão a realização de chamadas de método para obter a última mensagem de erro, portanto, essa preocupação é aliviada, mas isso exige que o chamador faça chamadas extras e, às vezes, exigirá acesso a um 'objeto especial' que contenha essas informações.

No caso de mensagens de exceção, forneço o máximo de contexto possível, como:

A policy of name "foo" could not be retrieved for user "bar", which was referenced in user's profile.

Compare isso com um código de retorno -85. Qual você prefere?

Pilha de chamadas

As exceções geralmente também possuem pilhas de chamadas detalhadas, que ajudam a depurar o código de maneira mais rápida e rápida, e também podem ser registradas pelo código de chamada, se desejado. Isso permite que os desenvolvedores identifiquem o problema normalmente na linha exata e, portanto, são muito poderosos. Mais uma vez, compare isso com um arquivo de log com valores de retorno (como -85, 101, 0, etc.), qual você prefere?

Falha na abordagem tendenciosa

Se um método for chamado em algum lugar que falhe, ele lançará uma exceção. O código de chamada deve suprimir a exceção explicitamente ou falhará. Eu achei isso incrível, porque durante o desenvolvimento e teste (e até mesmo na produção) o código falha rapidamente, forçando os desenvolvedores a consertá-lo. No caso de valores de retorno, se uma verificação de um valor de retorno for perdida, o erro é silenciosamente ignorado e as superfícies de erro são inesperadas, geralmente com um custo muito maior para depurar e corrigir.

Embrulhando e desembrulhando exceções

As exceções podem ser agrupadas dentro de outras exceções e depois desdobradas, se necessário. Por exemplo, seu código pode lançar ArgumentNullException , que o código de chamada pode incluir dentro de UnableToRetrievePolicyException , porque essa operação falhou no código de chamada. Embora possa ser mostrada ao usuário uma mensagem semelhante ao exemplo fornecido acima, algum código de diagnóstico pode desembrulhar a exceção e descobrir que um ArgumentNullException causou o problema, o que significa que é um erro de codificação no código do seu consumidor. Isso pode disparar um alerta para que o desenvolvedor possa corrigir o código. Esses cenários avançados não são fáceis de implementar com os valores de retorno.

Simplicidade do código

Este é um pouco mais difícil de explicar, mas eu aprendi através desta codificação tanto com valores de retorno quanto com exceções. O código que foi escrito usando valores de retorno normalmente faria uma chamada e depois teria uma série de verificações sobre qual era o valor de retorno. Em alguns casos, ele chamaria outro método e agora terá outra série de verificações para os valores de retorno desse método. Com exceções, o tratamento de exceções é muito mais simples na maioria, senão em todos os casos. Você tem um bloco try / catch / finally, com o runtime tentando o melhor para executar o código nos blocos finally para limpeza. Até mesmo os blocos try / catch / finally aninhados são relativamente mais fáceis de seguir e manter do que os valores de retorno if / else e associados de vários métodos.

Conclusão

Se a plataforma que você está usando suportar exceções (esp. como Java ou .NET), então você não deve definitivamente assumir que não há outra maneira a não ser lançar exceções porque essas plataformas possuem diretrizes para lançar exceções e seus clientes vão esperar que sim. Se eu estivesse usando sua biblioteca, não me preocuparei em verificar os valores de retorno porque espero que exceções sejam lançadas, é assim que o mundo nessas plataformas é.

No entanto, se fosse C ++, seria um pouco mais difícil determinar porque existe uma grande base de código com códigos de retorno e um grande número de desenvolvedores está ajustado para retornar valores em oposição a exceções (por exemplo, o Windows é abundante) com HRESULTs). Além disso, em muitas aplicações, também pode ser um problema de desempenho (ou pelo menos percebido como).

    
por 17.06.2014 / 09:20
fonte
20

As variáveis de erro são um relicto de linguagens como C, em que as exceções não estavam disponíveis. Hoje, você deve evitá-los, exceto se estiver escrevendo uma biblioteca potencialmente usada em um programa em C (ou em um idioma semelhante sem tratamento de exceção).

Claro, se você tem um tipo de erro que poderia ser melhor classificado como "aviso" (= sua biblioteca pode entregar um resultado válido e o chamador pode ignorar o aviso se ele achar que não é importante), então um status Um indicador em forma de variável pode fazer sentido mesmo em idiomas com exceções. Mas cuidado, os chamadores da biblioteca tendem a ignorar tais avisos mesmo que não devam, então pense duas vezes antes de introduzir tal construção em seu lib.

    
por 17.06.2014 / 09:45
fonte
16

Existem várias maneiras de sinalizar um erro:

O problema da variável de erro é que é fácil esquecer de verificar.

O problema das exceções é que cria caminhos ocultos de execuções e, embora try / catch seja fácil de gravar, é realmente difícil obter uma recuperação adequada na cláusula catch (sem suporte de sistemas de tipos / compiladores). / p>

O problema dos manipuladores de condição é que eles não compõem bem: se você tem execução dinâmica de código (funções virtuais), é impossível prever quais condições devem ser manipuladas. Além disso, se a mesma condição pode ser aumentada em vários pontos, não há como dizer que uma solução uniforme pode ser aplicada a cada vez ... rapidamente se torna confusa.

Retornos polimórficos ( Either a b em Haskell) são a minha solução favorita até agora:

  • explícito: nenhum caminho oculto de execução
  • explícito: totalmente documentado na assinatura do tipo de função (sem surpresa)
  • difícil de ignorar: você precisa combinar o padrão para obter o resultado desejado e lidar com o caso de erro

O único problema é que eles podem potencialmente levar a uma verificação excessiva; as linguagens que os utilizam possuem expressões idiomáticas para encadear as chamadas de funções que as utilizam, mas ainda pode exigir um pouco mais de digitação / desordem. Em Haskell isso seria monads , mas isso é muito mais assustador do que parece, veja < href="http://fsharpforfunandprofit.com/posts/recipe-part2/"> Programação Orientada Ferroviária .

    
por 18.06.2014 / 10:32
fonte
12

Eu acho horrível. Atualmente estou refatorando um aplicativo Java que usa valores de retorno em vez de exceções. Embora você possa não estar trabalhando com Java, acho que isso se aplica mesmo assim.

Você acaba com um código assim:

String result = x.doActionA();
if (result != null) {
  throw new Exception(result);
}
result = x.doActionB();
if (result != null) {
  throw new Exception(result);
}

Ou isto:

if (!x.doActionA()) {
  throw new Exception(x.getError());
}
if (!x.doActionB()) {
  throw new Exception(x.getError());
}

Eu prefiro que as ações lançem exceções, então você acaba com algo como:

x.doActionA();
x.doActionB();

Você pode envolvê-lo em um try-catch e obter a mensagem da exceção, ou pode optar por ignorar a exceção, por exemplo, ao excluir algo que já pode ter sido eliminado. Ele também preserva seu rastreamento de pilha, se você tiver um. Os métodos em si se tornam mais fáceis também. Em vez de lidar com as próprias exceções, elas simplesmente lançam o que deu errado.

Código atual (horrível):

private String doActionA() {
  try {
    someOperationThatCanGoWrong1();
    someOperationThatCanGoWrong2();
    someOperationThatCanGoWrong3();
    return null;
  } catch(Exception e) {
    return "Something went wrong!";
  }
}

Novo e melhorado:

private void doActionA() throws Exception {
  someOperationThatCanGoWrong1();
  someOperationThatCanGoWrong2();
  someOperationThatCanGoWrong3();
}

O rastreio de Strack é preservado e a mensagem está disponível na exceção, em vez do inútil "Algo correu mal!".

Você pode, claro, fornecer mensagens de erro melhores e você deve. Mas este post está aqui porque o código atual em que estou trabalhando é uma dor, e você não deveria fazer o mesmo.

    
por 17.06.2014 / 08:53
fonte
5

"Para lidar com vários possíveis erros, isso não deve interromper a execução",

Se você quer dizer que os erros não devem interromper a execução da função atual, mas devem ser reportados ao chamador de alguma forma - então você tem algumas opções que não foram realmente mencionadas. Este caso é realmente mais um aviso do que um erro. Atirar / Retornar não é uma opção, pois isso encerra a função atual. Um único parâmetro ou retorno de mensagem de erro permite que, no máximo, um desses erros ocorra.

Dois padrões que usei são:

  • Uma coleção de erros / avisos, transmitida ou mantida como uma variável de membro. Que você acrescenta coisas e apenas continua processando. Eu pessoalmente não gosto muito dessa abordagem, pois sinto que ela enfraquece o chamador.

  • Transmita um objeto manipulador de erros / avisos (ou configure-o como uma variável de membro). E cada erro chama uma função de membro do manipulador. Desta forma, o chamador pode decidir o que fazer com esses erros não terminados.

O que você passa para essas coleções / manipuladores deve conter contexto suficiente para o erro ser manipulado "corretamente" - Uma string geralmente é pequena demais, passando-a em alguma instância de Exception é geralmente sensível - mas às vezes desaprovada (como abuso de exceções).

O código típico que usa um manipulador de erros pode se parecer com isso

class MyFunClass {
  public interface ErrorHandler {
     void onError(Exception e);
     void onWarning(Exception e);
  }

  ErrorHandler eh;

  public void canFail(int i) {
     if(i==0) {
        if(eh!=null) eh.onWarning(new Exception("canFail shouldn't be called with i=0"));
     }
     if(i==1) {
        if(eh!=null) eh.onError(new Exception("canFail called with i=1 is fatal");
        throw new RuntimeException("canFail called with i=2");
     }
     if(i==2) {
        if(eh!=null) eh.onError(new Exception("canFail called with i=2 is an error, but not fatal"));
     }
  }
}
    
por 18.06.2014 / 03:08
fonte
5

Frequentemente, não há nada de errado em usar esse padrão ou esse padrão, contanto que você use o padrão que todos os outros usam. No desenvolvimento de Objective-C , o padrão mais preferido é passar um ponteiro onde o método chamado pode depositar um Objeto NSError. Exceções são reservadas para erros de programação e levam a uma falha (a menos que você tenha programadores Java ou .NET gravando seu primeiro aplicativo para iPhone ). E isso funciona muito bem.

    
por 17.06.2014 / 18:10
fonte
4

A pergunta já foi respondida, mas não consigo evitar.

Você não pode esperar que o Exception forneça uma solução para todos os casos de uso. Hammer anyone?

Existem casos em que as exceções não são o fim todas e são todas, por exemplo, se um método recebe uma solicitação e é responsável por validar todos os campos passados, e não apenas o primeiro, então você deve pensar que deveria ser possível indicar a causa do erro para mais de um campo. Deve ser possível também indicar se a natureza da validação impede que o usuário vá mais longe ou não. Um exemplo disso seria uma senha não strong. Você poderia mostrar uma mensagem para o usuário indicando que a senha digitada não é muito strong, mas que é strong o suficiente.

Você poderia argumentar que todas essas validações poderiam ser lançadas como uma exceção no final do módulo de validação, mas elas seriam códigos de erro em qualquer coisa, menos no nome.

Portanto, a lição aqui é: as exceções têm seus lugares, assim como os códigos de erro. Escolha sabiamente.

    
por 18.06.2014 / 09:07
fonte
4

Existem casos de uso em que os códigos de erro são preferíveis às exceções.

Se o seu código puder continuar apesar do erro, mas precisar de relatórios, uma exceção será uma opção ruim, pois as exceções terminam o fluxo. Por exemplo, se você estiver lendo um arquivo de dados e descobrir que ele contém alguns dados inválidos que não sejam de terminal, talvez seja melhor ler o restante do arquivo e relatar o erro, em vez de falhar completamente.

As outras respostas cobriram porque as exceções devem ser preferidas aos códigos de erro em geral.

    
por 18.06.2014 / 13:47
fonte
2

Não há nada de errado em não usar exceções quando as exceções não são adequadas.

Quando a execução do código não deve ser interrompida (por exemplo, atuando na entrada do usuário que pode conter vários erros, como um programa para compilar ou um formulário para processar), acho que coletar erros em variáveis de erro como has_errors e error_messages é de fato um design muito mais elegante do que lançar uma exceção no primeiro erro. Permite encontrar todos os erros na entrada do usuário sem forçar o usuário a reenviar desnecessariamente.

    
por 07.07.2014 / 20:39
fonte
1

Em algumas linguagens de programação dinâmica, você pode usar os valores de erro e o tratamento de exceções . Isso é feito retornando o objeto unthrown exception no lugar do valor de retorno ordinário, que pode ser verificado como um valor de erro, mas gera uma exceção se não estiver marcada.

Em Perl 6 , isso é feito por meio de fail , o que, se o no fatal; scope retornar uma exceção especial Failure , sem exceção.

Em Perl 5 você pode usar Contextual :: Return você pode fazer isso com return FAIL .

    
por 17.06.2014 / 15:32
fonte
-1

A menos que haja algo muito específico, acho que ter uma variável de erro para validação é uma má ideia. O propósito parece ser economizar o tempo gasto na validação (você pode simplesmente retornar o valor da variável)

Mas, se você alterar alguma coisa, terá que recomputar esse valor de qualquer maneira. Eu não posso dizer mais sobre parar e jogar Exception embora.

EDIT: Eu não sabia que isso é uma questão de paradigma de software, em vez de um caso específico.

Deixe-me esclarecer meus pontos em um caso particular em que minha resposta faria sentido

  1. Eu tenho uma coleção de objetos de entidade
  2. Tenho serviços da Web de estilo procedural que funcionam com esses objetos de entidade

Existem dois tipos de erros:

  1. Os erros durante o processamento ocorrem na camada de serviço
  2. Os erros porque existem inconsistências nos objetos de entidade

Na camada de serviço, não há escolha a não ser usar o objeto Result como wrapper, que é a equivalência da variável de erro. Simular uma exceção via chamada de serviço no protocolo como http é possível, mas definitivamente não é uma boa coisa a se fazer. Eu não estou falando sobre esse tipo de erro e não acho que esse é o tipo de erro que está sendo feito nesta questão.

Eu estava pensando no segundo tipo de erro. E minha resposta é sobre esse segundo tipo de erro. Nos objetos de entidade, existem escolhas para nós, algumas delas são

  • usando uma variável de validação
  • lança uma exceção imediatamente quando um campo é definido incorretamente a partir do setter

Usar a variável de validação é o mesmo que ter um único método de validação para cada objeto de entidade. Em particular, o usuário pode definir o valor de uma maneira que mantenha o setter como puramente setter, sem efeito colateral (isso geralmente é uma boa prática) ou pode-se incorporar validação em cada setter e depois salvar o resultado em uma variável de validação. A vantagem disso seria economizar tempo, o resultado da validação é armazenado em cache na variável de validação para que, quando o usuário chamar validação () várias vezes, não haja necessidade de fazer várias validações.

A melhor coisa a fazer neste caso é usar um único método de validação sem usar qualquer validação para armazenar em cache o erro de validação. Isso ajuda a manter o setter como apenas setter.

    
por 17.06.2014 / 08:34
fonte