¿Deberían los programadores utilizar variables booleanas para "documentar" su código?

79

Estoy leyendo Code Complete de McConell , y él analiza el uso de variables booleanas para documentar su código. Por ejemplo, en lugar de:

if((elementIndex < 0) || (MAX_ELEMENTS < elementIndex) || 
   (elementIndex == lastElementIndex)){
       ...
}

Él sugiere:

finished = ((elementIndex < 0) || (MAX_ELEMENTS < elementIndex));
repeatedEntry = (elementIndex == lastElementIndex);
if(finished || repeatedEntry){
   ...
}

Esto me parece lógico, una buena práctica y muy autodocumentado. Sin embargo, dudo en empezar a utilizar esta técnica con regularidad, ya que casi nunca me he encontrado con ella; y quizás sería confuso solo por ser raro. Sin embargo, mi experiencia aún no es muy amplia, por lo que estoy interesado en escuchar la opinión de los programadores sobre esta técnica, y me gustaría saber si alguien usa esta técnica con regularidad o la ha visto con frecuencia al leer código. ¿Es esta una convención / estilo / técnica que vale la pena adoptar? ¿Lo entenderán y apreciarán otros programadores, o lo considerarán extraño?

froadie
fuente
@Paul R - Ups, gracias. Comencé con la autodocumentación y me di cuenta de que no había una etiqueta para eso, así que intenté cambiarla a autodocumentación, que ya existía. :)
froadie
¿No es mejor comentar lo que está sucediendo en la prueba?
JS
3
Cuando es apropiado, trato de hacer esto también, especialmente si tengo que verificar la (s) condición (es) en diferentes partes del método. También es mucho más descriptivo.
geffchang
"Esto me parece lógico, una buena práctica y muy autodocumentado". Basándose en su experiencia, es probable que otros programadores que lo encuentren por primera vez lo entiendan fácilmente.
2
¿Por qué sigue abierta esta pregunta?
orangepips

Respuestas:

55

Dividir una expresión que está demasiado anidada y complicada en subexpresiones más simples asignadas a variables locales, y luego juntas de nuevo, es una técnica bastante común y popular, independientemente de si las subexpresiones y / o la expresión general son booleanas o de casi cualquier otro tipo. Con nombres bien elegidos, una descomposición elegante de este tipo puede aumentar la legibilidad, y un buen compilador no debería tener problemas para generar un código equivalente a la complicada expresión original.

Algunos lenguajes que no tienen el concepto de "asignación" per se, como Haskell, incluso introducen construcciones especializadas para permitirle usar la técnica "dar un nombre a una subexpresión" (la wherecláusula en Haskell) - parece indicar algo popularidad de la técnica en cuestión! -)

Alex Martelli
fuente
6
si es más simple Y fácil de leer, digo que es un caso bastante claro de ganar-ganar :)
djcouchycouch
Estoy de acuerdo. En muchos casos, probablemente podría salirse con la suya con un breve comentario, pero no veo cómo esto realmente podría doler.
Max E.
16

Lo he usado, aunque normalmente envuelvo la lógica booleana en un método reutilizable (si se llama desde múltiples ubicaciones).

Ayuda a la legibilidad y cuando la lógica cambia, solo necesita cambiarse en un lugar.

Otros lo entenderán y no lo encontrarán extraño (excepto para aquellos que solo escriben funciones de mil líneas, es decir).

Oded
fuente
¡Gracias por la respuesta! Con respecto a los métodos reutilizables, eso tiene otra razón válida (no relacionada) para ser descartada ... Así que supongo que mi pregunta realmente es si debería descartar las expresiones booleanas de una sola vez, cuando no hay otra razón que no sea la legibilidad. (Lo cual, por supuesto, es una razón suficientemente importante en sí misma :)) Gracias por señalar eso.
froadie
+1 por reducir los cambios de código necesarios cuando cambia la lógica y burlarse de los programadores de funciones de mil líneas.
Jeffrey L Whitledge
9

Intento hacerlo siempre que sea posible. Seguro, está utilizando una "línea adicional" de código, pero al mismo tiempo, está describiendo por qué está haciendo una comparación de dos valores.

En su ejemplo, miro el código y me pregunto "¿por qué la persona que ve que el valor es menor que 0?" En el segundo me estás diciendo claramente que algunos procesos han finalizado cuando esto ocurre. Sin adivinar en el segundo cuál era tu intención.

El más importante para mí es cuando veo un método como: DoSomeMethod(true); ¿Por qué se establece automáticamente en verdadero? Es mucho más legible como

bool deleteOnCompletion = true;

DoSomeMethod(deleteOnCompletion);
kemiller2002
fuente
7
No me gustan los parámetros booleanos precisamente por esta razón. Terminas recibiendo llamadas como "createOrder (true, false, true, true, false)" y ¿qué significa eso? Prefiero usar enum's, entonces dices algo como "createOrder (Source.MAIL_ORDER, BackOrder.NO, CustomOrder.CUSTOM, PayType.CREDIT)".
Jay
Pero si sigues el ejemplo de Kevin, es equivalente al tuyo. ¿Qué diferencia hay si la variable puede tomar 2 valores o más de 2?
Mark Ruzon
2
Con Jay's puede tener la ventaja de ser definitivamente más claro en ciertos casos. Por ejemplo, en el uso de PayType. Si fuera un booleano, el parámetro probablemente se llamaría isPayTypeCredit. No sabes cuál es la alternativa. Con una enumeración puede ver claramente qué opciones es el PayType: Crédito, Cheque, Efectivo y seleccione la correcta.
kemiller2002
++ también una enumeración no permite la asignación nula, por lo que el control de valor está literalmente completo, y la documentación automática de la enumeración es salsa en la parte superior
Hardryv
Objective-C y Smalltalk realmente resuelven este problema, en Objective-C:[Object createOrderWithSource:YES backOrder:NO custom:YES type:kCreditCard];
Grant Paul
5

La muestra proporcionada:

finished = ((elementIndex < 0) || (MAX_ELEMENTS < elementIndex));
repeatedEntry = (elementIndex == lastElementIndex);
if(finished || repeatedEntry){
   ...
}

También se puede reescribir para usar métodos, que mejoran la legibilidad y preservan la lógica booleana (como señaló Konrad):

if (IsFinished(elementIndex) || IsRepeatedEntry(elementIndex, lastElementIndex)){
   ...
}

...

private bool IsFinished(int elementIndex) {
    return ((elementIndex < 0) || (MAX_ELEMENTS < elementIndex));
}

private bool IsRepeatedEntry(int elementIndex, int lastElementIndex) {
    return (elementIndex == lastElementIndex);
}

Tiene un precio, por supuesto, que son dos métodos adicionales. Si hace esto mucho, puede hacer que su código sea más legible, pero sus clases menos transparentes. Pero, de nuevo, también puede mover los métodos adicionales a clases auxiliares.

Prutswonder
fuente
Si se metió en mucho ruido de código con C #, también puede aprovechar las clases parciales y mover el ruido al parcial y, si la gente está interesada en lo que IsFinished está comprobando, es fácil saltar.
Chris Marisic
3

La única forma en que podría ver que esto va mal es si el fragmento booleano no tiene un nombre que tenga sentido y se elige un nombre de todos modos.

//No clue what the parts might mean.
if(price>0 && (customer.IsAlive || IsDay(Thursday)))

=>

first_condition = price>0
second_condition =customer.IsAlive || IsDay(Thursday)

//I'm still not enlightened.
if(first_condition && second_condition)

Señalo esto porque es común hacer reglas como "comentar todo tu código", "usar booleanos con nombre para todos los criterios if con más de 3 partes" solo para obtener comentarios que están semánticamente vacíos del siguiente tipo

i++; //increment i by adding 1 to i's previous value
MatthewMartin
fuente
2
Ha agrupado incorrectamente las condiciones en su ejemplo, ¿no es así? '&&' se une más fuerte que '||' en la mayoría de los lenguajes que los utilizan (los scripts de shell son una excepción).
Jonathan Leffler
Parens agregó. Entonces, este sería otro ataque contra la división en variables con nombre, presenta una oportunidad para cambiar la agrupación de una manera no obvia.
MatthewMartin
2

Al hacer esto

finished = ((elementIndex < 0) || (MAX_ELEMENTS < elementIndex));
repeatedEntry = (elementIndex == lastElementIndex);
if(finished || repeatedEntry){
   ...
}

eliminas la lógica de tu cerebro y la pones en el código. Ahora el programa sabe lo que quiso decir.
Siempre que nombras algo, le das una representación física . Existe.
Puedes manipularlo y reutilizarlo.

Incluso puede definir todo el bloque como un predicado:

bool ElementBlahBlah? (elementIndex, lastElementIndex);

y hacer más cosas (más adelante) en esa función.

Nick Dandoulakis
fuente
Y lo que es más importante, el próximo desarrollador que mire el código también sabrá lo que quisiste decir. Esta es una gran práctica y la hago todo el tiempo.
Chris Thornton
1
Además, el próximo desarrollador a menudo puede ser usted mismo, mirando el código nuevamente después de unos meses (o incluso algunas semanas) y se alegra de que esté bien documentado.
Louis
2

Si la expresión es compleja, la muevo a otra función que devuelve un boolejemplo, isAnEveningInThePubAGoodIdea(dayOfWeek, sizeOfWorkLoad, amountOfSpareCash)o reconsidero el código para que no se requiera una expresión tan compleja.

Benedict Cohen
fuente
2

Recuerda que de esta forma calculas más de lo necesario. Debido a que se eliminan las condiciones del código, siempre se calculan ambas (sin cortocircuito).

Así que eso:

if((elementIndex < 0) || (MAX_ELEMENTS < elementIndex) || 
   (elementIndex == lastElementIndex)){
   ...
}

Después de la transformación se convierte en:

if((elementIndex < 0) || (MAX_ELEMENTS < elementIndex) |
   (elementIndex == lastElementIndex)){
   ...
}

No es un problema en la mayoría de los casos, pero aún en algunos puede significar un peor rendimiento u otros problemas, por ejemplo, cuando en la segunda expresión asume que la primera ha fallado.

Konrad Garus
fuente
Es cierto: me estaba dando cuenta de esto ahora cuando volví a parte de mi código para refactorizar un par de declaraciones if usando este método. Había una declaración if aprovechando la evaluación de cortocircuito con un && (la segunda parte arroja un NPE si la primera parte es falsa), y mi código fallaba cuando refactoré esto (porque siempre estaba evaluando ambos y almacenándolos en booleano variables). Buen punto, gracias! Pero me preguntaba mientras intentaba esto: ¿hay alguna forma de almacenar la lógica en una variable y hacer que demore la evaluación hasta la declaración if real?
froadie
2
De hecho, dudo que el compilador lo resuelva. Si la segunda llamada no es efectiva, generalmente se debe a la llamada de alguna función, y AFAIK ningún compilador está tratando de determinar si una función llamada no tiene efectos secundarios.
JS
Puede anidar los IF y no hacer los cálculos posteriores a menos que la primera prueba sea insuficiente para decidir si continuar.
Jay
@froadie Algunos lenguajes como Kotlin (y pronto Dart) permiten variables "perezosas" que solo se calcularán cuando se utilicen. Alternativamente, poner la lógica en una función en lugar de una variable tendrá el mismo efecto aquí. Solo, ya sabes, en caso de que todavía quieras saberlo 10 años después.
hacker1024
2

Creo que es mejor crear funciones / métodos en lugar de variables temporales. De esta manera, la legibilidad aumenta también porque los métodos se acortan. El libro Refactoring de Martin Fowler tiene buenos consejos para mejorar la calidad del código. Las refactorizaciones relacionadas con su ejemplo particular se denominan "Reemplazar temperatura con consulta" y "Método de extracción".

mkj
fuente
2
¿Estás diciendo que al saturar el espacio de clases con muchas funciones únicas, estás aumentando la legibilidad? Por favor explique.
Zano
Siempre es una compensación. Mejorará la legibilidad de la función original. Si la función original es corta, puede que no valga la pena.
mkj
También "saturar el espacio de clases" es algo que creo que depende del lenguaje utilizado y de cómo particione su código.
mkj
2

Personalmente, creo que esta es una gran práctica. Su impacto en la ejecución del código es mínimo, pero la claridad que puede proporcionar, si se usa correctamente, es invaluable cuando se trata de mantener el código más adelante.

Perro gruñido
fuente
1

si el método requiere una notificación de éxito: (ejemplos en c #) Me gusta usar el

bool success = false;

para empezar. el código es falso hasta que lo cambio a:

success = true;

luego al final:

return success;
Chris Hayes
fuente
0

Creo que depende del estilo que prefiera usted o su equipo. La refactorización "Introducir variable" podría ser útil, pero a veces no :)

Y debería estar en desacuerdo con Kevin en su publicación anterior. Su ejemplo, supongo, utilizable en caso de que se pueda cambiar la variable introducida, pero introducirla solo para un booleano estático es inútil, porque tenemos el nombre del parámetro en una declaración de método, entonces, ¿por qué duplicarlo en el código?

por ejemplo:

void DoSomeMethod(boolean needDelete) { ... }

// useful
boolean deleteOnCompletion = true;
if ( someCondition ) {
    deleteOnCompletion = false;
}
DoSomeMethod(deleteOnCompletion);

// useless
boolean shouldNotDelete = false;
DoSomeMethod(shouldNotDelete);
dchekmarev
fuente
0

En mi experiencia, a menudo volví a algunos guiones antiguos y me preguntaba '¿qué diablos estaba pensando en ese entonces?'. Por ejemplo:

Math.p = function Math_p(a) {
    var r = 1, b = [], m = Math;
    a = m.js.copy(arguments);
    while (a.length) {
        b = b.concat(a.shift());
    }
    while (b.length) {
        r *= b.shift();
    }
    return r;
};

que no es tan intuitivo como:

/**
 * An extension to the Math object that accepts Arrays or Numbers
 * as an argument and returns the product of all numbers.
 * @param(Array) a A Number or an Array of numbers.
 * @return(Number) Returns the product of all numbers.
 */
Math.product = function Math_product(a) {
    var product = 1, numbers = [];
    a = argumentsToArray(arguments);
    while (a.length) {
        numbers = numbers.concat(a.shift());
    }
    while (numbers.length) {
        product *= numbers.shift();
    }
    return product;
};
rolandog
fuente
-1. Esto tiene un poco que ver con la pregunta original, para ser honesto. La pregunta es sobre algo diferente y muy específico, no sobre cómo escribir un buen código en general.
P Shved el
0

Rara vez creo variables separadas. Lo que hago cuando las pruebas se complican es anidar los IF y agregar comentarios. Me gusta

boolean processElement=false;
if (elementIndex < 0) // Do we have a valid element?
{
  processElement=true;
}
else if (elementIndex==lastElementIndex) // Is it the one we want?
{
  processElement=true;
}
if (processElement)
...

El defecto admitido de esta técnica es que el próximo programador que se presente puede cambiar la lógica pero no se molestará en actualizar los comentarios. Supongo que es un problema general, pero muchas veces he visto un comentario que dice "validar la identificación del cliente" y la siguiente línea es examinar el número de pieza o algo así y me pregunto dónde está el cliente. entra id.

Arrendajo
fuente