Projetar por contrato usando asserções ou exceções? [fechadas]

123

Ao programar por contrato, uma função ou método primeiro verifica se suas condições prévias foram cumpridas, antes de começar a trabalhar em suas responsabilidades, certo? As duas formas mais proeminentes para fazer essas verificações são, por asserte por exception.

  1. afirma falhar apenas no modo de depuração. Para garantir que é crucial (unidade) testar todas as condições prévias do contrato para verificar se elas realmente falham.
  2. exceção falha no modo de depuração e liberação. Isso tem o benefício de que o comportamento de depuração testado é idêntico ao do release, mas incorre em uma penalidade no desempenho do tempo de execução.

Qual você acha que é preferível?

Veja a pergunta repetida aqui

andreas buykx
fonte
3
O ponto principal do design por contrato é que você não precisa (e provavelmente não deve) verificar as pré-condições em tempo de execução. Você verifica a entrada antes de passá-la para o método com as pré-condições, é assim que você respeita o final do contrato. Se a entrada for inválida ou violar seu término do contrato, o programa geralmente falhará de qualquer maneira através de seu curso normal de ações (o que você deseja).
precisa saber é o seguinte
Boa pergunta, mas acho que você realmente deve mudar a resposta aceita (como mostram os votos também)!
DaveFar
Para sempre, eu sei, mas essa pergunta deveria realmente ter a tag c ++? Eu estava procurando por esta resposta, para usar em outro idioma (Delpih), e não consigo imaginar nenhum idioma que contenha exceções e asserções que não sigam as mesmas regras. (Ainda aprendendo diretrizes estouro de pilha.)
Eric G
Resposta muito sucinta dada nesta resposta : "Em outras palavras, as exceções tratam da robustez do seu aplicativo enquanto as afirmações tratam da sua exatidão."
Shmuel Levine

Respostas:

39

Desabilitar a declaração nas compilações de versão é como dizer "Nunca terei nenhum problema em uma compilação de versão", o que geralmente não é o caso. Portanto, a afirmação não deve ser desativada em uma compilação de versão. Mas você não quer que a versão seja travada sempre que ocorrerem erros, não é?

Portanto, use exceções e use-as bem. Use uma hierarquia boa e sólida de exceções e garanta que você captura e pode colocar um gancho na exceção lançada no depurador para capturá-la. No modo de liberação, você pode compensar o erro e não uma falha direta. É o caminho mais seguro a seguir.

coppro
fonte
4
As asserções são úteis, no mínimo, nos casos em que a verificação da correção seria ineficiente ou ineficiente para implementar corretamente.
Casebash 25/10/09
89
O objetivo das afirmações não é corrigir erros, mas alertar o programador. Mantê-los ativados nas versões de lançamento é inútil por esse motivo: O que você teria ganho com uma declaração de demissão? O desenvolvedor não poderá entrar e depurar. As asserções são um auxiliar de depuração, não substituem exceções (e nem as exceções substituem as asserções). Exceções alertam o programa para uma condição de erro. A declaração alerta o desenvolvedor.
jalf
12
Mas uma asserção deve ser usada quando os dados internos tiverem sido corrompidos após a correção - se uma asserção disparar, você não poderá fazer suposições sobre o estado do programa, porque isso significa que algo está / errado /. Se uma afirmação foi disparada, você não pode assumir que nenhum dado é válido. É por isso que uma compilação de versão deve afirmar - não para informar ao programador onde está o problema, mas para que o programa possa desligar e não arriscar problemas maiores. O programa deve fazer o possível para facilitar a recuperação posteriormente, quando os dados puderem ser confiáveis.
coppro
5
@ jalf, Embora você não possa colocar um gancho no depurador nas compilações de versão, é possível aproveitar o log para que os desenvolvedores vejam as informações relevantes para a sua declaração. Neste documento ( martinfowler.com/ieeeSoftware/failFast.pdf ), Jim Shore ressalta: "Lembre-se de que um erro que ocorre no site do cliente passou por seu processo de teste. Você provavelmente terá problemas para reproduzi-lo. Esses erros são o mais difícil de encontrar e uma afirmação bem colocada explicando o problema pode economizar dias de esforço ".
precisa
5
Pessoalmente, prefiro declarações de design por meio de contratos. As exceções são defensivas e estão verificando o argumento dentro da função. Além disso, as condições prévias do dbc não dizem "Não funcionarei se você usar valores fora do intervalo de trabalho", mas "Não garantirei fornecer a resposta certa, mas ainda o posso fazer". As declarações fornecem ao desenvolvedor o feedback de que eles estão chamando uma função com uma violação de condição, mas não os impeçam de usá-la se sentirem que sabem melhor. A violação pode causar exceções, mas vejo isso como uma coisa diferente.
22611 Matt_JD
194

A regra geral é que você deve usar asserções ao tentar capturar seus próprios erros e exceções ao tentar capturar os erros de outras pessoas. Em outras palavras, você deve usar exceções para verificar as pré-condições para as funções públicas da API e sempre que obtiver dados externos ao seu sistema. Você deve usar declarações para as funções ou dados internos ao seu sistema.

Dima
fonte
que tal serializar / desserializar sentado em diferentes módulos / aplicativos e eventualmente sair de sincronia? Quero dizer, por parte do leitor, sempre é meu erro tentar ler as coisas da maneira errada, por isso costumo usar declarações, mas por outro lado tenho dados externos, que podem mudar de formato sem aviso prévio.
Slava
Se os dados forem externos, você deverá usar exceções. Nesse caso em particular, você provavelmente também deve capturar essas exceções e tratá-las de uma maneira razoável, em vez de apenas deixar seu programa morrer. Além disso, minha resposta é uma regra de ouro, não uma lei da natureza. :) Então você deve considerar cada caso individualmente.
Dima
Se a sua função f (int * x) contiver uma linha x-> len, então f (v) onde v é comprovadamente nulo é garantido que ele trava. Além disso, se mesmo antes em v for nulo e for comprovado que f (v) seja chamado, você terá uma contradição lógica. É o mesmo que ter a / b em que b é finalmente provado ser 0. Idealmente, esse código deve falhar na compilação. Desativar verificações de suposições é completamente estúpido, a menos que o problema seja o custo das verificações, porque obscurece o local em que uma suposição foi violada. Ele deve pelo menos ser registrado. Você deve ter um design de reiniciar em caso de falha.
Rob
22

O princípio que sigo é o seguinte: Se uma situação puder ser realisticamente evitada pela codificação, use uma asserção. Caso contrário, use uma exceção.

As afirmações são para garantir que o Contrato está sendo respeitado. O contrato deve ser justo, para que o cliente esteja em posição 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.

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 de afirmação / 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 cumprido. No meu exemplo de URL acima, a melhor coisa a fazer é ter uma classe que encapsule uma URL e seja Nula ou uma URL válida. É 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 que um método com um parâmetro String e uma asserção que especifica um URL.

Ged Byrne
fonte
6

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

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

Por outro lado, se o problema puder ser causado apenas por um erro do programador, use uma declaração, pois você deseja saber sobre isso o mais rápido possível. Uma exceção pode ser detectada e tratada, e você nunca descobriria isso. E sim, você deve desativar as declarações no código de lançamento, porque deseja que o aplicativo se recupere se houver a menor chance possível. Mesmo se o estado do seu programa estiver profundamente quebrado, o usuário poderá salvar seu trabalho.

DJClayworth
fonte
5

Não é exatamente verdade que "a declaração falha apenas no modo de depuração".

Em Object Oriented Software Construction, 2ª Edição, de Bertrand Meyer, o autor deixa uma porta aberta para verificar as pré-condições no modo de liberação. Nesse caso, o que acontece quando uma asserção falha é que ... uma exceção de violação de asserção é criada! Nesse caso, não há recuperação da situação: algo útil pode ser feito e é gerar automaticamente um relatório de erros e, em alguns casos, reiniciar o aplicativo.

A motivação por trás disso é que as pré-condições são geralmente mais baratas de testar do que invariantes e pós-condições, e que, em alguns casos, a correção e a "segurança" na criação da versão são mais importantes que a velocidade. Para muitas aplicações, a velocidade não é um problema, mas a robustez (a capacidade do programa de se comportar de maneira segura quando seu comportamento não é 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ê estiver criando 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 a velocidade possível e, se alguém conseguir 1000 pontos em vez de 10 por causa de um bug que as condições prévias não capturaram (porque não estão ativadas), azar.

Em ambos os casos, idealmente, você deve ter detectado esse bug durante o teste e deve fazer uma parte significativa de seu teste com as afirmações ativadas. O que está sendo discutido aqui é qual é a melhor política para os casos raros nos quais 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 as deixar ativadas - pelo menos em Eiffel. Eu acho que para fazer o mesmo em C ++, você precisa digitá-lo.

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

Daniel Daranas
fonte
1
Seu ponto é definitivamente válido. O SO não especificou uma linguagem particular - no caso de C # a assert padrão é System.Diagnostics.Debug.Assert, que não só não em uma compilação de depuração, e será removido em tempo de compilação em uma compilação de lançamento.
yoyo
2

Havia uma enorme discussão sobre a habilitação / desabilitação de asserções nas versões baseadas no comp.lang.c ++. Moderado, que, se você tiver algumas semanas, poderá ver como as opiniões são variadas. :)

Ao contrário do coppro , acredito que, se você não tiver certeza de que uma asserção pode ser desabilitada em uma compilação de versão, ela não deveria ter sido uma afirmação. As asserções são para proteger contra invariáveis ​​do programa serem 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 de SO, resultando em uma chamada para abortar. (Sem afirmar)
  2. Morra através de uma chamada direta para abortar. (Com afirmação)

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 presente na grande maioria das execuções em que o código não falha.

A resposta para a pergunta realmente depende muito mais de quem serão os clientes da API. Se você estiver escrevendo uma biblioteca que fornece uma API, precisará de alguma forma de mecanismo para notificar seus clientes que eles usaram a API incorretamente. A menos que você forneça duas versões da biblioteca (uma com afirmativas e outra sem), afirmar é muito improvável a escolha apropriada.

Pessoalmente, no entanto, também não tenho certeza se aceitaria exceções neste caso. As 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 memória. Quando você pega uma exceção 'std :: bad_alloc', pode ser possível liberar memória e tentar novamente.

Richard Corden
fonte
2

Descrevi minha visão sobre o estado da questão aqui: como você valida o estado interno de um objeto? . Geralmente, afirme suas reivindicações e lance por violação de outras pessoas. Para desabilitar declarações em compilações de versão, você pode:

  • Desativar declarações para verificações caras (como verificar se um intervalo está ordenado)
  • Mantenha as verificações triviais ativadas (como procurar um ponteiro nulo ou um valor booleano)

Obviamente, nas compilações de versão, asserções com falha e exceções não capturadas devem ser tratadas de outra maneira que não nas compilações de depuração (onde pode ser chamado apenas de 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.

Johannes Schaub - litb
fonte
1

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

As asserções são 'ei programador, isso está quebrado' notificações, elas estão lá para lembrá-lo de bugs que você não teria notado quando eles aconteceram.

as exceções são: 'ei, usuário, algo deu errado' (obviamente você pode codificar para capturá-las para que o usuário nunca seja informado), mas elas foram projetadas para ocorrer em tempo de execução quando o usuário Joe estiver usando o aplicativo.

Portanto, se você acha que consegue eliminar todos os seus erros, use apenas exceções. Se você acha que não pode ..... usar exceções. Você ainda pode usar declarações de depuração para diminuir o número de exceções, é claro.

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

Por fim, eu usaria nenhum - os códigos de erro são muito superiores aos erros que você acha que ocorrerão regularmente. :)

gbjbaanb
fonte
0

Eu prefiro o segundo. Enquanto seus testes podem ter corrido bem, Murphy diz que algo inesperado vai dar errado. Portanto, em vez de obter uma exceção na chamada de método incorreta real, você acaba rastreando um NullPointerException (ou equivalente) 10 quadros de pilha mais fundo.

jdmichal
fonte
0

As respostas anteriores estão corretas: use exceções para funções públicas da API. O único momento em que você pode querer desobedecer essa regra é quando o cheque é computacionalmente caro. Nesse caso, você pode colocá-lo em uma afirmação.

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

Mike Elkins
fonte
0

Você deve usar os dois. As declaraçõ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.

Apreciei as funções de relatório de erros da glib em vez de afirmações antigas e simples. Eles se comportam como declarações de afirmação, mas, em vez de interromper o programa, retornam um valor e deixam o programa continuar. Funciona surpreendentemente bem e, como bônus, você vê o que acontece com o resto do seu programa quando uma função não retorna "o que deveria". Se travar, você sabe que a verificação de erros é laxista em outro lugar no caminho.

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

#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;
}
indiv
fonte
Acho que não vi no OP questionar nada relacionado ao C ++. Eu acredito que não deve ser incluído na sua resposta.
ForceMagic
@ForceMagic: A pergunta tinha a tag C ++ em 2008, quando publiquei essa resposta, e de fato a tag C ++ foi removida há apenas 5 horas. Independentemente disso, o código ilustra um conceito independente de idioma.
indiv
0

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

Use asserções para os casos em que você deseja desativá-lo na produção, errando para deixá-los dentro. O único motivo real para desativar na produção, mas não no desenvolvimento, é acelerar o programa. Na maioria dos casos, essa velocidade não será significativa, mas às vezes o código é de tempo crítico ou o teste é computacionalmente caro. Se o código é essencial, as exceções podem ser as melhores, apesar da desaceleração.

Se houver alguma chance real de recuperação, use uma exceção, pois as asserções não foram projetadas para serem recuperadas. Por exemplo, o código raramente é projetado para se recuperar 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, em comparação com os erros de codificação, os torna mais passíveis de recuperação.

Re-argumento de 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 deve capturar apenas 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 o força a capturar todas as exceções.

Casebash
fonte
0

A regra geral, para mim, é que use expressões assertivas para encontrar erros internos e exceções para erros externos. Você pode se beneficiar muito da discussão a seguir, de Greg, daqui .

Expressões de afirmação são usadas para encontrar erros de programação: erros na própria lógica do programa ou erros na implementação correspondente. Uma condição de afirmação verifica se o programa permanece em um estado definido. Um "estado definido" é basicamente aquele que concorda com as suposições do programa. Observe 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 posteriormente.

Para entender como as asserções se encaixam 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 desreferenciação ou deve afirmar que o ponteiro não é NULL e, em seguida, prosseguir e desreferê-lo independentemente?

Eu imagino que a maioria dos desenvolvedores queira fazer as duas coisas, adicionar a declaração, mas também verificar o ponteiro quanto a um valor NULL, para não travar caso a condição declarada falhe. Na superfície, executar o teste e a verificação pode parecer a decisão mais sensata

Diferentemente de suas condições declaradas, o tratamento de erros de um programa (exceções) não se refere a erros no programa, mas a entradas que o programa obtém de seu ambiente. Geralmente, são "erros" da parte de alguém, como um usuário tentando fazer login em uma conta sem digitar uma senha. E mesmo que o erro possa impedir a conclusão bem-sucedida da tarefa do programa, não há falha no programa. O programa falha ao efetuar login no usuário sem uma senha devido a um erro externo - um erro por parte do usuário. Se as circunstâncias fossem diferentes e o usuário digitasse a senha correta e o programa não a reconhecesse; embora o resultado ainda fosse o mesmo, o fracasso agora pertenceria ao programa.

O objetivo do tratamento de erros (exceções) é duas vezes. O primeiro é comunicar ao usuário (ou algum outro cliente) que um erro na entrada do programa foi detectado e o que isso significa. O segundo objetivo é restaurar o aplicativo após a detecção do erro, para um estado bem definido. Observe que o programa em si não está errado nessa situação. Concedido, o programa pode estar em um estado não ideal, ou mesmo em um estado que não pode ser útil, mas não há erro de programação. Pelo contrário, como o estado de recuperação de erro é antecipado pelo design do programa, ele é aquele que o programa pode manipular.

PS: você pode querer verificar a pergunta semelhante: Exceção versus afirmação .

herohuyongtao
fonte
-1

Veja também esta pergunta :

Em alguns casos, as declarações são desativadas ao criar para liberação. Você pode não ter controle sobre isso (caso contrário, você pode criar com afirmações ativadas); portanto, pode ser uma boa ideia fazê-lo dessa maneira.

O problema com a "correção" dos valores de entrada é que o chamador não vai conseguir o que espera, e isso pode levar a problemas ou até travar em partes totalmente diferentes do programa, tornando a depuração um pesadelo.

Eu costumo lançar uma exceção na instrução if para assumir o papel da declaração, caso estejam desabilitadas

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