Como devo lidar com entrada de usuário inválida?

12

Estou pensando nesse problema há algum tempo e gostaria de ter opiniões de outros desenvolvedores.

Eu costumo ter um estilo de programação muito defensivo. Meu bloco ou método típico é assim:

T foo(par1, par2, par3, ...)
{
    // Check that all parameters are correct, return undefined (null)
    // or throw exception if this is not the case.

    // Compute and (possibly) return result.
}

Além disso, durante o cálculo, verifico todos os ponteiros antes de retirá-los de referência. Minha idéia é que, se houver algum bug e algum ponteiro NULL aparecer em algum lugar, meu programa deve lidar com isso muito bem e simplesmente se recusar a continuar o cálculo. Obviamente, ele pode notificar o problema com uma mensagem de erro no log ou algum outro mecanismo.

Para colocar de uma maneira mais abstrata, minha abordagem é

if all input is OK --> compute result
else               --> do not compute result, notify problem

Outros desenvolvedores, entre os quais alguns colegas meus, usam outra estratégia. Por exemplo, eles não verificam ponteiros. Eles assumem que um pedaço de código deve receber a entrada correta e não deve ser responsável pelo que acontece se a entrada estiver incorreta. Além disso, se uma exceção de ponteiro NULL travar o programa, um erro será encontrado mais facilmente durante o teste e terá mais chances de ser corrigido.

Minha resposta é normalmente: mas e se o bug não for encontrado durante o teste e aparecer quando o produto já estiver sendo usado pelo cliente? Qual é a maneira preferida para o bug se manifestar? Deve ser um programa que não executa uma determinada ação, mas ainda pode continuar funcionando, ou um programa que trava e precisa ser reiniciado?

Resumindo

Qual das duas abordagens para lidar com informações erradas você recomendaria?

Inconsistent input --> no action + notification

ou

Inconsistent input --> undefined behaviour or crash

Editar

Obrigado pelas respostas e sugestões. Também sou fã de design por contrato. Mas mesmo que eu confie na pessoa que escreveu o código chamando meus métodos (talvez seja eu mesmo), ainda pode haver erros, levando a informações incorretas. Portanto, minha abordagem é nunca assumir que um método recebe a entrada correta.

Além disso, eu usaria um mecanismo para capturar o problema e notificá-lo. Em um sistema de desenvolvimento, por exemplo, abriria uma caixa de diálogo para notificar o usuário. Em um sistema de produção, basta escrever algumas informações no log. Não acho que verificações extras possam levar a problemas de desempenho. Não tenho certeza se afirmações são suficientes, se elas estão desativadas em um sistema de produção: talvez alguma situação ocorra na produção que não ocorreu durante o teste.

De qualquer forma, fiquei realmente surpreso que muitas pessoas sigam a abordagem oposta: elas deixam o aplicativo travar "de propósito", porque sustentam que isso facilitará a localização de erros durante o teste.

Giorgio
fonte
Sempre codifique na defensiva. Eventualmente, por motivos de desempenho, você pode colocar uma opção para desativar alguns testes no modo de liberação.
deadalnix
Hoje eu corrigi um bug relacionado a uma verificação de ponteiro NULL ausente. Algum objeto foi criado durante o logoff do aplicativo e o construtor usou um getter para acessar outro objeto que não existia mais. O objeto não foi criado para ser criado nesse ponto. Foi criado devido a outro bug: algum timer não foi parado durante o logout -> um sinal foi enviado -> o destinatário tentou criar um objeto -> construtor consultado e usou outro objeto -> ponteiro NULL -> falha ) Eu realmente não gostaria que uma situação tão desagradável trava meu aplicativo.
Giorgio
1
Regra de reparo: Quando você deve falhar, falhe ruidosamente e o mais rápido possível.
Deadalnix 08/09
"Regra de reparo: quando você deve falhar, falhe ruidosamente e o mais rápido possível.": Eu acho que todos os BSODs do Windows são uma aplicação desta regra. :-)
Giorgio

Respostas:

8

Você acertou. Seja paranóico. Não confie em outro código, mesmo que seja seu próprio código. Você esquece as coisas, faz alterações, o código evolui. Não confie no código externo.

Um bom argumento foi exposto acima: e se as entradas forem inválidas, mas o programa não travar? Então você obtém lixo no banco de dados e erros na linha.

Quando solicitado um número (por exemplo, preço em dólares ou número de unidades), gosto de inserir "1e9" e ver o que o código faz. Pode acontecer.

Há quatro décadas, obtendo meu bacharelado em Ciência da Computação pela UCBerkeley, nos disseram que um bom programa é o tratamento de erros de 50%. Seja paranóico.

Andy Canfield
fonte
Sim, IMHO, esta é uma das poucas situações em que ser paranóico é um recurso e não um problema.
Giorgio
"E se as entradas forem inválidas, mas o programa não falhar? Então você obtém lixo no banco de dados e erros na linha.": Em vez de travar, o programa pode se recusar a executar a operação e retornar um resultado indefinido. Indefinido será propagado através da computação e nenhum lixo será produzido. Mas o programa não precisa travar para conseguir isso.
Giorgio
Sim, mas - meu argumento é que o programa deve DETECTAR a entrada inválida e lidar com ela. Se a entrada não for verificada, ela funcionará no sistema e coisas desagradáveis ​​surgirão mais tarde. Mesmo bater é melhor que isso!
Andy Canfield
Eu concordo totalmente com você: meu método ou função típica começa com uma sequência de verificações para garantir que os dados de entrada estejam corretos.
Giorgio
Hoje, tive novamente uma confirmação de que a estratégia "verifique tudo, não confie em nada" costuma ser uma boa idéia. Um colega meu teve uma exceção de ponteiro NULL devido a uma verificação ausente. Aconteceu que, nesse contexto, era correto ter um ponteiro NULL porque alguns dados não haviam sido carregados, e era correto verificar o ponteiro e simplesmente não fazer nada quando era NULL. :-)
Giorgio
7

Você já tem a ideia certa

Qual das duas abordagens para lidar com informações erradas você recomendaria?

Entrada inconsistente -> nenhuma ação + notificação

ou melhor

Entrada inconsistente -> ação tratada adequadamente

Você não pode realmente adotar uma abordagem de programação de cookie (você poderia), mas você acabaria com um design de fórmula que faz as coisas por hábito e não por escolha consciente.

Tempere o dogmatismo com o pragmatismo.

Steve McConnell disse que melhor

Steve McConnell praticamente escreveu o livro ( Code Complete ) sobre programação defensiva e esse foi um dos métodos que ele aconselhou que você sempre validasse suas informações.

Não me lembro se Steve mencionou isso, no entanto, você deve considerar isso para métodos e funções não particulares , e somente para outros quando necessário.

Justin Shield
fonte
2
Em vez de público, eu sugeriria, todos os métodos não privados para cobrir defensivamente os idiomas que protegiam, compartilhavam ou não tinham conceito de restrição de acesso (tudo é público, implicitamente).
JustinC
3

Não há resposta "correta" aqui, principalmente sem especificar o idioma, o tipo de código e o tipo de produto no qual o código pode entrar. Considerar:

  • A linguagem é importante. No Objective-C, geralmente não há problema em enviar mensagens para zero; nada acontece, mas o programa também não falha. O Java não possui ponteiros explícitos; portanto, ponteiros nulos não são uma grande preocupação. Em C, você precisa ter um pouco mais de cuidado.

  • Ser paranóico é desconfiar ou desconfiar injustificadamente, injustificadamente. Provavelmente isso não é melhor para o software do que para as pessoas.

  • Seu nível de preocupação deve ser proporcional ao nível de risco no código e à provável dificuldade de identificar qualquer problema que apareça. O que acontece no pior dos casos? O usuário reinicia o programa e continua de onde parou? A empresa perde milhões de dólares?

  • Você nem sempre pode identificar informações incorretas. Você pode comparar religiosamente seus ponteiros a zero, mas isso apenas captura um dentre 2 ^ 32 valores possíveis, quase todos ruins.

  • Existem muitos mecanismos diferentes para lidar com erros. Novamente, isso depende até certo ponto do idioma. Você pode usar macros de declaração, instruções condicionais, testes de unidade, tratamento de exceções, design cuidadoso e outras técnicas. Nenhum deles é infalível e nenhum é apropriado para todas as situações.

Portanto, tudo se resume a onde você deseja colocar a responsabilidade. Se você está escrevendo uma biblioteca para ser usada por outras pessoas, provavelmente deseja ter o máximo de cuidado possível com as entradas recebidas e fazer o possível para emitir erros úteis sempre que possível. Em suas próprias funções e métodos particulares, você pode usar asserts para detectar erros tolos, mas de outra forma coloca a responsabilidade do chamador (que é você) de não passar lixo.

Caleb
fonte
+1 - boa resposta. Minha principal preocupação é que uma entrada errada possa causar um problema que aparece em um sistema de produção (quando é tarde demais para fazer algo a respeito). Claro, acho que você está totalmente certo ao dizer que depende do dano que esse problema pode causar ao usuário.
Giorgio
A linguagem desempenha um grande papel. No PHP, metade do código do método acaba verificando o tipo de variável e executando a ação apropriada. Em Java, se o método aceita um int, você não pode transmiti-lo para mais nada, portanto seu método acaba sendo mais claro.
chap
1

Definitivamente, deve haver uma notificação, como uma exceção lançada. Serve de alerta para outros codificadores que podem estar usando mal o código que você escreveu (tentando usá-lo para algo que não se destinava a fazer) de que a entrada deles é inválida ou resulta em erros. Isso é muito útil para rastrear erros, enquanto se você simplesmente retornar nulo, o código continuará até que eles tentem usar o resultado e obtenham uma exceção de código diferente.

Se o seu código encontrar um erro durante uma chamada para outro código (talvez uma falha na atualização do banco de dados) que esteja além do escopo desse trecho de código específico, você realmente não terá controle sobre ele e seu único recurso será lançar uma exceção explicando o que você sabe (apenas o que lhe é dito pelo código que você chamou). Se você souber que determinadas entradas levarão inevitavelmente a esse resultado, você simplesmente não poderá se incomodar em executar seu código e lançar uma exceção informando que entrada não é válida e por quê.

Em uma nota mais relacionada ao usuário final, é melhor retornar algo descritivo, porém simples, para que qualquer pessoa possa entendê-lo. Se o seu cliente ligar e dizer "o programa travou, corrija-o", você terá muito trabalho para rastrear o que deu errado e por quê, e esperando poder reproduzir o problema. O uso adequado de exceções pode não apenas impedir uma falha, mas também fornecer informações valiosas. Uma chamada de um cliente dizendo "O programa está me dando um erro. Ele diz 'XYZ não é uma entrada válida para o método M, porque Z é muito grande", ou algo assim, mesmo que eles não tenham idéia do que isso significa, você sabe exatamente onde procurar. Além disso, dependendo das práticas comerciais da sua empresa / de sua empresa, pode ser que você não esteja solucionando esses problemas; portanto, é melhor deixar um bom mapa para eles.

Portanto, a versão curta da minha resposta é que sua primeira opção é a melhor.

Inconsistent input -> no action + notify caller
yoozer8
fonte
1

Eu lutei com esse mesmo problema ao passar por uma aula universitária de programação. Inclinei-me para o lado paranóico e costumo verificar tudo, mas me disseram que esse era um comportamento equivocado.

Estávamos aprendendo "Design por contrato". Ênfase é que as pré-condições, invariantes e pós-condições sejam especificadas nos comentários e documentos de design. Como a pessoa que implementa minha parte do código, devo confiar no arquiteto de software e habilitá-lo, seguindo as especificações que incluem as pré-condições (quais entradas meus métodos devem ser capazes de lidar e quais entradas não serão enviadas) . A verificação excessiva em cada chamada de método resulta em inchaço.

As asserções devem ser usadas durante as iterações de construção para verificar a correção do programa (validação de pré-condições, invariantes, pós-condições). As asserções seriam ativadas na compilação de produção.

Richard
fonte
0

Usar "asserções" é o caminho a seguir para notificar outros desenvolvedores de que eles estão fazendo errado, somente nos métodos "particulares" . Ativá-los / desativá-los é apenas um sinalizador para adicionar / remover em tempo de compilação e, como tal, é fácil remover asserções do código de produção. Também há uma ótima ferramenta para saber se você está fazendo algo errado em seus próprios métodos.

Quanto à verificação de parâmetros de entrada em métodos públicos / protegidos, prefiro trabalhar defensivamente, verificar parâmetros e lançar InvalidArgumentException ou algo parecido. É por isso que existem aqui. Também depende se você está escrevendo uma API ou não. Se for uma API, e mais ainda se for de código fechado, valide melhor tudo para que os desenvolvedores saibam exatamente o que deu errado. Caso contrário, se a fonte estiver disponível para outros desenvolvedores, não será em preto / branco. Apenas seja consistente com suas escolhas.

Edit: apenas para adicionar que, se você procurar, por exemplo, no Oracle JDK, verá que eles nunca verificam "null" e deixam o código travar. Como ele lançará uma NullPointerException de qualquer maneira, por que se preocupar em procurar nulo e lançar uma exceção explícita. Eu acho que faz algum sentido.

Jalayn
fonte
Em Java, você obtém uma exceção de ponteiro nulo. No C ++, um ponteiro nulo trava o aplicativo. Talvez haja outros exemplos: divisão por zero, índice fora da faixa e assim por diante.
Giorgio