Verificar pré-condições ou não


8

Eu estava querendo encontrar uma resposta sólida para a questão de ter ou não verificações em tempo de execução para validar as entradas com o objetivo de garantir que um cliente permaneça no final do contrato, conforme o design por contrato. Por exemplo, considere um construtor de classe simples:

class Foo
{
public:
  Foo( BarHandle bar )
  {
    FooHandle handle = GetFooHandle( bar );
    if( handle == NULL ) {
      throw std::exception( "invalid FooHandle" );
    }
  }
};

Eu argumentaria neste caso que um usuário não deve tentar construir um Foosem um válido BarHandle. Não parece correto verificar se baré válido dentro do Fooconstrutor. Se eu simplesmente documentar que Fooo construtor requer um válido BarHandle , não é suficiente? Essa é uma maneira adequada de impor minha pré-condição no projeto por contrato?

Até agora, tudo o que li tem opiniões contraditórias sobre isso. Parece que 50% das pessoas diriam para verificar se baré válido, os outros 50% diriam que eu não deveria fazê-lo, por exemplo, considere um caso em que o usuário verifique se BarHandleestá correto, mas uma segunda verificação (e desnecessária) também está sendo feito dentro do Fooconstrutor.


Respostas:


10

Eu não acho que haja uma única resposta para isso. Eu acho que a principal coisa necessária é a consistência - ou você aplica todas as condições prévias em uma função, ou então não tenta aplicar nenhuma delas. Infelizmente, isso é bastante raro - o que normalmente acontece é que, em vez de pensar nas pré-condições e aplicá-las, os programadores adicionam bits de código para impor condições prévias cuja violação causou falhas durante o teste, mas frequentemente deixa em aberto outras possibilidades que podem causar falhas, mas não aconteceu nos testes.

Em muitos casos, é bastante razoável fornecer duas camadas: uma para uso "interno" que não tenta impor nenhuma condição prévia e, em seguida, uma segunda para uso "externo" que apenas impõe condições prévias e depois invoca a primeira.

No entanto, acho que é melhor ter as pré-condições aplicadas no nó de origem, não apenas documentadas. Uma exceção ou declaração é muito mais difícil de ignorar do que a documentação e muito mais provável de permanecer sincronizada com o restante do código.


Em princípio, concordo com o seu último parágrafo. Embora isso agora signifique que há três coisas que precisam ser mantidas em sincronia; a documentação, as próprias declarações e os casos de teste que provam que as declarações estão fazendo seu trabalho (se você acredita nessas coisas)!
9788 Oliver Stoneworth #

@OliCharlesworth: Sim, ele cria uma terceira coisa para manter em sincronia, mas estabelece uma (a imposição no código-fonte) como aquela que geralmente é confiável quando há discordância. Caso contrário, você geralmente não sabe.
Jerry Coffin

2
@JerryCoffin Eu poderia verificar se fooé NULL, mas ser NULL não é a única maneira de fooser inválido. Por exemplo, que tal -1 convertido em a FooHandle? Não consigo verificar todas as formas possíveis de identificador ser inválido. NULL é uma escolha óbvia e algo que normalmente é verificado, mas não uma verificação conclusiva. O que você recomendaria aqui?
precisa saber é o seguinte

@RobertDailey: Por fim, é quase impossível garantir contra todos os abusos possíveis, especialmente quando / se o elenco estiver envolvido. Com a transmissão, o usuário pode subverter essencialmente qualquer coisa que possa verificar. O que mais enfatizo é a diferença entre 1) supor que os parâmetros sejam bons e adicionar verificações das coisas que dão errado nos testes; e 2) descobrir as pré-condições com a maior precisão possível e aplicá-las da melhor maneira possível .
21712 Jerry Coffin

@JerryCoffin Como isso difere da Programação Defensiva, que geralmente não é considerada uma "coisa boa"? Na maioria das vezes, técnicas de programação defensiva como essa e muitas outras que eu já vi não são muito pragmáticas. É um design feito para combater seus colegas de trabalho com maus hábitos de codificação ou outras coisas, em vez de focar na funcionalidade prática e na implementação de seus métodos. Eu vejo isso facilmente saindo do controle como um hábito que adiciona lógica extra à placa de aquecimento em todas as funções de classe. Você acha que o teste de unidade elimina a necessidade dessas verificações de pré-condição.
precisa saber é o seguinte

4

É uma pergunta muito difícil, porque existem vários conceitos diferentes:

  • Correção
  • Documentação
  • atuação

No entanto, esse é principalmente um artefato de uma falha de tipo , neste caso. A nulidade é melhor aplicada por restrições de tipo, porque o compilador realmente as verifica. Ainda assim, como nem tudo pode ser capturado em um sistema de tipos, especialmente em C ++, a pergunta em si ainda vale a pena.


Pessoalmente, acho que a correção e a documentação são fundamentais. Ser rápido e errado é inútil. Ser rápido e errado apenas às vezes é um pouco melhor, mas também não traz muita coisa para a mesa.

O desempenho, porém, pode ser crítico em algumas partes dos programas, e algumas verificações podem ser bastante extensas (ou seja: provar que um gráfico direcionado tem todos os seus nós acessíveis e co-acessíveis). Por isso, votaria em uma abordagem dupla.

Princípio um: falha rápida . Esse é um princípio orientador da programação defensiva em geral, que defende a detecção de erros o mais cedo possível. Eu acrescentaria Fail Hard à equação.

if (not bar) { abort(); }

Infelizmente, em um ambiente de produção, falhar bastante não é necessariamente a melhor solução. Nesse caso, uma exceção específica pode ajudar a sair de lá com pressa e permitir que algum manipulador de alto nível entenda e lide com o caso com falha adequadamente (provavelmente registrando e avançando com um novo caso).

Isso, no entanto, não trata da questão de testes caros . Em locais quentes, esses testes podem custar muito. Nesse caso, é razoável habilitar apenas o teste nas compilações DEBUG.

Isso nos deixa com uma solução simples e agradável:

  • SOFT_ASSERT(Cond_, Text_)
  • DEBUG_ASSERT(Cond_, Text_)

Onde as duas macros são definidas assim:

 #ifdef NDEBUG
 #  define SOFT_ASSERT(Cond_, Text_)                                                \
        while (not (Cond_)) { throw Exception(Text_, __FILE__, __LINE__); }
 #  define DEBUG_ASSERT(Cond_, Text_) while(false) {}
 #else // NDEBUG
 #  define SOFT_ASSERT(Cond_, Text_)                                                \
        while (not (Cond_)) {                                                       \
            std::cerr << __FILE__ << '#' << __LINE__ << ": " << Text_ << std::endl; \
            abort();                                                                \
        }
 #  define DEBUG_ASSERT(Cond_, Text_) SOFT_ASSERT(Cond_, Text_)
 #endif // NDEBUG

0

Uma citação que ouvi sobre isso é:

"Seja conservador no que faz e liberal no que aceita."

O que se resume a seguir os contratos em busca de argumentos quando você chama funções e verificar todas as entradas antes de agir quando você escreve funções.

Em última análise, depende do domínio. Se você estiver criando uma API do sistema operacional, é melhor verificar todas as entradas, não confie em todos os dados recebidos como válidos antes de começar a agir com base nela. Se você estiver criando uma biblioteca para uso de outras pessoas, vá em frente, deixe o usuário se ferrar (o OpenGL vem à mente primeiro por algum motivo desconhecido).

EDIT: no sentido OO, parece haver duas abordagens - uma que diz que um objeto nunca deve estar malformado (todos os seus invariantes devem ser verdadeiros) durante todo o tempo em que um objeto é acessível e outra que diz que você tem um construtor que não define todos os invariantes, você define mais alguns valores e tem uma segunda função de inicialização que finaliza o init.

Eu meio que gosto mais do primeiro, pois ele não requer conhecimento mágico ou depende da documentação atual para saber quais partes da inicialização o construtor não faz.


Para essa inicialização, prefiro o objeto construtor que contém dados de inicialização parciais e, posteriormente, cria um objeto "útil" totalmente inicializado.
user470365
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.