Validação do parâmetro de entrada no chamador: duplicação de código?


16

Onde é o melhor local para validar os parâmetros de entrada da função: no chamador ou na própria função?

Como gostaria de melhorar meu estilo de codificação, tento encontrar as melhores práticas ou algumas regras para esse problema. Quando e o que é melhor.

Nos meus projetos anteriores, costumávamos verificar e tratar todos os parâmetros de entrada dentro da função (por exemplo, se não for nulo). Agora, li aqui em algumas respostas e também no livro Pragmatic Programmer, que a validação do parâmetro de entrada é de responsabilidade do chamador.

Então, isso significa que eu devo validar os parâmetros de entrada antes de chamar a função. Em todos os lugares a função é chamada. E isso levanta uma questão: ela não cria uma duplicação da condição de verificação em qualquer lugar em que a função é chamada?

Não estou interessado apenas em condições nulas, mas na validação de quaisquer variáveis ​​de entrada (valor negativo para sqrtfuncionar, dividir por zero, combinação incorreta de estado e CEP ou qualquer outra coisa)

Existem algumas regras para decidir onde verificar a condição de entrada?

Estou pensando em alguns argumentos:

  • quando o tratamento de variável inválida pode variar, é bom validá-la no lado do chamador (por exemplo, sqrt()função - em alguns casos, talvez eu queira trabalhar com números complexos, por isso trato a condição no chamador)
  • quando a condição de verificação é a mesma em todos os chamadores, é melhor verificar dentro da função, para evitar duplicações
  • a validação do parâmetro de entrada no chamador ocorre apenas uma antes da chamada de muitas funções com esse parâmetro. Portanto, a validação de um parâmetro em cada função não é eficaz
  • a solução certa depende do caso particular

Espero que esta pergunta não seja duplicada de nenhuma outra, procurei esse problema e encontrei perguntas semelhantes, mas elas não mencionam exatamente esse caso.

Respostas:


15

Depende. A decisão de onde colocar a validação deve basear-se na descrição e na força do contrato implícito (ou documentado) pelo método. A validação é uma boa maneira de reforçar a adesão a um contrato específico. Se, por qualquer motivo, o método tiver um contrato muito rígido, sim, é sua responsabilidade verificar antes de ligar.

Esse é um conceito especialmente importante quando você cria um método público , porque basicamente anuncia que algum método executa alguma operação. É melhor fazer o que você diz que faz!

Tome o seguinte método como exemplo:

public void DeletePerson(Person p)
{            
    _database.Delete(p);
}

Qual é o contrato implícito DeletePerson? O programador pode apenas assumir que, se algum Personfor passado, ele será excluído. No entanto, sabemos que isso nem sempre é verdade. E se pfor um nullvalor? E se pnão existir no banco de dados? E se o banco de dados estiver desconectado? Portanto, o DeletePerson não parece cumprir bem seu contrato. Às vezes, ele exclui uma pessoa e, às vezes, lança uma NullReferenceException, ou uma DatabaseNotConnectedException, ou às vezes não faz nada (como se a pessoa já tiver sido excluída).

APIs como essa são notoriamente difíceis de usar, porque quando você chama essa "caixa preta" de um método, todo tipo de coisa terrível pode acontecer.

Aqui estão algumas maneiras de melhorar o contrato:

  • Adicione validação e adicione uma exceção ao contrato. Isso fortalece o contrato , mas exige que o chamador realize a validação. A diferença, no entanto, é que agora eles conhecem seus requisitos. Nesse caso, comunico isso com um comentário XML em C #, mas você pode adicionar um throws(Java), usar um Assertou usar uma ferramenta de contrato como o Code Contracts.

    ///<exception>ArgumentNullException</exception>
    ///<exception>ArgumentException</exception>
    public void DeletePerson(Person p)
    {            
        if(p == null)
            throw new ArgumentNullException("p");
        if(!_database.Contains(p))
            throw new ArgumentException("The Person specified is not in the database.");
    
        _database.Delete(p);
    }
    

    Nota lateral: O argumento contra esse estilo geralmente é que causa pré-validação excessiva em todos os códigos de chamada, mas, na minha experiência, esse geralmente não é o caso. Pense em um cenário em que você está tentando excluir uma Pessoa nula. Como isso aconteceu? De onde veio a Pessoa nula? Se essa é uma interface do usuário, por exemplo, por que a tecla Delete foi manipulada se não há uma seleção atual? Se já foi excluído, já não deveria ter sido removido da exibição? Obviamente, há exceções a isso, mas à medida que o projeto cresce, muitas vezes você agradece a códigos como esse por impedir que bugs penetrem profundamente no sistema.

  • Adicione validação e código defensivamente. Isso torna o contrato mais flexível , porque agora esse método faz mais do que apenas excluir a pessoa. Alterei o nome do método para refletir isso, mas pode não ser necessário se você for consistente em sua API. Essa abordagem tem seus prós e contras. O ponto principal é que agora você pode chamar a TryDeletePersonpassagem com todo tipo de entrada inválida e nunca se preocupar com exceções. O truque, é claro, é que os usuários do seu código provavelmente chamarão muito esse método, ou isso poderá dificultar a depuração nos casos em que p for nulo. Isso pode ser considerado uma violação leve do Princípio da Responsabilidade Única ; portanto, lembre-se disso se ocorrer uma guerra de chamas.

    public void TryDeletePerson(Person p)
    {            
        if(p == null || !_database.Contains(p))
            return;
    
        _database.Delete(p);
    }
    
  • Combine abordagens. Às vezes, você quer um pouco de ambos, em que deseja que os chamadores externos sigam as regras de perto (para forçá-los a codificar responsáveis), mas deseja que seu código privado seja flexível.

    ///<exception>ArgumentNullException</exception>
    ///<exception>ArgumentException</exception>
    public void DeletePerson(Person p)
    {            
        if(p == null)
            throw new ArgumentNullException("p");
        if(!_database.Contains(p))
            throw new ArgumentException("The Person specified is not in the database.");
    
        TryDeletePerson(p);
    }
    
    internal void TryDeletePerson(Person p)
    {            
        if(p == null || !_database.Contains(p))
            return;
    
        _database.Delete(p);
    }
    

Na minha experiência, concentrar-se nos contratos que você implicou, em vez de uma regra rígida, funciona melhor. A codificação defensiva parece funcionar melhor nos casos em que é difícil ou difícil para o chamador determinar se uma operação é válida. Contratos estritos parecem funcionar melhor quando você espera que o chamador faça apenas chamadas de método quando realmente faz muito sentido.


Obrigado pela resposta muito agradável com o exemplo. Eu gosto do ponto de abordagens "defensivas" e "contratos estritos".
Srnka

7

É uma questão de convenção, documentação e caso de uso.

Nem todas as funções são iguais. Nem todos os requisitos são iguais. Nem toda validação é igual.

Por exemplo, se seu projeto Java tenta evitar ponteiros nulos sempre que possível (consulte as recomendações de estilo Guava , por exemplo), você ainda valida todos os argumentos de função para garantir que não sejam nulos? Provavelmente não é necessário, mas é provável que você ainda faça isso, para facilitar a localização de bugs. Mas você pode usar uma declaração onde você lançou anteriormente um NullPointerException.

E se o projeto estiver em C ++? A convenção / tradição em C ++ é documentar pré-condições, mas apenas verificá-las (se houver) nas compilações de depuração.

Nos dois casos, você tem uma condição prévia documentada em sua função: nenhum argumento pode ser nulo. Em vez disso, você pode estender o domínio da função para incluir nulos com comportamento definido, por exemplo, "se algum argumento for nulo, gera uma exceção". Obviamente, isso é novamente minha herança C ++ falando aqui - em Java, é comum o suficiente documentar pré-condições dessa maneira.

Mas nem todas as pré-condições podem ser verificadas razoavelmente. Por exemplo, um algoritmo de pesquisa binária tem a pré-condição de que a sequência a ser pesquisada deve ser classificada. Mas verificar se definitivamente é assim é uma operação O (N), fazendo isso em cada chamada meio que derrota o ponto de usar um algoritmo O (log (N)) em primeiro lugar. Se você estiver programando defensivamente, poderá fazer verificações menores (por exemplo, verificando se, para cada partição pesquisada, os valores inicial, intermediário e final são classificados), mas isso não captura todos os erros. Normalmente, você só precisa confiar na condição prévia a ser cumprida.

O único local real em que você precisa de verificações explícitas é nos limites. Entrada externa para o seu projeto? Validar, validar, validar. Uma área cinza é os limites da API. Realmente depende de quanto você deseja confiar no código do cliente, quanto dano a entrada inválida causa e quanta assistência você deseja fornecer na busca de bugs. Qualquer limite de privilégio deve contar como externo, é claro - os syscalls, por exemplo, são executados em um contexto de privilégio elevado e, portanto, devem ter muito cuidado para validar. É claro que qualquer validação desse tipo deve ser interna ao syscall.


Obrigado pela sua resposta. Você pode, por favor, fornecer o link para a recomendação de estilo Guava? Não consigo pesquisar no Google e descobrir o que você quis dizer com isso. +1 para validação dos limites.
Srnka

Link adicionado. Na verdade, não é um guia de estilo completo, apenas uma parte da documentação dos utilitários não nulos.
Sebastian Redl

6

A validação de parâmetro deve ser a preocupação da função que está sendo chamada. A função deve saber o que é considerado entrada válida e o que não é. Os chamadores podem não saber disso, especialmente quando não sabem como a função é implementada internamente. A função deve lidar com qualquer combinação de valores de parâmetros dos chamadores.

Como a função é responsável pela validação de parâmetros, é possível gravar testes de unidade nessa função para garantir que ela se comporte conforme o esperado com valores de parâmetros válidos e inválidos.


Obrigado pela resposta. Então você pensa que essa função deve verificar parâmetros de entrada válidos e inválidos em todos os casos. Algo diferente da afirmação do livro do programador pragmático: "a validação do parâmetro de entrada é responsabilidade do chamador". É bom pensar "A função deve saber o que é considerado válido ... Os chamadores podem não saber disso" ... Então você não gosta de usar pré-condições?
Srnka

1
Você pode usar pré-condições, se quiser (veja a resposta de Sebastian ), mas prefiro ficar na defensiva e lidar com qualquer tipo de contribuição possível.
22413 Bernard

4

Dentro da própria função. Se a função for usada mais de uma vez, você não deseja verificar o parâmetro para cada chamada de função.

Além disso, se a função for atualizada de maneira a afetar a validação do parâmetro, você deverá procurar todas as ocorrências da validação de chamadas para atualizá-las. Não é adorável :-).

Você pode consultar a Cláusula de Guarda

Atualizar

Veja minha resposta para cada cenário que você forneceu.

  • quando o tratamento de variável inválida pode variar, é bom validá-la no lado do chamador (por exemplo, sqrt()função - em alguns casos, talvez eu queira trabalhar com números complexos, por isso trato a condição no chamador)

    Responda

    A maioria das linguagens de programação suporta números inteiros e reais por padrão, número não complexo, portanto, sua implementação sqrtaceita apenas números não negativos. O único caso de uma sqrtfunção que retorna um número complexo é quando você usa uma linguagem de programação orientada para a matemática, como o Mathematica

    Além disso, sqrtpara a maioria das linguagens de programação já está implementada, portanto, você não pode modificá-la e, se você tentar substituir a implementação (consulte a aplicação de patches de macaco), seus colaboradores ficarão totalmente chocados com o porquê de sqrtaceitar números negativos repentinamente.

    Se você quiser um, pode envolvê-lo em sua sqrtfunção personalizada , que lida com números negativos e retorna números complexos.

  • quando a condição de verificação é a mesma em todos os chamadores, é melhor verificar dentro da função, para evitar duplicações

    Responda

    Sim, essa é uma boa prática para evitar a dispersão da validação de parâmetro no seu código.

  • a validação do parâmetro de entrada no chamador ocorre apenas uma antes da chamada de muitas funções com esse parâmetro. Portanto, a validação de um parâmetro em cada função não é eficaz

    Responda

    Seria bom se o chamador fosse uma função, você não acha?

    Se as funções dentro do chamador são usadas por outro chamador, o que impede você de validar o parâmetro nas funções chamadas pelo chamador?

  • a solução certa depende do caso particular

    Responda

    Procure o código de manutenção. Mover a validação de parâmetro garante uma fonte de verdade sobre o que a função pode aceitar ou não.


Obrigado pela resposta. O sqrt () foi apenas um exemplo, o mesmo comportamento com o parâmetro de entrada pode ser usado por muitas outras funções. "se a função for atualizada de maneira a afetar a validação do parâmetro, você deverá procurar todas as ocorrências da validação de chamadas" - não concordo com isso. Podemos dizer o mesmo para o valor de retorno: se a função for atualizada de maneira a afetar o valor de retorno, você precisará corrigir todos os chamadores ... Acho que a função precisa ter uma tarefa bem definida para executar ... Caso contrário, a mudança no chamador é necessária de qualquer maneira.
Srnka

2

Uma função deve indicar suas condições pré e pós.
As pré-condições são as condições que devem ser atendidas pelo chamador antes que ele possa usar corretamente a função e possa (e geralmente inclua) incluir a validade dos parâmetros de entrada.
As pós-condições são as promessas que a função faz aos seus interlocutores.

Quando a validade dos parâmetros de uma função faz parte das pré-condições, é responsabilidade do chamador garantir que esses parâmetros sejam válidos. Mas isso não significa que todo chamador deve verificar explicitamente cada parâmetro antes da chamada. Na maioria dos casos, nenhum teste explícito é necessário porque a lógica interna e as pré-condições do chamador já garantem que os parâmetros sejam válidos.

Como medida de segurança contra erros de programação (bugs), você pode verificar se os parâmetros passados ​​para uma função realmente atendem às pré-condições indicadas. Como esses testes podem ser caros, é uma boa ideia desativá-los para compilações de versões. Se esses testes falharem, o programa deverá ser encerrado, porque provavelmente ocorreu um erro.

Embora, à primeira vista, a verificação no chamador pareça convidar a duplicação de código, na verdade é o contrário. A verificação no chamado resulta na duplicação de código e em muito trabalho desnecessário.
Basta pensar nisso: com que frequência você passa parâmetros por várias camadas de funções, fazendo apenas pequenas alterações em algumas delas ao longo do caminho. Se você aplicar consistentemente o método de check-in-call , cada uma dessas funções intermediárias precisará refazer a verificação para cada um dos parâmetros.
E agora imagine que um desses parâmetros seja uma lista classificada.
Com a verificação no chamador, apenas a primeira função precisaria garantir que a lista seja realmente classificada. Todos os outros sabem que a lista já está classificada (como foi o que declararam em sua pré-condição) e podem transmiti-la sem verificações adicionais.


+1 Obrigado pela resposta. Boa reflexão: "A verificação no chamado resulta na duplicação de código e em muito trabalho desnecessário sendo realizado". E na frase: "Na maioria dos casos, nenhum teste explícito é necessário porque a lógica interna e as pré-condições do chamador já garantem" - o que você quer dizer com expressão "lógica interna"? A funcionalidade DBC?
Srnka

@srnka: Com "lógica interna" quero dizer os cálculos e decisões em uma função. É essencialmente a implementação da função.
Bart van Ingen Schenau 23/02

0

Na maioria das vezes, você não pode saber quem, quando e como chamará a função que você escreveu. É melhor assumir o pior: sua função será chamada com parâmetros inválidos. Então você definitivamente deveria cobrir isso.

No entanto, se o idioma que você usa suportar exceções, talvez você não verifique certos erros e certifique-se de que uma exceção seja lançada, mas, neste caso, você deve descrever o caso na documentação (você precisa ter documentação). A exceção fornecerá ao chamador informações suficientes sobre o que aconteceu e também direcionará a atenção para os argumentos inválidos.


Na verdade, pode ser melhor validar o parâmetro e, se o parâmetro for inválido, defina você mesmo uma exceção. Eis o porquê: os palhaços que chamam sua rotina sem se preocupar em fornecer dados válidos são os mesmos que não se preocupam em verificar o código de retorno de erro que indica que passaram dados inválidos. Lançar uma exceção FORÇA o problema a ser corrigido.
John R. Strohm
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.