Compuesto de estilo Y / O si las declaraciones

13

¿Cómo diseñas las declaraciones compuestas complejas AND / OR if para una máxima legibilidad? ¿Cómo sangras y dónde colocas los saltos de línea? Mi situación particular es algo como lo siguiente. Definitivamente es mejor que romper todo en una línea, pero aún se ve desordenado.

if (
    (
        x == y
        && a != b
        && p.isGood() 
        && (
            i + u == b
            || q >= a
        )
    )
    || k.isSomething()
    || m > n
) {
    doSomething();
}
JoJo
fuente
1
¿La sangría tonta y la estructura de paréntesis / refuerzo son intencionales o parte del estilo?
Ed S.
Gracioso. Hice esta misma pregunta en SO hace una semana y se cerró. Me alegra ver esta pregunta viva en alguna parte.
Eric Belair

Respuestas:

6

Haga variables booleanas para cada pequeño paso:

bool step1 = i + u == b || q >= a;
bool step2 = a != b && p.isGood() && group1;
bool step3 = group2 || k.isSomething() || m > n;
if (step3) { doSomething(); }

Por supuesto, esto es similar a la respuesta de Lacrymology, excepto con diferentes nombres para cada paso.

Si nombra step1, step2y step3de manera que tenga un buen sentido conceptual, este debería ser, con mucho, el más legible. p.isGood()y a k.isSomething()veces puede invocarse en situaciones en las que no estaría en su código original, por lo que esta no sería una opción si esas funciones son caras o si está ejecutando este código en un ciclo muy cerrado.

Por otro lado, no necesita preocuparse por el impacto en el rendimiento en el que podría incurrir la creación de nuevas variables; un buen compilador los optimizará.

Un ejemplo con la detección de colisión rectangular (que probablemente no usaría debido al impacto de rendimiento mencionado anteriormente):

if((a.x + a.width >= b.x || b.x + b.width >= a.x)
 && (a.y + a.height >= b.y || b.y + b.width >= a.y)
)
{ collision(); }

Podría convertirse:

bool horizMatch = a.x + a.width >= b.x || b.x + b.width >= a.x;
bool vertMatch = a.y + a.height >= b.y || b.y + b.width >= a.y;
if(horizMatch && vertMatch) { collision(); }

Además, si quieres dejar tu código como está, creo que eso también estaría totalmente bien. Sinceramente, creo que su código es bastante legible. Obviamente no sé qué a b x y i u p k m nson exactamente , pero en lo que respecta a la estructura, me parece bien.

Rei Miyasaka
fuente
8

Por lo general, vuelvo a factorizar mi código para que sea más modular si mis condicionales se vuelven tan complicados.

JohnFx
fuente
Evitaría la refractación en esta situación. Sería una tontería probar funciones tan pequeñas de forma aislada, y tener las definiciones colgando fuera de la función hace que el código sea menos evidente.
Rei Miyasaka
Depende de qué tan bien refactorice. Yo diría que hace que su código sea MUCHO MÁS evidente al tener nombres de funciones significativos en lugar de una cadena de condicionales que parece un libro de texto de álgebra arrojado en su código.
JohnFx
Sin embargo, visualmente, tendrías tus funciones colgando afuera, y no sería inmediatamente obvio qué hace exactamente la función. Es momentáneamente una caja negra hasta que te desplazas hacia arriba. A menos que esté en un lenguaje que permita funciones en funciones, no creo que sea muy conveniente para el lector o el escritor, independientemente de qué tan bien refractor. Y si está en un lenguaje que permite funciones en funciones, entonces es probable que la sintaxis no sea muy diferente de declarar un enlace o una variable, por ejemplo, let x = a > bo let f a b = a > b.
Rei Miyasaka
Las variables funcionarían igualmente bien. Consideraría esa refactorización también.
JohnFx
Ah bien.
Rei Miyasaka
8

Haría algo más como esto, a este nivel de complejidad

bool doIt = x == y && a != b && p.isGood();
doIt &= ( i + u == b || q >= a);
doIt |= k.isSomething() || m > n;

if(doIt)
{
    doSomething();
}

es feo, pero es legible y estoy bastante seguro de que el compilador sabrá cómo refactorizarlo.

Por otro lado, si alguna vez me veo en la situación de escribir una declaración IF de este tipo, vuelvo a pensar la solución, porque estoy SEGURO de que hay una manera de hacerlo más simple, o al menos abstrayendo parte de esa condición (por ejemplo: tal vez x == y && a != b && p.isGood()realmente solo significa this->isPolygon()y puedo hacer ese método;

Lacrymology
fuente
4

Me obsesiono menos con la alineación vertical con el tiempo, pero mi forma general con expresiones de varias líneas es ...

if (   (   (expr1 == expr2)
        || (expr3 == expr4)
        || (expr5 == expr6)
       )
    && (   (expr7 == expr8)
        || (expr9 == expra)
       )
   )
{
  blah;
}

Puntos clave...

  • Los parens cercanos se alinean verticalmente con los parens abiertos, como con las llaves.
  • Las subexpresiones que caben dentro de una línea están dentro de una línea y están alineadas verticalmente a la izquierda. Donde ayuda a la legibilidad, los operadores de infijo dentro de esas partes de una sola línea también están alineados verticalmente.
  • Las llaves de cierre crean naturalmente líneas casi en blanco, lo que ayuda a agrupar visualmente las cosas.

A veces, formateo +y / *o algunos otros operadores como este también. Algunas expresiones complejas toman una forma de suma de producto o producto de suma (que puede referirse a "sumas" y "productos" booleanos) por lo que probablemente sea lo suficientemente común que valga la pena usar un estilo consistente.

Pero ten cuidado con esto. A menudo es mejor refactorizar (mover partes de la expresión a una función, o calcular y almacenar partes intermedias en una variable) en lugar de usar sangría para tratar de hacer que una expresión sobrecompleja sea más legible.

Si prefieres apilar a tus parientes cercanos en el lado derecho, no lo odio , pero supongo que no es tan malo. Sin embargo, llevado demasiado lejos, corre el riesgo de que un error pueda dejar la sangría tergiversando lo que hacen los paréntesis.

if (   (   (expr1 == expr2)
        || (expr3 == expr4)
        || (expr5 == expr6))

    && (   (expr7 == expr8)
        || (expr9 == expra)))
{
  blah;
}
Steve314
fuente
+1 Me gusta tu estilo. Respondió mi pregunta directamente, pero creo que Rei Miyasaka clavó la raíz del problema. Si alguna vez soy demasiado flojo para hacer el método de Rei, usaré tu estilo.
JoJo
Wow, esto es realmente agradable.
Rei Miyasaka
1

http://www.codinghorror.com/blog/2006/01/flattening-arrow-code.html

Estoy de acuerdo con la respuesta de JohnFx y con la de Lacrymology. Construiría un montón de funciones (preferiblemente estáticas) que cumplan objetivos pequeños y luego los desarrolle de manera inteligente.

Entonces, ¿qué tal algo como esto? Tenga en cuenta que esta no es la solución perfecta, pero funciona. Hay formas de limpiar esto aún más, pero se necesita información más específica. Nota: este código debe ejecutarse igual de rápido, ya que el compilador es inteligente.

// Currently based on members or global vars
// (which is often a bad idea too)
function doSomethingCondirionally()
{
  if (k.isSomething() || m > n)
  {
    doSomething();
    return;
  }

  // Else ... 
  if (x != y) return;
  if (a == b) return;
  if (!p.isGood()) return;

  // Final, positive check
  if (i + u == b || q >= a)
  {
    doSomething();
  }
}
Trabajo
fuente
Si tener un solo punto de salida de una función es algo que valora (por ejemplo, si está en un lenguaje funcional), esta podría no ser la mejor opción, o incluso una disponible. Dicho esto, sí, esto es lo que hago a menudo.
Rei Miyasaka
¿Qué pasa si es un lenguaje interpretado como Javascript?
JoJo
@ Rei Miyasaka, cuánto lo valoro depende de un idioma. Si bien me gusta la familia de idiomas LISP, no he tenido que usar uno en el trabajo. Si tengo que re-factorizar la función de otra persona pero no toco ningún otro código (a menudo una realidad), entonces haría algo como lo anterior. Si puedo escribir / reescribir esta lógica desde cero, entonces mi enfoque será diferente, pero no puedo escribir ese código sin tener un ejemplo específico de lo que el autor está tratando de hacer aquí.
Trabajo
1
@Rei Miyasaka, esa persona podría ser un genio o estar lleno de basura. No lo sé todo, pero sería curioso saber la defensa de esa persona del punto de salida única. Hay una discusión sobre eso aquí y en SO, y la impresión que tuve fue que este enfoque ha sido popular entre los académicos en los años 80 tal vez, pero ya no importa, y de hecho puede dificultar la legibilidad. Por supuesto, si está haciendo todo en el estilo funcional de LINQ, entonces este problema ni siquiera surgiría.
Trabajo
2
@Job @Steve Creo que es una guía más importante en los idiomas que requieren una asignación explícita. Obviamente no para todas las funciones, pero probablemente sea un hábito que los programadores principiantes fueron alentados a mantener para evitar olvidar liberar recursos.
Rei Miyasaka
1

Por lo que vale, me sorprendió ver que su ejemplo se parece mucho a los predicados complicados que he escrito. Estoy de acuerdo con otros en que un predicado complicado no es el mejor para la mantenibilidad o la legibilidad, pero a veces surgen.

Permítanme enfatizar que hicieron esta parte correctamente: && a != b NUNCA coloque un conector lógico al final de una línea, es demasiado fácil pasarlo por alto visualmente. Otro lugar donde NUNCA debe colocar un operador al final de la línea es en la concatenación de cadenas, en idiomas con dicho operador.

Hacer esto:

String a = b
   + "something"
   + c
   ;

No hagas esto:

String a = b +
   "something" +
   c;
Bruce Ediger
fuente
¿Tiene alguna lógica o estudios que respalden su afirmación para los conectores lógicos, o es simplemente su preferencia declarada como un hecho? Lo he escuchado mucho en varios lugares y nunca lo he entendido (a diferencia de los condicionales yoda, que tienen una razón válida [si está equivocada]).
Caleb Huitt - cjhuitt
@Caleb: las matemáticas se han compuesto de esa manera durante siglos. Al descremar el código, nos enfocamos en el lado izquierdo de cada línea. Obviamente, las líneas que comienzan con un operador son una continuación de la línea anterior y no una nueva declaración con sangría incorrecta.
Kevin Cline
También me gusta el prefijo de operadores matemáticos. Me di cuenta demasiado tarde en mi carrera de programación :)
JoJo
0

Si el condicional es tan complicado, generalmente es una indicación de que debe dividirse en partes. Quizás se pueda asignar una cláusula a una variable intermedia. Quizás una cláusula puede convertirse en un método auxiliar. Generalmente prefiero no tener tantos ands y ors en una línea.

Zhehao Mao
fuente
0

Puede dividir el código en varias declaraciones, lo que facilita su comprensión. Pero un verdadero ninja haría algo como esto. :-)

if
(
    (
        x == y
    &&
        a != b
    &&
        p.isGood()
    &&
        (
            i + u == b
        ||
            q >= a
        )
    )
||
    k.isSomething()
||
    m > n
)
{
    doSomething();
}
Daniel Lubarov
fuente
55
Soy fanático de los espacios en blanco, pero esto está excesivamente relleno con líneas casi en blanco para mi gusto.
Steve314