É mau estilo checar redundantemente uma condição?

10

Costumo chegar a posições no meu código onde me vejo checando uma condição específica repetidamente.

Quero dar um pequeno exemplo: suponha que exista um arquivo de texto que contenha linhas começando com "a", linhas começando com "b" e outras linhas e, na verdade, só queira trabalhar com os dois primeiros tipos de linhas. Meu código seria algo parecido com isto (usando python, mas leia-o como pseudocódigo):

# ...
clear_lines() # removes every other line than those starting with "a" or "b"
for line in lines:
    if (line.startsWith("a")):
        # do stuff
    elif (line.startsWith("b")):
        # magic
    else:
        # this else is redundant, I already made sure there is no else-case
        # by using clear_lines()
# ...

Você pode imaginar que eu não apenas verificarei essa condição aqui, mas talvez também em outras funções e assim por diante.

Você pensa nisso como ruído ou agrega algum valor ao meu código?

marktani
fonte
5
É basicamente sobre se você está ou não codificando defensivamente. Você vê esse código sendo muito editado? É provável que isso faça parte de um sistema que precisa ser extremamente confiável? Não vejo muito mal em empurrar um assert()lá para ajudar nos testes, mas além disso é provavelmente excessivo. Dito isto, variará dependendo da situação.
Latty
seu caso 'else' é essencialmente um código morto / inacessível. Verifique se não há requisitos de todo o sistema que proíbam isso.
NWS
@ NWS: você está dizendo que eu deveria manter o caso? Desculpe, eu não te entendo completamente.
marktani
2
não especialmente relacionado à pergunta - mas eu transformaria essa 'afirmação' em invariável - o que exigiria uma nova classe "Linha" (talvez com classes derivadas para A e B), em vez de tratar as linhas como seqüências de caracteres e dizer o que eles representam de fora. Eu ficaria feliz em elaborar sobre isso em cima da codereview
MattDavey
você quis dizer elif (line.startsWith("b"))? a propósito, você pode remover com segurança os parênteses que sobraram nas condições, eles não são idiomáticos no Python.
tokland

Respostas:

14

Essa é uma prática extremamente comum e a maneira de lidar com ela é por meio de filtros de ordem superior .

Essencialmente, você passa uma função para o método de filtro, juntamente com a lista / sequência na qual deseja filtrar e a lista / sequência resultante contém apenas os elementos que deseja.

Não estou familiarizado com a sintaxe python (porém, ela contém uma função como vista no link acima), mas em c # / f # parece com isso:

c #:

var linesWithAB = lines.Where(l => l.StartsWith("a") || l.StartsWith("b"));
foreach (var line in linesWithAB)
{
    /* line is guaranteed to ONLY start with a or b */
}

f # (assume inumerável, caso contrário, List.filter seria usado):

let linesWithAB = lines
    |> Seq.filter (fun l -> l.StartsWith("a") || l.StartsWith("b"))

for line in linesWithAB do
    /* line is guaranteed to ONLY start with a or b */

Portanto, para deixar claro: se você usa códigos / padrões testados e aprovados, é um estilo ruim. Isso, e alterar a lista na memória da maneira que você parece via clear_lines (), perde a segurança do encadeamento e as esperanças de paralelismo que você poderia ter.

Steven Evers
fonte
3
Como uma nota, a sintaxe python para isso seria uma expressão gerador: (line for line in lines if line.startswith("a") or line.startswith("b")).
Latty
11
+1 por apontar que a implementação imperativa (desnecessária) de clear_linesé realmente uma má ideia. No Python, você provavelmente usaria geradores para evitar carregar o arquivo completo na memória.
precisa saber é o seguinte
O que acontece quando o arquivo de entrada é maior que a memória disponível?
Blrfl
@Blrfl: bem, se o termo gerador for consistente entre c # / f # / python, o que @tokland e @Lattyware traduzem em c # / f # yield e / ou yield! afirmações. É um pouco mais óbvio no meu exemplo de f # porque o Seq.filter só pode ser aplicado a coleções de IEnumerable <T>, mas os dois exemplos de código funcionarão se linesfor uma coleção gerada.
Steven Evers
@mcwise: Quando você começa a olhar para todas as outras funções disponíveis que funcionam dessa maneira, começa a ficar realmente sexy e incrivelmente expressivo, porque todas elas podem ser encadeadas e compostas. Olhe skip, take, reduce( aggregateem .NET), map( selectna NET), e não há mais, mas isso é um começo realmente sólido.
Steven Evers
14

Recentemente, tive que implementar um programador de firmware usando o formato Motorola S-record , muito semelhante ao que você descreve. Como tivemos uma pressão de tempo, meu primeiro rascunho ignorou redundâncias e simplificou com base no subconjunto que eu realmente precisava usar no meu aplicativo. Ele passou nos meus testes com facilidade, mas falhou bastante assim que outra pessoa tentou. Não havia idéia de qual era o problema. Foi o caminho todo, mas falhou no final.

Portanto, não tive escolha a não ser implementar todas as verificações redundantes, a fim de diminuir onde estava o problema. Depois disso, levei cerca de dois segundos para encontrar o problema.

Levei talvez duas horas extras para fazer da maneira certa, mas perdi um dia do tempo de outras pessoas também na solução de problemas. É muito raro que alguns ciclos de processador valham um dia desperdiçado na solução de problemas.

Dito isto, no que diz respeito à leitura de arquivos, geralmente é benéfico projetar seu software para trabalhar com a leitura e o processamento de uma linha por vez, em vez de ler o arquivo inteiro na memória e processá-lo na memória. Dessa forma, ainda funcionará em arquivos muito grandes.

Karl Bielefeldt
fonte
"É muito raro que alguns ciclos de processador valham um dia desperdiçado na solução de problemas". Obrigado pela resposta, você tem um bom argumento.
marktani
5

Você pode gerar uma exceção no elsecaso. Dessa forma, não é redundante. Exceções não são coisas que não deveriam acontecer, mas são verificadas de qualquer maneira.

clear_lines() # removes every other line than those starting with "a" or "b"
for line in lines:
    if (line.startsWith("a)):
        # do stuff
    if (line.startsWith("b")):
        # magic
    else:
        throw BadLineException
# ...
Tulains Córdova
fonte
Eu diria que a última é uma má idéia, pois é menos explícita - se você decidir mais tarde adicionar uma "c", pode ser menos claro.
Latty
Primeira sugestão tem mérito ... o segundo (assumir "b") é uma má idéia
Andrew
@Lattyware eu melhorei a resposta. Obrigado por seus comentários.
Tulains Córdova
11
@ Andrew eu melhorei a resposta. Obrigado por seus comentários.
Tulains Córdova
3

No design por contrato , supõe-se que cada função deve executar seu trabalho conforme descrito em sua documentação. Portanto, cada função tem uma lista de pré-condições, ou seja, condições nas entradas da função e pós-condições, ou seja, condições da saída da função.

A função deve garantir a seus clientes que, se as entradas respeitarem as pré-condições, a saída será a descrita pelas pós-condições. Se pelo menos uma das pré-condições não for respeitada, a função poderá fazer o que quiser (travar, retornar qualquer resultado, ...). Portanto, pré e pós-condições são uma descrição semântica da função.

Graças ao contrato, uma função tem certeza de que seus clientes a usam corretamente e um cliente tem certeza de que a função faz seu trabalho corretamente.

Alguns idiomas processam contratos de forma nativa ou por meio de uma estrutura dedicada. Para os outros, o melhor é verificar as condições pré e pós graças às declarações, como disse o @Lattyware. Mas eu não chamaria isso de programação defensiva, pois, na minha opinião, esse conceito está mais focado na proteção contra as entradas do usuário (humano).

Se você explorar contratos, poderá evitar a condição redundantemente verificada, pois a função chamada funciona perfeitamente e você não precisa da verificação dupla, ou a função chamada é disfuncional e a função de chamada pode se comportar como deseja.

O mais difícil é, então, definir qual função é responsável por quê e documentar estritamente essas funções.

mgoeminne
fonte
1

Na verdade, você não precisa do clear_lines () no início. Se a linha não for "a" ou "b", os condicionais simplesmente não serão acionados. Se você quiser se livrar dessas linhas, faça o resto em clear_line (). Como está, você está fazendo duas passagens pelo documento. Se você pular o clear_lines () no início e fizer isso como parte do loop foreach, reduzirá o tempo de processamento pela metade.

Não é apenas estilo ruim, é ruim computacionalmente.

Engenheiro Mundial
fonte
2
Pode ser que essas linhas estejam sendo usadas para outra coisa, e elas precisam ser tratadas antes de lidar com as "a"/ "b"linhas. Não dizendo que é provável (o nome claro implica que eles estão sendo descartados), apenas que existe a possibilidade de que seja necessário. Se esse conjunto de linhas for repetidamente repetido no futuro, também poderá valer a pena removê-las antecipadamente, para evitar muitas iterações inúteis.
Latty
0

Se você realmente quiser fazer alguma coisa se encontrar uma string inválida (texto de depuração de saída, por exemplo), diria que está absolutamente correto. Algumas linhas extras e alguns meses depois, quando ele parar de funcionar por algum motivo desconhecido, você pode ver a saída para descobrir o porquê.

Se, no entanto, for seguro ignorá-lo, ou você tiver certeza de que nunca obterá uma sequência inválida, não haverá necessidade de ramificação extra.

Pessoalmente, sou sempre a favor de colocar pelo menos uma saída de rastreamento para qualquer condição inesperada - isso facilita muito a vida quando você tem um bug com a saída anexada, informando exatamente o que deu errado.

Bok McDonagh
fonte
0

... suponha que exista um arquivo de texto que contenha linhas começando com "a", linhas começando com "b" e outras linhas, e eu realmente só queira trabalhar com os dois primeiros tipos de linhas. Meu código seria algo parecido com isto (usando python, mas leia-o como pseudocódigo):

# ...
clear_lines() # removes every other line than those starting with "a" or "b"
for line in lines:
    if ...

Eu odeio if...then...elseconstruções. Eu evitaria todo o problema:

process_lines_by_first_character (lines,  
                                  'a' => { |line| ... a code ... },
                                  'b' => { |line| ... b code ... } )
Kevin Cline
fonte