Devo validar o valor de retorno de uma chamada de método, mesmo sabendo que o método não pode retornar uma entrada incorreta?

30

Estou me perguntando se devo me defender contra o valor de retorno de uma chamada de método, validando que eles atendam às minhas expectativas, mesmo que eu saiba que o método que estou chamando atenderá a essas expectativas.

DADO

User getUser(Int id)
{
    User temp = new User(id);
    temp.setName("John");

    return temp;
}

EU DEVERIA FAZER

void myMethod()
{
    User user = getUser(1234);
    System.out.println(user.getName());
}

OU

void myMethod()
{
    User user = getUser(1234);

    // Validating
    Preconditions.checkNotNull(user, "User can not be null.");
    Preconditions.checkNotNull(user.getName(), "User's name can not be null.");

    System.out.println(user.getName());
}

Estou perguntando isso no nível conceitual. Se eu conheço o funcionamento interno do método que estou chamando. Ou porque eu escrevi ou inspecionei. E a lógica dos possíveis valores que ela retorna atende às minhas pré-condições. É "melhor" ou "mais apropriado" pular a validação ou ainda devo me defender de valores errados antes de prosseguir com o método que estou implementando atualmente, mesmo que sempre seja aprovado.


Minha conclusão de todas as respostas (sinta-se à vontade para vir para o seu próprio país):

Afirme quando
  • O método demonstrou se comportar mal no passado
  • O método é de uma fonte não confiável
  • O método é usado em outros lugares e não indica explicitamente suas pós-condições
Não afirme quando:
  • O método está próximo ao seu (consulte a resposta escolhida para obter detalhes)
  • O método define explicitamente seu contrato com algo como documento adequado, segurança de tipo, teste de unidade ou verificação pós-condição
  • O desempenho é crítico (nesse caso, uma declaração do modo de depuração pode funcionar como uma abordagem híbrida)
Didier A.
fonte
11
Apontar para essa cobertura de código% 100!
precisa saber é o seguinte
9
Por que você está se concentrando em apenas um problema ( Null)? Provavelmente é igualmente problemático se o nome for ""(não nulo, mas a sequência vazia) ou "N/A". Ou você pode confiar no resultado ou precisa ser adequadamente paranóico.
MSalters
3
Em nossa base de código, temos um esquema de nomes: getFoo () promete não retornar nulo, e getFooIfPresent () pode retornar nulo.
Pjc50
De onde vem sua presunção de sanidade do código? Você está assumindo que o valor sempre será não nulo porque é validado na entrada ou devido à estrutura pela qual o valor é recuperado? E, mais importante, você está testando todos os cenários possíveis de casos extremos? Incluindo a possibilidade de script malicioso?
Zibbobz

Respostas:

42

Isso depende da probabilidade de alteração do getUser e do myMethod e, mais importante, da probabilidade de alteração independente um do outro .

Se, de alguma forma, você tem certeza de que o getUser nunca mudará no futuro, então sim, é uma perda de tempo validando-a, tanto quanto é uma perda de tempo validando que item um valor de 3 imediatamente após uma i = 3;declaração. Na realidade, você não sabe disso. Mas existem algumas coisas que você sabe:

  • Essas duas funções "vivem juntas"? Em outras palavras, eles mantêm as mesmas pessoas, fazem parte do mesmo arquivo e, portanto, tendem a permanecer "sincronizados" entre si por conta própria? Nesse caso, provavelmente é um exagero adicionar verificações de validação, pois isso significa apenas mais código que precisa ser alterado (e potencialmente gera bugs) toda vez que as duas funções mudam ou são refatoradas para um número diferente de funções.

  • A função getUser faz parte de uma API documentada com um contrato específico e o myMethod é apenas um cliente dessa API em outra base de código? Nesse caso, você pode ler essa documentação para descobrir se deve validar valores de retorno (ou pré-validar parâmetros de entrada!) Ou se é realmente seguro seguir cegamente o caminho feliz. Se a documentação não esclarecer isso, peça ao mantenedor para corrigir isso.

  • Finalmente, se essa função em particular mudou repentinamente e inesperadamente seu comportamento no passado, de uma maneira que quebrou seu código, você tem todo o direito de ficar paranóico com relação a ela. Os erros tendem a se agrupar.

Observe que todas as opções acima se aplicam mesmo que você seja o autor original das duas funções. Não sabemos se é esperado que essas duas funções "convivam" pelo resto de suas vidas, ou se elas se separem lentamente em módulos separados, ou se você, de alguma forma, se matou com um bug nos antigos versões do getUser. Mas você provavelmente pode adivinhar bastante.

Ixrec
fonte
2
Além disso: se você alterasse getUsermais tarde, para que pudesse retornar nulo, a verificação explícita forneceria informações que você não pôde obter de "NullPointerException" e o número da linha?
usar o seguinte comando
Parece que há duas coisas que você pode validar: (1) Que o método funcione corretamente, como em, não possui bugs. (2) Que o método respeita o seu contrato. Eu posso ver a validação para o número 1 ser um exagero. Você deve ter os testes executando o número 1. É por isso que eu não validaria i = 3 ;. Então, minha pergunta está relacionada ao # 2. Gosto da sua resposta para isso, mas, ao mesmo tempo, é uma resposta do seu julgamento. Você vê algum outro problema decorrente da validação explícita de seus contratos, além de perder um pouco de tempo escrevendo a afirmação?
Didier A.
O único outro "problema" é que, se sua interpretação do contrato estiver errada, ou se tornar errada, você deverá alterar toda a validação. Mas isso se aplica a todos os outros códigos também.
Ixrec
11
Não pode ir contra a multidão. Esta resposta é definitivamente a mais correta para cobrir o tópico da minha pergunta. Gostaria de acrescentar que, ao ler todas as respostas, especialmente @ MikeNakis, agora acho que você deve afirmar suas pré-condições, pelo menos no modo de depuração, quando tiver um dos dois últimos pontos principais do Ixrec. Se você se deparar com o primeiro ponto, provavelmente será um exagero afirmar suas pré-condições. Por fim, dado um contrato explícito, como documento adequado, segurança de tipo, teste de unidade ou verificação pós-condição, você não deve afirmar suas condições prévias, o que seria um exagero.
Didier A.
22

A resposta do Ixrec é boa, mas adotarei uma abordagem diferente porque acredito que vale a pena considerar. Para os propósitos desta discussão, falarei sobre afirmações, já que esse é o nome pelo qual você Preconditions.checkNotNull()é tradicionalmente conhecido.

O que eu gostaria de sugerir é que os programadores geralmente superestimam o grau em que têm certeza de que algo se comportará de uma certa maneira e subestimam as consequências de não se comportar dessa maneira. Além disso, os programadores geralmente não percebem o poder das asserções como documentação. Posteriormente, na minha experiência, os programadores afirmam as coisas com muito menos frequência do que deveriam.

Meu lema é:

A pergunta que você deve sempre se perguntar não é "devo afirmar isso?" mas "há algo que eu esqueci de afirmar?"

Naturalmente, se você tiver certeza absoluta de que algo se comporta de uma certa maneira, você se abstém de afirmar que de fato se comportou dessa maneira, e isso é geralmente razoável. Não é realmente possível escrever software se você não pode confiar em nada.

Mas há exceções até para esta regra. Curiosamente, para tomar o exemplo de Ixrec, existe um tipo de situação em que é perfeitamente desejável seguir int i = 3;com uma afirmação que iestá de fato dentro de um certo intervalo. Essa é a situação em que ié suscetível de ser alterada por alguém que está tentando vários cenários hipotéticos, e o código a seguir baseia-se em iter um valor dentro de um determinado intervalo, e não é imediatamente óbvio olhando rapidamente para o código a seguir. o intervalo aceitável é. Portanto, int i = 3; assert i >= 0 && i < 5;a princípio pode parecer sem sentido, mas se você pensar sobre isso, ele diz que você pode brincar ie também indica o alcance dentro do qual você deve permanecer. Uma asserção é o melhor tipo de documentação, porqueé imposta pela máquina , por isso é uma garantia perfeita e é refatorada juntamente com o código , portanto permanece sempre pertinente.

Seu getUser(int id)exemplo não é um parente conceitual muito distante do assign-constant-and-assert-in-rangeexemplo. Você vê, por definição, erros que acontecem quando as coisas exibem um comportamento diferente do que esperávamos. É verdade que não podemos afirmar tudo, portanto, às vezes, haverá alguma depuração. Mas é um fato bem estabelecido na indústria que, quanto mais rápido detectamos um erro, menos isso custa. As perguntas que você deve fazer não são apenas a certeza de que você getUser()nunca retornará nulo ((e nunca retornará um usuário com um nome nulo)), mas também que tipos de coisas ruins acontecerão com o restante do código, se ocorrerem em um dia, retorne algo inesperado e como é fácil olhar rapidamente para o restante do código para saber exatamente o que era esperado getUser().

Se o comportamento inesperado fizer com que seu banco de dados fique corrompido, talvez você deva afirmar mesmo se tiver 101% de certeza de que isso não acontecerá. Se um nullnome de usuário causasse algum erro estranho e não rastreável de milhares de linhas mais abaixo e bilhões de ciclos de clock depois, talvez seja melhor falhar o mais cedo possível.

Portanto, mesmo que eu não discorde da resposta de Ixrec, sugiro que você considere seriamente o fato de que as afirmações não custam nada; portanto, não há realmente nada a perder, apenas a ser ganho, usando-as liberalmente.

Mike Nakis
fonte
2
Sim. Afirme. Eu vim aqui para dar mais ou menos essa resposta. + +
RubberDuck 29/04
2
@SteveJessop Gostaria de corrigir isso ... && sureness < 1).
Mike # Nakis #
2
@ MikeNakis: isso é sempre um pesadelo ao escrever testes de unidade, o valor de retorno permitido pela interface, mas impossível de gerar a partir de qualquer entrada ;-) De qualquer forma, quando você tem 101% de certeza de que está certo, está definitivamente errado !
21415 Steve Jobs (
2
+1 por apontar coisas que esqueci completamente de mencionar na minha resposta.
Ixrec 29/04
11
@DavidZ Está correto, minha pergunta assumiu a validação em tempo de execução. Mas dizer que você não confia no método na depuração e confia no prod é um ponto de vista possivelmente bom sobre o tópico. Portanto, uma afirmação no estilo C pode ser uma boa maneira de lidar com essa situação.
Didier A.
8

Um não definitivo.

Um chamador nunca deve verificar se a função que está chamando respeita seu contrato. O motivo é simples: existem potencialmente muitos chamadores e é impraticável e até mesmo errado do ponto de vista conceitual de cada chamador duplicar a lógica para verificar os resultados de outras funções.

Se alguma verificação for feita, cada função deve verificar suas próprias pós-condições. As pós-condições dão a você a confiança de que você implementou a função corretamente e os usuários poderão confiar no contrato de sua função.

Se uma determinada função nunca retornar nula, isso deve ser documentado e se tornar parte do contrato dessa função. Este é o objetivo desses contratos: oferecer aos usuários alguns invariantes em que possam confiar, para que enfrentem menos obstáculos ao escrever suas próprias funções.

Paulo
fonte
Concordo com o sentimento geral, mas há circunstâncias em que definitivamente faz sentido validar valores de retorno. Por exemplo, se você estiver chamando um serviço da Web externo, às vezes deseja validar pelo menos o formato da resposta (xsd, etc ...)
AK_
Parece que você está defendendo um estilo Design by Contract em detrimento de um estilo de Programação Defensiva. Eu acho que essa é uma ótima resposta. Se o método tivesse um contrato estrito, eu poderia confiar nele. No meu caso, não, mas eu poderia alterá-lo para fazê-lo. Diga que eu não poderia? Você argumentaria que eu deveria confiar na implementação? Mesmo se não declarar explicitamente no documento, ele não retorna nulo ou realmente tem uma verificação pós-condição?
Didier A.
Na minha opinião, você deve usar seu bom senso e ponderar aspectos diferentes, como o quanto você confia no autor para fazer a coisa certa, qual a probabilidade de a interface da função mudar e quão apertadas são as restrições de tempo. Dito isto, na maioria das vezes, o autor de uma função tem uma idéia bastante clara de qual deve ser o contrato da função, mesmo que ele não a documente. Por isso, digo que você deve primeiro confiar nos autores para fazer a coisa certa e só se tornar defensivo quando souber que a função não está bem escrita.
Paul
Acho que primeiro confiar em outras pessoas para fazer a coisa certa me ajuda a fazer o trabalho mais rapidamente. Dito isto, os tipos de aplicativos que escrevo são fáceis de corrigir quando ocorre um problema. Alguém que trabalha com software mais crítico para a segurança pode pensar que estou sendo muito branda e prefere uma abordagem mais defensiva.
Paul
5

Se null for um valor de retorno válido do método getUser, seu código deverá tratar disso com um If, não uma Exception - não é um caso excepcional.

Se null não for um valor de retorno válido do método getUser, ocorrerá um erro no getUser - o método getUser deve lançar uma exceção ou ser corrigido.

Se o método getUser fizer parte de uma biblioteca externa ou não puder ser alterado e você desejar que exceções sejam lançadas no caso de um retorno nulo, agrupe a classe e o método para executar as verificações e ative a exceção para que todas as instâncias de a chamada para getUser é consistente no seu código.

MatthewC
fonte
Nulo não é um valor de retorno válido de getUser. Ao inspecionar getUser, vi que a implementação nunca retornaria nula. Minha pergunta é: devo confiar na minha inspeção e não ter uma verificação no meu método, ou devo duvidar da minha inspeção e ainda ter uma verificação. Também é possível que o método mude no futuro, quebrando minhas expectativas de não retornar nulo.
Didier A.
E se getUser ou user.getName forem alterados para gerar exceções? Você deve ter uma verificação, mas não como parte do método usando o Usuário. Encapsule o método getUser - e até a classe User - para impor seus contratos, se você estiver preocupado com alterações futuras na quebra de código getUser. Crie também testes de unidade para verificar suas suposições sobre o comportamento da classe.
precisa saber é o seguinte
@didibus Parafraseando sua resposta ... Um emoticon sorridente não é um valor de retorno válido de getUser. Ao inspecionar getUser, vi que a implementação nunca retornaria um emoticon sorridente . Minha pergunta é: devo confiar na minha inspeção e não ter uma verificação no meu método, ou devo duvidar da minha inspeção e ainda ter uma verificação. Também é possível que o método mude no futuro, quebrando minhas expectativas de não retornar um emoticon sorridente . Não é tarefa do chamador confirmar que a função chamada está cumprindo seu contrato.
Vince O'Sullivan
@ VinceO'Sullivan Parece que o "contrato" está no centro da questão. E que minha pergunta é sobre contratos implícitos, versus explícitos. Um método que retorna int, não afirmo que não estou recebendo uma string. Mas, se eu apenas quero ints positivos, devo? Está implícito que o método não retorna int negativo, porque eu o inspecionei. Não está claro na assinatura ou na documentação que isso não ocorre.
Didier A.
11
Não importa se o contrato é implícito ou explícito. Não é tarefa do código de chamada validar que o método chamado retorne dados válidos quando dados válidos. No seu exemplo, você sabe que inits negativos não são respostas incorretas, mas você sabe se int positivo está correto? Quantos testes você realmente precisa para provar que o método está funcionando? O único curso correto é assumir que o método está correto.
Vince O'Sullivan
5

Eu diria que a premissa da sua pergunta está errada: por que usar uma referência nula para começar?

O padrão de objeto nulo é uma maneira de retornar objetos que essencialmente não fazem nada: em vez de retornar nulo para o usuário, retorne um objeto que represente "nenhum usuário".

Esse usuário ficaria inativo, teria um nome em branco e outros valores não nulos indicando "nada aqui". O principal benefício é que você não precisa desarrumar o seu programa, irá anular verificações, registros etc. Você apenas usa seus objetos.

Por que um algoritmo deve se importar com um valor nulo? Os passos devem ser os mesmos. É igualmente fácil e muito mais claro ter um objeto nulo que retorne zero, sequência em branco etc.

Aqui está um exemplo de verificação de código para null:

User u = getUser();
String displayName = "";
if (u != null) {
  displayName = u.getName();
}
return displayName;

Aqui está um exemplo de código usando um objeto nulo:

return getUser().getName();

Qual é mais claro? Eu acredito que o segundo é.


Enquanto a pergunta é marcada como , é importante notar que o padrão de objeto nulo é realmente útil em C ++ ao usar referências. As referências Java são mais parecidas com ponteiros C ++ e permitem nulo. Referências C ++ não podem ser mantidas nullptr. Se alguém quiser ter algo análogo a uma referência nula em C ++, o padrão de objeto nulo é a única maneira que conheço para fazer isso.

Comunidade
fonte
Nulo é apenas o exemplo do que afirmo. Pode muito bem ser que eu precise de um nome não vazio. Minha pergunta ainda se aplicaria, você validaria isso? Ou pode ser algo que retorna um int, e eu não posso ter um int negativo, ou deve estar em um determinado intervalo. Não pense nisso como um problema nulo, por exemplo.
Didier A.
11
Por que um método retornaria um valor incorreto? O único local em que isso deve ser verificado é nos limites do seu aplicativo: interfaces com o usuário e com outros sistemas. Caso contrário, é por isso que temos exceções.
Imagine que eu tinha: int i = getCount; flutuar x = 100 / i; Se eu sei que getCount não retorna 0, porque eu escrevi o método ou o inspecionei, devo ignorar a verificação de 0?
Didier A.
@didibus: você pode pular a verificação se getCount() documentar uma garantia de que agora não retornará 0, e não o fará no futuro, exceto em circunstâncias em que alguém decida fazer uma alteração significativa e procure atualizar tudo o que for necessário para lidar com isso. a nova interface. Ver o que o código atualmente faz não ajuda, mas se você for inspecionar o código, o código a ser inspecionado são os testes de unidade getCount(). Se eles testarem para não retornar 0, você saberá que qualquer alteração futura para retornar 0 será considerada uma alteração de ruptura porque os testes falharão.
21715 Steve
Não está claro para mim que objetos nulos são melhores que nulos. Um pato nulo pode grasnar, mas não é um pato - de fato, semanticamente, é a antítese de um pato. Depois de introduzir um objeto nulo, qualquer código usando essa classe pode ter que verificar se algum de seus objetos é o objeto nulo - por exemplo, você tem algo nulo em um conjunto, quantas coisas você tem? Isso me parece uma maneira de adiar falhas e ofuscar erros. O artigo vinculado adverte especificamente contra o uso de objetos nulos "apenas para evitar verificações nulas e tornar o código mais legível".
sdenham
1

Geralmente, eu diria que depende principalmente dos três aspectos a seguir:

  1. robustez: o método de chamada pode lidar com o nullvalor? Se um nullvalor resultar em uma RuntimeExceptionrecomendação, eu recomendaria uma verificação - a menos que a complexidade seja baixa e os métodos de chamada / chamada sejam criados pelo mesmo autor e nullnão sejam esperados (por exemplo, ambos privateno mesmo pacote).

  2. responsabilidade do código: se o desenvolvedor A é responsável pelo getUser()método e o desenvolvedor B o usa (por exemplo, como parte de uma biblioteca), recomendo fortemente que valide seu valor. Só porque o desenvolvedor B pode não saber sobre uma alteração que resulta em um potencial nullvalor de retorno.

  3. complexidade: quanto maior a complexidade geral do programa ou ambiente, mais eu recomendaria validar o valor de retorno. Mesmo se você tiver certeza de que não pode ser nullhoje no contexto do método de chamada, pode ser que você precise mudar getUser()para outro caso de uso. Passados ​​alguns meses ou anos e adicionadas milhares de linhas de códigos, isso pode ser uma armadilha.

Além disso, eu recomendo documentar possíveis nullvalores de retorno em um comentário JavaDoc. Devido ao destaque das descrições do JavaDoc na maioria dos IDEs, esse pode ser um aviso útil para quem usa getUser().

/** Returns ....
  * @param: id id of the user
  * @return: a User instance related to the id;
  *          null, if no user with identifier id exists
 **/
User getUser(Int id)
{
    ...
}
André
fonte
-1

Você definitivamente não precisa.

Por exemplo, se você já sabe que um retorno nunca pode ser nulo - Por que você deseja adicionar uma verificação nula? Adicionar uma verificação nula não quebrará nada, mas é apenas redundante. E melhor não codificar lógica redundante, desde que você possa evitá-la.

Yellen
fonte
11
Eu sei, mas apenas porque inspecionei ou escrevi a implementação do outro método. O contrato do método não diz explicitamente que não pode. Assim, poderia, por exemplo, mudar no futuro. Ou poderia ter um bug que faz com que um nulo retorne, mesmo que estivesse tentando não fazê-lo.
Didier A.