¿Es un mal estilo verificar una condición de forma redundante?

10

A menudo llego a posiciones en mi código donde me encuentro comprobando una condición específica una y otra vez.

Quiero darles un pequeño ejemplo: supongamos que hay un archivo de texto que contiene líneas que comienzan con "a", líneas que comienzan con "b" y otras líneas y en realidad solo quiero trabajar con los dos primeros tipos de líneas. Mi código se vería así (usando python, pero léelo 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()
# ...

Puedes imaginar que no solo comprobaré esta condición aquí, sino que tal vez también en otras funciones, etc.

¿Lo considera ruido o agrega algún valor a mi código?

Marktani
fuente
55
Básicamente se trata de si estás o no codificando a la defensiva. ¿Ves que este código se edita mucho? ¿Es probable que esto sea parte de un sistema que necesita ser extremadamente confiable? No veo mucho daño al empujar assert()allí para ayudar con las pruebas, pero más allá de eso probablemente sea excesivo. Dicho esto, variará según la situación.
Latty
su caso 'else' es esencialmente código muerto / inalcanzable. Verifique que no existan requisitos de todo el sistema que lo prohíban.
NWS
@NWS: ¿estás diciendo que debería mantener el caso más? Lo siento, no te entiendo completamente.
marktani
2
no está especialmente relacionado con la pregunta, pero convertiría esa 'afirmación' en una invariante, lo que requeriría una nueva clase de "Línea" (quizás con clases derivadas para A y B), en lugar de tratar las líneas como cadenas y decirles qué Representan desde el exterior. Me encantaría dar más detalles sobre esto en CodeReview
MattDavey
quisiste decir elif (line.startsWith("b"))? por cierto, puede eliminar de forma segura los paréntesis que rodean las condiciones, no son idiomáticos en Python.
tokland el

Respuestas:

14

Esta es una práctica extremadamente común y la forma de tratarla es a través de filtros de orden superior .

Esencialmente, pasa una función al método de filtro, junto con la lista / secuencia contra la que desea filtrar y la lista / secuencia resultante contiene solo aquellos elementos que desea.

No estoy familiarizado con la sintaxis de Python (aunque contiene una función como la que se ve en el enlace de arriba), pero en c # / f # se ve así:

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 # (se supone que no es numerable, de lo contrario se usaría List.filter):

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 */

Entonces, para ser claros: si usa códigos / patrones probados y probados, es un mal estilo. Eso, y al mutar la lista en la memoria de la forma en que pareces a través de clear_lines (), pierdes la seguridad del hilo y cualquier esperanza de paralelismo que podrías haber tenido.

Steven Evers
fuente
3
Como nota, la sintaxis de Python para esto sería una expresión generadora: (line for line in lines if line.startswith("a") or line.startswith("b")).
Latty
1
+1 por señalar que la implementación imperativa (innecesaria) de clear_lineses realmente una mala idea. En Python probablemente usaría generadores para evitar cargar el archivo completo en la memoria.
tokland el
¿Qué sucede cuando el archivo de entrada es más grande que la memoria disponible?
Blrfl
@Blrfl: bueno, si el término generador es consistente entre c # / f # / python, ¡entonces lo que @tokland y @Lattyware se traducen en c # / f # rendimiento y / o rendimiento! declaraciones. Es un poco más obvio en mi ejemplo de f # porque Seq.filter solo se puede aplicar a colecciones de IEnumerable <T> pero ambos ejemplos de código funcionarán si lineses una colección generada.
Steven Evers
@mcwise: cuando comienzas a mirar todas las otras funciones disponibles que funcionan de esta manera, comienza a ponerse realmente sexy e increíblemente expresivo porque todas pueden encadenarse y componerse juntas. Mira skip, take, reduce( aggregateen .NET), map( selecten .NET), y no hay más, pero eso es un comienzo muy sólido.
Steven Evers
14

Recientemente tuve que implementar un programador de firmware usando el formato S-record de Motorola , muy similar a lo que usted describe. Como teníamos cierta presión de tiempo, mi primer borrador ignoró las redundancias e hizo simplificaciones basadas en el subconjunto que realmente necesitaba usar en mi aplicación. Pasó mis pruebas fácilmente, pero falló mucho tan pronto como alguien más lo intentó. No había idea de cuál era el problema. Llegó hasta el final pero falló al final.

Así que no tuve más remedio que implementar todas las verificaciones redundantes, para reducir dónde estaba el problema. Después de eso, me tomó alrededor de dos segundos encontrar el problema.

Me llevó tal vez dos horas más hacerlo de la manera correcta, pero también desperdicié un día del tiempo de otras personas en la solución de problemas. Es muy raro que unos pocos ciclos de procesador valgan un día de desperdicio de solución de problemas.

Dicho esto, en lo que respecta a la lectura de archivos, a menudo es beneficioso diseñar su software para que funcione con la lectura y el procesamiento de una línea a la vez, en lugar de leer todo el archivo en la memoria y procesarlo en la memoria. De esa manera, seguirá funcionando en archivos muy grandes.

Karl Bielefeldt
fuente
"Es muy raro que unos pocos ciclos de procesador valgan un día de desperdicio de solución de problemas". Gracias por la respuesta, tienes un buen punto.
marktani
5

Puede plantear una excepción en el elsecaso. De esta manera no es redundante. Las excepciones no son cosas que no se supone que sucedan, pero que se verifican de todos modos.

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
fuente
Yo diría que esta última es una mala idea, ya que es menos explícita: si luego decide agregar una "c", podría ser menos clara.
Latty
La primera sugerencia tiene mérito ... la segunda (suponga "b") es una mala idea
Andrew
@Lattyware mejoré la respuesta. Gracias por tus comentarios.
Tulains Córdova
1
@ Andrew, mejoré la respuesta. Gracias por tus comentarios.
Tulains Córdova
3

En el diseño por contrato , uno adivina que cada función debe hacer su trabajo como se describe en su documentación. Por lo tanto, cada función tiene una lista de condiciones previas, es decir, condiciones en las entradas de la función, así como condiciones posteriores, es decir, condiciones de salida de la función.

La función debe garantizar a sus clientes que, si las entradas respetan las condiciones previas, la salida será como se describe en las condiciones posteriores. Si al menos una de las condiciones previas no se respeta, la función puede hacer lo que quiera (bloquearse, devolver cualquier resultado, ...). Por lo tanto, las condiciones previas y posteriores son una descripción semántica de la función.

Gracias al contrato, una función está segura de que sus clientes la usan correctamente y un cliente está seguro de que la función hace su trabajo correctamente.

Algunos idiomas manejan contratos de forma nativa oa través de un marco dedicado. Para los demás, lo mejor es verificar las condiciones previas y posteriores gracias a las afirmaciones, como dijo @Lattyware. Pero no llamaría a eso programación defensiva, ya que, en mi opinión, este concepto está más centrado en la protección contra las entradas (humanas) del usuario.

Si explota contratos, puede evitar la condición verificada de forma redundante ya que la función llamada funciona perfectamente y no necesita la doble verificación, o la función llamada es disfuncional y la función de llamada puede comportarse como lo desea.

La parte más difícil es definir qué función es responsable de qué y documentar estrictamente estos roles.

mgoeminne
fuente
1

En realidad, no necesita clear_lines () al comienzo. Si la línea no es "a" o "b", los condicionales simplemente no se dispararán. Si desea deshacerse de esas líneas, convierta el resto en clear_line (). Tal como está, está haciendo dos pases a través de su documento. Si omite clear_lines () al principio y lo hace como parte del bucle foreach, entonces reduce el tiempo de procesamiento a la mitad.

No es solo un mal estilo, es malo computacionalmente.

Ingeniero mundial
fuente
2
Puede ser que esas líneas se estén utilizando para otra cosa, y deben tratarse antes de tratar con las líneas "a"/ "b". No digo que sea probable (el nombre claro implica que se están descartando), solo que existe la posibilidad de que sea necesario. Si ese conjunto de líneas se repite repetidamente en el futuro, también podría valer la pena eliminarlas de antemano para evitar una gran cantidad de iteraciones sin sentido.
Latty
0

Si realmente quiere hacer algo si encuentra una cadena no válida (texto de depuración de salida, por ejemplo), entonces diría que está absolutamente bien. Un par de líneas adicionales y unos meses más adelante cuando deja de funcionar por alguna razón desconocida, puede mirar la salida para averiguar por qué.

Sin embargo, si es seguro simplemente ignorarlo, o si sabe con certeza que nunca obtendrá una cadena no válida, entonces no hay necesidad de una rama adicional.

Personalmente, siempre estoy por poner al menos una salida de rastreo para cualquier condición inesperada: hace la vida mucho más fácil cuando tienes un error con salida adjunta que te dice exactamente qué salió mal.

Bok McDonagh
fuente
0

... supongamos que hay un archivo de texto que contiene líneas que comienzan con "a", líneas que comienzan con "b" y otras líneas y en realidad solo quiero trabajar con los dos primeros tipos de líneas. Mi código se vería así (usando python, pero léelo como pseudocódigo):

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

Odio las if...then...elseconstrucciones. Evitaría todo el problema:

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