A condição redundante é comparada às melhores práticas?

16

Desenvolvo software nos últimos três anos, mas recentemente acordei com a ignorância sobre as boas práticas. Isso me levou a começar a ler o livro Clean Code , que está melhorando minha vida, mas estou lutando para ter uma ideia de algumas das melhores abordagens para escrever meus programas.

Eu tenho um programa Python no qual eu ...

  1. use argparse required=Truepara aplicar dois argumentos, que são os dois nomes de arquivo. o primeiro é o nome do arquivo de entrada, o segundo é o nome do arquivo de saída
  2. tem uma função readFromInputFileque primeiro verifica se um nome de arquivo de entrada foi inserido
  3. tem uma função writeToOutputFileque primeiro verifica se um nome de arquivo de saída foi inserido

Meu programa é pequeno o suficiente para que eu acredite que as verificações 2 e 3 são redundantes e devem ser removidas, liberando ambas as funções de uma ifcondição desnecessária . No entanto, também fui levado a acreditar que "a verificação dupla está correta" e pode ser a solução certa em um programa em que as funções podem ser chamadas de um local diferente, onde a análise de argumentos não ocorre.

(Além disso, se a leitura ou gravação falhar, eu tenho um try exceptem cada função para gerar uma mensagem de erro apropriada.)

Minha pergunta é: é melhor evitar toda verificação de condição redundante? A lógica de um programa deve ser tão sólida que as verificações precisem ser feitas apenas uma vez? Existem bons exemplos que ilustram isso ou o contrário?

EDIT: Obrigado a todos pelas respostas! Eu aprendi algo com cada um. Ver tantas perspectivas me dá uma compreensão muito melhor de como abordar esse problema e determinar uma solução com base em meus requisitos. Obrigado!

tese
fonte
Aqui está uma versão altamente generalizada da sua pergunta: softwareengineering.stackexchange.com/questions/19549/… . Eu não diria que é duplicado, pois tem um foco muito maior, mas talvez ajude.
Doc Brown

Respostas:

15

O que você está pedindo é chamado de "robustez" e não há resposta certa ou errada. Depende do tamanho e da complexidade do programa, do número de pessoas que trabalham nele e da importância de detectar falhas.

Em pequenos programas que você escreve sozinho e somente para si, a robustez é normalmente uma preocupação muito menor do que quando você está escrevendo um programa complexo que consiste em vários componentes, talvez escritos por uma equipe. Em tais sistemas, existem limites entre os componentes na forma de APIs públicas e, em cada limite, geralmente é uma boa ideia validar os parâmetros de entrada, mesmo que "a lógica do programa seja tão sólida que essas verificações sejam redundantes". " Isso facilita a detecção de bugs e ajuda a manter os tempos de depuração menores.

No seu caso, você deve decidir por si mesmo, que tipo de ciclo de vida espera para o seu programa. É um programa que você espera que seja usado e mantido ao longo dos anos? A adição de uma verificação redundante provavelmente é melhor, pois não será improvável que seu código seja refatorado no futuro e que suas funções reade writepossam ser usadas em um contexto diferente.

Ou é um pequeno programa apenas para fins de aprendizado ou diversão? Então essas verificações duplas não serão necessárias.

No contexto de "Código Limpo", pode-se perguntar se uma verificação dupla viola o princípio DRY. Na verdade, às vezes ocorre, pelo menos em um grau menor: a validação de entrada pode ser interpretada como parte da lógica de negócios de um programa, e tê-lo em dois locais pode levar aos problemas de manutenção habituais causados ​​pela violação do DRY. Robustez vs. DRY geralmente é uma troca - a robustez requer redundância no código, enquanto o DRY tenta minimizar a redundância. E com a crescente complexidade do programa, a robustez se torna cada vez mais importante do que ser seco na validação.

Por fim, deixe-me dar um exemplo do que isso significa no seu caso. Vamos supor que seus requisitos mudem para algo como

  • o programa também trabalhará com um argumento, o nome do arquivo de entrada; se não houver um nome de arquivo de saída fornecido, ele será automaticamente construído a partir do nome do arquivo de entrada, substituindo o sufixo.

Isso torna provável que você precise alterar sua dupla validação em dois lugares? Provavelmente não, esse requisito leva a uma alteração ao chamar argparse, mas nenhuma alteração writeToOutputFile: essa função ainda exigirá um nome de arquivo. Portanto, no seu caso, eu votaria na validação de entrada duas vezes, o risco de obter problemas de manutenção por causa de dois locais para mudar é IMHO muito menor do que o risco de problemas de manutenção devido a erros mascarados causados ​​por poucas verificações.

Doc Brown
fonte
"... limites entre componentes em forma de APIs públicas ..." Observo que "classes ultrapassam limites", por assim dizer. Então, o que é necessário é uma classe; uma classe de domínio comercial coerente. Estou deduzindo deste OP que o onipresente princípio de "é simples, portanto não precisa de uma classe" está em ação aqui. Pode haver uma classe simples envolvendo o "objeto principal", aplicando regras de negócios como "um arquivo deve ter um nome", que não apenas seca o código existente, mas o mantém no futuro.
Radarbob
@radarbob: o que escrevi não se restringe a OOP ou componentes em forma de classes. Isso também se aplica a bibliotecas arbitrárias com uma API pública, orientada a objetos ou não.
Doc Brown
5

Redundância não é pecado. Redundância desnecessária é.

  1. Se readFromInputFile()e writeToOutputFile()são funções públicas (e pelas convenções de nomenclatura do Python, elas são porque seus nomes não começaram com dois sublinhados), as funções poderão um dia ser usadas por alguém que evite o argparse por completo. Isso significa que, quando eles deixam de lado os argumentos, não conseguem ver sua mensagem de erro personalizada argparse.

  2. Se readFromInputFile()e writeToOutputFile()verificar os próprios parâmetros, você novamente poderá mostrar uma mensagem de erro personalizada que explica a necessidade de nomes de arquivos.

  3. Se readFromInputFile()e writeToOutputFile()não verificar os próprios parâmetros, nenhuma mensagem de erro personalizada será mostrada. O usuário terá que descobrir a exceção resultante por conta própria.

Tudo se resume a 3. Escreva um código que realmente use essas funções, evitando argparse, e produza a mensagem de erro. Imagine que você não olhou para essas funções e confia apenas nos nomes delas para fornecer entendimento suficiente para usar. Quando isso é tudo que você sabe, existe alguma maneira de ser confundido com a exceção? Existe uma necessidade de uma mensagem de erro personalizada?

Desligar a parte do cérebro que lembra o interior dessas funções é difícil. Tanto é assim que alguns recomendam escrever o código de uso antes do código que é usado. Dessa forma, você chega ao problema já sabendo como são as coisas do lado de fora. Você não precisa fazer o TDD para fazer isso, mas se o fizer, já estará vindo de fora primeiro.

candied_orange
fonte
4

A medida em que você torna seus métodos independentes e reutilizáveis é uma coisa boa. Isso significa que os métodos devem perdoar o que aceitam e devem ter resultados bem definidos (precisos no que retornam). Isso também significa que eles devem ser capazes de lidar com tudo o que é passado com facilidade e não fazer suposições sobre a natureza da entrada, qualidade, tempo, etc.

Se um programador tem o hábito de escrever métodos que fazem suposições sobre o que é passado, com base em idéias como "se isso for quebrado, temos coisas maiores com que nos preocupar" ou "o parâmetro X não pode ter o valor Y porque o restante o código impede ", de repente você não tem mais componentes independentes e dissociados. Seus componentes são essencialmente dependentes do sistema mais amplo. Esse é um tipo de acoplamento sutil e rígido e leva ao aumento exponencial do custo total de propriedade à medida que a complexidade do sistema aumenta.

Observe que isso pode significar que você está validando as mesmas informações mais de uma vez. Mas tudo bem. Cada componente é responsável por sua própria validação , à sua maneira . Isso não é uma violação do DRY, porque as validações são por componentes independentes desacoplados, e uma alteração na validação em um não precisa necessariamente ser replicada exatamente no outro. Não há redundância aqui. X tem a responsabilidade de verificar suas entradas para suas próprias necessidades e passar algumas para Y. Y tem sua própria responsabilidade de verificar suas entradas para suas necessidades .

Brad Thomas
fonte
1

Suponha que você tenha uma função (em C)

void readInputFile (const char* path);

E você não encontra nenhuma documentação sobre o caminho. E então você olha para a implementação e diz

void readInputFile (const char* path)
{
    assert (path != NULL && strlen (path) > 0);

Isso não apenas testa a entrada da função, mas também informa ao usuário da função que o caminho não pode ser NULL ou uma sequência vazia.

gnasher729
fonte
0

Em geral, a verificação dupla nem sempre é boa ou ruim. Sempre há muitos aspectos da questão em seu caso particular, dos quais o assunto depende. No seu caso:

  • Qual é o tamanho do programa? Quanto menor, mais óbvio é que o chamador faz a coisa certa. Quando seu programa cresce, torna-se mais importante especificar exatamente quais são as pré-condições e pós-condições de cada rotina.
  • os argumentos já estão verificados pelo argparsemódulo. Muitas vezes, é uma má idéia usar uma biblioteca e depois fazer o trabalho sozinho. Por que usar a biblioteca então?
  • Qual a probabilidade de o seu método ser reutilizado em um contexto em que o chamador não verifica argumentos? Quanto mais provável, mais importante é validar argumentos.
  • O que acontece se um argumento não vão faltar? Não encontrar um arquivo de entrada provavelmente interromperá o processamento definitivo. Provavelmente, esse é um modo de falha óbvio, fácil de corrigir. O tipo insidioso de erros são aqueles em que o programa continua trabalhando e produz resultados errados sem que você perceba .
Kilian Foth
fonte
0

Suas verificações duplas parecem estar em locais onde raramente são usadas. Portanto, essas verificações estão simplesmente tornando seu programa mais robusto:

Um cheque a mais não vai doer, um a menos.

No entanto, se você estiver verificando dentro de um loop repetido com frequência, pense em remover a redundância, mesmo que a verificação em si na maioria das vezes não seja dispendiosa em comparação com o que se segue após a verificação.

qwerty_so
fonte
E como você já o tem, não vale a pena remover, a menos que esteja em um loop ou algo assim.
StarWeaver 26/11/16
0

Talvez você possa mudar seu ponto de vista:

Se algo der errado, qual é o resultado? Isso prejudicará seu aplicativo / usuário?

É claro que você sempre pode argumentar se mais ou menos verificações são melhores ou piores, mas essa é uma questão bastante escolástica. E como você está lidando com software do mundo real , há consequências no mundo real.

No contexto que você está dando:

  • um arquivo de entrada A
  • um arquivo de saída B

Eu suponho que você está fazendo transformação de A para B . Se A e B são pequenos e a transformação é pequena, quais são as consequências?

1) Você esqueceu de especificar de onde ler: Então o resultado não é nada . E o tempo de execução será menor que o esperado. Você olha para o resultado - ou melhor: procura um resultado ausente, vê que invocou o comando de maneira errada, recomeça e tudo está bem novamente

2) Você esqueceu de especificar o arquivo de saída. Isso resulta em diferentes cenários:

a) A entrada é lida de uma só vez. A transformação começa e o resultado deve ser gravado, mas você recebe um erro. Dependendo do tempo, o usuário precisa esperar (dependendo da massa de dados que deve ser processada) isso pode ser irritante.

b) A entrada é lida passo a passo. Em seguida, o processo de gravação é encerrado imediatamente como em (1) e o usuário começa novamente.

A verificação desleixada pode ser considerada OK em algumas circunstâncias. Depende totalmente do seu caso e da sua intenção.

Além disso: você deve evitar a paranóia e não fazer muitas checagens duplas.

Thomas Junk
fonte
0

Eu argumentaria que os testes não são redundantes.

  • Você tem duas funções públicas que requerem um nome de arquivo como parâmetro de entrada. É apropriado validar seus parâmetros. As funções podem ser usadas em qualquer programa que precise de sua funcionalidade.
  • Você tem um programa que requer dois argumentos que devem ser nomes de arquivos. Acontece usar as funções. É apropriado que o programa verifique seus parâmetros.

Enquanto os nomes de arquivos estão sendo verificados duas vezes, eles estão sendo verificados para propósitos diferentes. Em um pequeno programa em que você pode confiar na verificação dos parâmetros para as funções, as verificações nas funções podem ser consideradas redundantes.

Uma solução mais robusta teria um ou dois validadores de nome de arquivo.

  • Para um arquivo de entrada, convém verificar se o parâmetro especificou um arquivo legível.
  • Para um arquivo de saída, convém verificar se o parâmetro é um arquivo gravável ou um nome de arquivo válido que pode ser criado e gravado.

Eu uso duas regras para quando executar ações:

  • Faça-os o mais cedo possível. Isso funciona bem para coisas que sempre serão necessárias. Do ponto de vista deste programa, esta é a verificação dos valores argv, e as validações subsequentes na lógica do programa seriam redundantes. Se as funções forem movidas para uma biblioteca, elas não serão mais redundantes, pois a biblioteca não pode confiar que todos os chamadores tenham validado os parâmetros.
  • Faça-os o mais tarde possível. Isso funciona muito bem para coisas que raramente serão necessárias. Do ponto de vista deste programa, são as verificações nos parâmetros de função.
BillThor
fonte
0

O cheque é redundante. Corrigindo isso, porém, é necessário remover readFromInputFile e writeToOutputFile e substituí-los por readFromStream e writeToStream.

No ponto em que o código recebe o fluxo de arquivos, você sabe que possui um fluxo válido conectado a um arquivo válido ou qualquer outra coisa à qual um fluxo possa ser conectado. Isso evita verificações redundantes.

Você pode perguntar: bem, você ainda precisa abrir o fluxo em algum lugar. Sim, mas isso acontece internamente no método de análise de argumentos. Você tem duas verificações lá, uma para verificar se um nome de arquivo é necessário, a outra é para verificar se o arquivo apontado pelo nome de arquivo é um arquivo válido no contexto especificado (por exemplo, existe um arquivo de entrada, o diretório de saída é gravável). Esses são tipos diferentes de verificações, portanto, não são redundantes e ocorrem no método de análise de argumentos (perímetro do aplicativo) e não no aplicativo principal.

Lie Ryan
fonte