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 Foo
sem um válido BarHandle
. Não parece correto verificar se bar
é válido dentro do Foo
construtor. Se eu simplesmente documentar que Foo
o 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 BarHandle
está correto, mas uma segunda verificação (e desnecessária) também está sendo feito dentro do Foo
construtor.
fonte
Respostas:
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.
fonte
foo
é NULL, mas ser NULL não é a única maneira defoo
ser inválido. Por exemplo, que tal -1 convertido em aFooHandle
? 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?É uma pergunta muito difícil, porque existem vários conceitos diferentes:
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.
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:
fonte
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.
fonte