¿Deberían los programadores usar variables booleanas para "documentar" su código?
conventions self-documenting (14)
Estoy leyendo el Código Completo de McConell y él discute el uso de variables booleanas para documentar tu 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 auto-documentado. Sin embargo, dudo en comenzar a utilizar esta técnica con regularidad ya que casi nunca me he encontrado con ella; y tal vez sería confuso solo en virtud de ser raro. Sin embargo, mi experiencia no es muy amplia todavía, por lo que me interesa 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 si la ha visto a menudo al leer el código. ¿Es esta una convención / estilo / técnica que vale la pena adoptar? ¿Los otros programadores lo entenderán y lo apreciarán, o lo considerarán extraño?
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 quieres decir.
Cuando nombras algo, le das representación física . Existe.
Puedes manipularlo y reutilizarlo.
Incluso puedes definir el bloque completo como un predicado:
bool ElementBlahBlah? (elementIndex, lastElementIndex);
y hacer más cosas (más adelante) en esa función.
Creo que depende del estilo que tu / tu equipo prefiera. 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 el caso, cuando se puede 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);
Creo que es mejor crear funciones / métodos en lugar de variables temporales. De esta forma, 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".
Dividir una expresión que está demasiado anidada y complicada en sub-expresiones más simples asignadas a variables locales, y luego juntas nuevamente, es una técnica bastante común y popular, independientemente de si las expresiones parciales y / o la expresión general son booleanas o de casi cualquier otro tipo. Con nombres bien elegidos, una descomposición de buen gusto de este tipo puede aumentar la legibilidad, y un buen compilador no debería tener problemas para generar código que sea equivalente a la expresión original y complicada.
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 cláusula where
en Haskell) - parece indicar algo de popularidad por la técnica en cuestión! -)
En mi experiencia, a menudo volví a algunos guiones antiguos y me preguntaba ''¿en qué demonios estaba pensando 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;
};
Intento hacerlo siempre que sea posible. Claro, estás usando una "línea adicional" de código, pero al mismo tiempo, estás describiendo por qué estás haciendo una comparación de dos valores.
En su ejemplo, miro el código y me pregunto "¿por qué la persona que ve el valor es menor a 0?" En el segundo, me estás diciendo claramente que algunos procesos han terminado cuando esto ocurre. Sin adivinar en el segundo cuál era tu intención.
El más grande 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);
La única forma en que pude ver que esto iba mal es si el fragmento booleano no tiene un nombre que tenga sentido y de todos modos se elige un nombre.
//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 nombres booleanos para todos los criterios 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
La muestra provista:
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 haces esto mucho, puede hacer que tu código sea más legible, pero tus clases menos transparentes. Pero, de nuevo, también puedes mover los métodos extra a clases auxiliares.
Lo he usado, aunque normalmente envolviendo la lógica booleana en un método reutilizable (si se llama desde múltiples ubicaciones).
Ayuda a la lectura y cuando la lógica cambia, solo necesita cambiarse en un solo lugar.
Otros lo entenderán y no lo encontrarán extraño (excepto para aquellos que solo escriben mil funciones de línea, eso es).
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 tarde.
Raramente creo variables separadas. Lo que hago cuando las pruebas se complican es anidar las 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 moleste en actualizar los comentarios. Supongo que es un problema general, pero he tenido muchas veces que he visto un comentario que dice "validar la identificación del cliente" y la siguiente línea está examinando el número de parte o algo así y me pregunto dónde está el cliente. id entra.
Recuerde que de esta manera calcula más de lo necesario. Debido a las condiciones de salida del código, siempre se calculan ambos (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 2da expresión asume que el 1er ha fallado.
Si la expresión es compleja, la muevo a otra función que devuelve un bool
, por ejemplo, isAnEveningInThePubAGoodIdea(dayOfWeek, sizeOfWorkLoad, amountOfSpareCash)
o reconsidera el código para que no se requiera una expresión tan compleja.
si el método requiere una notificación de éxito: (ejemplos en c #) Me gusta usar el
bool success = false;
para comenzar el código está falure hasta que lo cambie a:
success = true;
luego al final:
return success;