Projeto por contrato usando afirmações ou exceções?

Ao programar por contrato, uma function ou método primeiro verifica se suas pré-condições são atendidas, antes de começar a trabalhar em suas responsabilidades, certo? As duas formas mais importantes de fazer essas verificações são por assert e por exception .

  1. declaração falha apenas no modo de debugging. Para certificar-se de que é crucial testar (unidade) todas as pré-condições de contrato separadas para ver se elas realmente falharam.
  2. exceção falha no modo de debugging e liberação. Isso tem o benefício de que o comportamento de debugging testado é idêntico ao comportamento de liberação, mas gera uma penalidade de desempenho em tempo de execução.

Qual você acha que é preferível?

Veja a questão reluzida aqui

Desativar a declaração em compilações de lançamento é como dizer “Eu nunca terei problemas em uma criação de versão”, o que geralmente não é o caso. Portanto, assert não deve ser desabilitado em uma versão build. Mas você não quer que o release seja quebrado sempre que ocorrerem erros, não é?

Portanto, use exceções e use-as bem. Use uma hierarquia de exceções sólida e boa e garanta a captura e você pode colocar um gancho no lançamento de exceção em seu depurador para capturá-lo, e no modo de liberação você pode compensar o erro em vez de uma falha direta. É o caminho mais seguro para ir.

A regra geral é que você deve usar asserções quando estiver tentando capturar seus próprios erros e exceções ao tentar detectar os erros de outras pessoas. Em outras palavras, você deve usar exceções para verificar as condições prévias para as funções públicas da API e sempre que receber dados externos ao sistema. Você deve usar afirmações para as funções ou dados que são internos ao seu sistema.

O princípio que sigo é o seguinte: se uma situação pode ser evitada realisticamente por codificação, use uma afirmação. Caso contrário, use uma exceção.

As afirmações são para garantir que o contrato está sendo cumprido. O contrato deve ser justo, para que o cliente esteja em condições de garantir a conformidade. Por exemplo, você pode declarar em um contrato que um URL deve ser válido porque as regras sobre o que é e o que não é um URL válido são conhecidas e consistentes.

As exceções são para situações que estão fora do controle do cliente e do servidor. Uma exceção significa que algo deu errado, e não há nada que possa ter sido feito para evitá-lo. Por exemplo, a conectividade de rede está fora do controle de aplicativos, portanto não há nada que possa ser feito para evitar um erro de rede.

Eu gostaria de acrescentar que a distinção entre Asserção e Exceção não é realmente a melhor maneira de pensar sobre isso. O que você realmente quer pensar é o contrato e como ele pode ser aplicado. No meu exemplo de URL acima, a melhor coisa a fazer é ter uma class que encapsule um URL e seja Nulo ou um URL válido. É a conversão de uma string em um URL que impõe o contrato e uma exceção é lançada se for inválida. Um método com um parâmetro de URL é muito mais claro do que um método com um parâmetro String e uma declaração que especifica um URL.

As afirmações são para capturar algo que um desenvolvedor fez errado (não apenas você mesmo – outro desenvolvedor em sua equipe também). Se for razoável que um erro do usuário possa criar essa condição, isso deve ser uma exceção.

Da mesma forma, pense nas conseqüências. Uma declaração normalmente encerra o aplicativo. Se houver alguma expectativa realista de que a condição possa ser recuperada, provavelmente você deve usar uma exceção.

Por outro lado, se o problema pode ser devido a um erro do programador, então use uma afirmação, porque você quer saber sobre isso o mais rápido possível. Uma exceção pode ser capturada e manipulada, e você nunca descobriria isso. E sim, você deve desabilitar as afirmações no código da release, porque você quer que o aplicativo seja recuperado se houver a menor chance. Mesmo que o estado do seu programa esteja profundamente quebrado, o usuário poderá salvar seu trabalho.

Não é exatamente verdade que “declaração falha apenas no modo de debugging”.

Na construção de software orientada a object, 2ª edição por Bertrand Meyer, o autor deixa uma porta aberta para verificar pré-condições no modo de liberação. Nesse caso, o que acontece quando uma declaração falha é que … uma exceção de violação de asserção é levantada! Nesse caso, não há recuperação da situação: algo útil pode ser feito, e é para gerar automaticamente um relatório de erros e, em alguns casos, para reiniciar o aplicativo.

A motivação por trás disso é que pré-condições são tipicamente mais baratas para testes do que invariantes e pós-condições, e que, em alguns casos, correção e “segurança” na compilation do release são mais importantes que a velocidade. Por exemplo, para muitas aplicações a velocidade não é um problema, mas a robustez (a capacidade do programa de se comportar de maneira segura quando o seu comportamento não está correto, ou seja, quando um contrato é quebrado) é.

Você deve sempre deixar as verificações de pré-condição ativadas? Depende. Você decide. Não há resposta universal. Se você está fazendo software para um banco, talvez seja melhor interromper a execução com uma mensagem alarmante do que transferir US $ 1.000.000 em vez de US $ 1.000. Mas e se você estiver programando um jogo? Talvez você precise de toda velocidade possível, e se alguém obtiver 1000 pontos ao invés de 10 por causa de um bug que as condições prévias não pegaram (porque elas não estão habilitadas), azar.

Em ambos os casos, você deve idealmente ter detectado esse bug durante o teste, e você deve fazer uma parte significativa do seu teste com as asserções ativadas. O que está sendo discutido aqui é qual é a melhor política para os casos raros em que as pré-condições falham no código de produção em um cenário que não foi detectado anteriormente devido a testes incompletos.

Para resumir, você pode ter afirmações e ainda obter as exceções automaticamente , se você deixá-las ativadas – pelo menos em Eiffel. Eu acho que para fazer o mesmo em C ++ você precisa digitar você mesmo.

Veja também: Quando as afirmações devem permanecer no código de produção?

Houve um enorme tópico sobre a habilitação / desabilitação de asserções em versões de compilation em comp.lang.c ++. Moderated, que se você tiver algumas semanas, você pode ver o quão variadas são as opiniões sobre isso. 🙂

Ao contrário do coppro , acredito que, se você não tiver certeza de que uma afirmação pode ser desabilitada em uma compilation de release, ela não deveria ter sido uma afirmação. As asserções são para proteger contra invariantes de programas que estão sendo quebrados. Nesse caso, no que diz respeito ao cliente do seu código, haverá um dos dois resultados possíveis:

  1. Morra com algum tipo de falha do tipo OS, resultando em uma chamada para abortar. (Sem afirmar)
  2. Morrer através de uma chamada direta para abortar. (Com assert)

Não há diferença para o usuário, no entanto, é possível que as asserções adicionem um custo de desempenho desnecessário no código que está presente na grande maioria das execuções em que o código não falha.

A resposta para a pergunta, na verdade, depende muito mais de quem serão os clientes da API. Se você estiver escrevendo uma biblioteca fornecendo uma API, precisará de algum tipo de mecanismo para notificar seus clientes de que eles usaram a API incorretamente. A menos que você forneça duas versões da biblioteca (uma com afirmações, outra sem), então assert é muito improvável a escolha apropriada.

Pessoalmente, no entanto, não tenho certeza se gostaria de ir com exceções para este caso também. Exceções são mais adequadas para onde uma forma adequada de recuperação pode ocorrer. Por exemplo, pode ser que você esteja tentando alocar memory. Quando você pega uma exceção ‘std :: bad_alloc’, pode ser possível liberar memory e tentar novamente.

Eu delineei minha opinião sobre o estado da questão aqui: Como você valida o estado interno de um object? . Geralmente, declare suas reivindicações e jogue por violação de outras pessoas. Para desabilitar as declarações em versões de lançamento, você pode fazer:

  • Desativar afirmações para verificações caras (como verificar se um intervalo está ordenado)
  • Manter as verificações triviais habilitadas (como a verificação de um ponteiro nulo ou um valor booleano)

Naturalmente, em compilações de versões, as declarações com falha e as exceções não identificadas devem ser tratadas de outra maneira que nas compilações de debugging (onde ele poderia apenas chamar std :: abort). Escreva um log do erro em algum lugar (possivelmente em um arquivo), informe ao cliente que ocorreu um erro interno. O cliente poderá enviar o arquivo de log.

você está perguntando sobre a diferença entre erros de tempo de design e de tempo de execução.

Afirmações são ‘Ei programador, isso está quebrado’ notifications, eles estão lá para lembrá-lo de erros que você não teria notado quando eles aconteceram.

exceções são notifications de ‘hey usuário, somethings gone wrong’ (obviamente você pode codificar para capturá-las para que o usuário nunca seja informado), mas elas são projetadas para ocorrer em tempo de execução quando o usuário Joe está usando o aplicativo.

Então, se você acha que pode tirar todos os seus bugs, use apenas exceções. Se você acha que não pode … usar exceções. Você ainda pode usar declarações de debugging para diminuir o número de exceções, é claro.

Não esqueça que muitas das pré-condições serão dados fornecidos pelo usuário, então você precisará de uma boa maneira de informar ao usuário que seus dados não são bons. Para fazer isso, muitas vezes você precisará retornar os dados de erro na pilha de chamadas para os bits com os quais ele está interagindo. As afirmações não serão úteis, então, se seu aplicativo for n-tier.

Por último, eu usaria nenhum código de erro é muito superior para erros que você acha que irá ocorrer regularmente. 🙂

Eu prefiro o segundo. Enquanto seus testes podem ter corrido bem, Murphy diz que algo inesperado vai dar errado. Assim, em vez de obter uma exceção na chamada de método errônea, você acaba descobrindo um quadro de pilha NullPointerException (ou equivalente) 10 mais profundo.

As respostas anteriores estão corretas: use exceções para funções públicas da API. A única vez que você pode querer dobrar esta regra é quando o cheque é computacionalmente caro. Nesse caso, você pode colocá-lo em uma declaração.

Se você acha que a violação dessa pré-condição é provável, mantenha-a como uma exceção ou refaça a pré-condição.

Você deveria usar ambos. As afirmações são para sua conveniência como desenvolvedor. Exceções capturam coisas que você perdeu ou não esperava durante o tempo de execução.

Eu gostei das funções de relatório de erros do glib em vez de simples afirmações antigas. Eles se comportam como assert declarações, mas em vez de parar o programa, eles apenas retornam um valor e deixam o programa continuar. Funciona surpreendentemente bem, e como bônus você consegue ver o que acontece com o resto do seu programa quando uma function não retorna “o que é suposto”. Se ele falhar, você sabe que a sua verificação de erros é frouxa em algum outro lugar na estrada.

No meu último projeto, usei esse estilo de funções para implementar a verificação de pré-condição e, se uma delas falhasse, eu imprimiria um rastreamento de pilha no arquivo de log, mas continuaria em execução. Economizei muito tempo de debugging quando outras pessoas encontravam um problema ao executar minha compilation de debugging.

 #ifdef DEBUG #define RETURN_IF_FAIL(expr) do { \ if (!(expr)) \ { \ fprintf(stderr, \ "file %s: line %d (%s): precondition `%s' failed.", \ __FILE__, \ __LINE__, \ __PRETTY_FUNCTION__, \ #expr); \ ::print_stack_trace(2); \ return; \ }; } while(0) #define RETURN_VAL_IF_FAIL(expr, val) do { \ if (!(expr)) \ { \ fprintf(stderr, \ "file %s: line %d (%s): precondition `%s' failed.", \ __FILE__, \ __LINE__, \ __PRETTY_FUNCTION__, \ #expr); \ ::print_stack_trace(2); \ return val; \ }; } while(0) #else #define RETURN_IF_FAIL(expr) #define RETURN_VAL_IF_FAIL(expr, val) #endif 

Se eu precisasse de verificação de argumentos em tempo de execução, faria o seguinte:

 char *doSomething(char *ptr) { RETURN_VAL_IF_FAIL(ptr != NULL, NULL); // same as assert(ptr != NULL), but returns NULL if it fails. // Goes away when debug off. if( ptr != NULL ) { ... } return ptr; } 

Eu tentei sintetizar várias das outras respostas aqui com meus próprios pontos de vista.

Use asserções para casos em que você deseja desabilitá-lo na produção, errando para deixá-los dentro. A única razão real para desativar na produção, mas não no desenvolvimento, é acelerar o programa. Na maioria dos casos, essa aceleração não será significativa, mas às vezes o código é crítico em termos de tempo ou o teste é computacionalmente caro. Se o código é de missão crítica, as exceções podem ser melhores, apesar da lentidão.

Se houver alguma chance real de recuperação, use uma exceção, pois as asserções não foram criadas para serem recuperadas. Por exemplo, o código raramente é projetado para recuperar-se de erros de programação, mas é projetado para se recuperar de fatores como falhas de rede ou arquivos bloqueados. Erros não devem ser tratados como exceções simplesmente por estar fora do controle do programador. Em vez disso, a previsibilidade desses erros, comparada aos erros de codificação, os torna mais fáceis de recuperar.

Re argumenta que é mais fácil depurar asserções: O rastreamento de pilha de uma exceção nomeada corretamente é tão fácil de ler quanto uma asserção. Um bom código só deve capturar tipos específicos de exceções, portanto as exceções não devem passar despercebidas devido à captura. No entanto, acho que o Java às vezes obriga você a pegar todas as exceções.

Veja também esta pergunta :

Em alguns casos, as afirmações são desativadas ao criar um release. Você pode não ter controle sobre isso (caso contrário, você poderia construir com as instruções), então pode ser uma boa ideia fazer isso dessa maneira.

O problema de “corrigir” os valores de input é que o chamador não obterá o que eles esperam, e isso pode levar a problemas ou até mesmo a falhas em partes totalmente diferentes do programa, tornando a debugging um pesadelo.

Eu costumo jogar uma exceção na declaração if para assumir o papel da declaração no caso de serem desativados

 assert(value>0); if(value< =0) throw new ArgumentOutOfRangeException("value"); //do stuff 

A regra geral, para mim, é que o uso afirma expressões para encontrar erros internos e exceções para erros externos. Você pode se beneficiar muito da seguinte discussão feita por Greg a partir daqui .

As expressões de declaração são usadas para encontrar erros de programação: erros na própria lógica do programa ou em erros em sua implementação correspondente. Uma condição de declaração verifica se o programa permanece em um estado definido. Um “estado definido” é basicamente aquele que concorda com as suposições do programa. Note que um “estado definido” para um programa não precisa ser um “estado ideal” ou mesmo “um estado usual”, ou mesmo um “estado útil”, mas mais sobre esse ponto importante depois.

Para entender como as asserções se encheckboxm em um programa, considere uma rotina em um programa C ++ que está prestes a desreferenciar um ponteiro. Agora a rotina deve testar se o ponteiro é NULL antes da desreferência, ou deve afirmar que o ponteiro não é NULL e, em seguida, ir em frente e desreferê-lo, independentemente disso?

Eu imagino que a maioria dos desenvolvedores gostaria de fazer as duas coisas, adicionar a afirmação, mas também verificar o ponteiro para um valor NULL, a fim de não travar caso a condição declarada falhe. Na superfície, a execução do teste e do teste pode parecer a decisão mais sábia

Diferentemente de suas condições declaradas, o tratamento de erros de um programa (exceções) refere-se não a erros no programa, mas a inputs que o programa obtém de seu ambiente. Estes são muitas vezes “erros” por parte de alguém, como um usuário tentando fazer login em uma conta sem digitar uma senha. E, embora o erro possa impedir a conclusão bem-sucedida da tarefa do programa, não há falha no programa. O programa não consegue acessar o usuário sem uma senha devido a um erro externo – um erro da parte do usuário. Se as circunstâncias fossem diferentes, e o usuário digitasse a senha correta e o programa não conseguisse reconhecê-la; então, embora o resultado ainda fosse o mesmo, a falha passaria a pertencer ao programa.

A finalidade do tratamento de erros (exceções) é dupla. A primeira é comunicar ao usuário (ou algum outro cliente) que um erro na input do programa foi detectado e o que isso significa. O segundo objective é restaurar o aplicativo depois que o erro for detectado, para um estado bem definido. Note que o programa em si não está em erro nesta situação. Concedido, o programa pode estar em um estado não ideal, ou até mesmo um estado em que não pode fazer nada útil, mas não há erro de programação. Pelo contrário, uma vez que o estado de recuperação de erro é um antecipado pelo design do programa, ele é aquele que o programa pode manipular.

PS: você pode querer verificar a pergunta semelhante: Exceção Vs Assertion .