Escribir código elegante y legible
Spanish (Español) translation by Elías Nicolás (you can also view the original English article)
En este tutorial, le daremos nueve técnicas prácticas para escribir código elegante y legible. No hablaremos de arquitecturas, lenguajes o plataformas específicas. El enfoque está en escribir mejor código. Empecemos.
"Medir el progreso de la programación por líneas de código es como medir el progreso de la construcción de aviones por peso". - Bill Gates
Introducción
Si eres un desarrollador, es probable que haya habido ocasiones en las que hayas escrito un código y, después de unos días, semanas o meses, lo miraste y te dijiste "¿Qué hace esta pieza de código?" La respuesta a esa pregunta podría haber sido "¡Realmente no lo sé!" En ese caso, lo único que puede hacer es revisar el código de principio a fin, tratando de entender lo que estaba pensando cuando lo escribió.
Esto ocurre principalmente cuando somos perezosos y solo queremos implementar esa nueva característica que el cliente solicitó. Solo queremos hacer el trabajo con el menor esfuerzo posible. Y cuando funciona, no nos importa el código en sí, porque el cliente nunca verá la horrible verdad, y mucho menos lo entenderá. ¿Verdad? Incorrecto. En estos días, colaborar en el software se ha convertido en el valor predeterminado y las personas verán, leerán e inspeccionarán el código que usted escribe. Incluso si su código no es examinado por sus colegas, debería convertirlo en un hábito de escribir código claro y legible. Siempre.
La mayoría de las veces, no trabajas solo en un proyecto. Con frecuencia vemos código feo con variables que tienen nombres como i, a, p, pro y rqs. Y si realmente se pone malo, este patrón es visible en todo el proyecto. Si esto suena familiar, entonces estoy bastante seguro de que te has preguntado: "¿Cómo puede esta persona escribir un código como este?" Por supuesto, esto te hace más agradecido cuando te encuentras con un código claro, legible e incluso hermoso. El código claro y limpio se puede leer en segundos y puede ahorrarle a usted y a sus colegas mucho tiempo. Esa debe ser su motivación para escribir código de calidad.
1. Fácil de entender
Todos estamos de acuerdo en que el código debe ser fácil de entender. ¿Derecha? El primer ejemplo se centra en el espaciado. Veamos dos ejemplos.
1 |
return gender == "1" ? weight * (height / 10) : weight * (height * 10); |
1 |
if(gender == "1"){ |
2 |
return weight * (height / 10); |
3 |
} else { |
4 |
return weight * (height * 10); |
5 |
}
|
Aunque el resultado de estos ejemplos es idéntico, se ven muy diferentes. ¿Por qué debería usar más líneas de código si puede escribir menos? Vamos a explorar otros dos ejemplos, algo que apuesto a que ves con frecuencia.
1 |
for (Node* node = list->head; node != NULL; node = node->next) |
2 |
print(node->data); |
1 |
Node* node = list->head; |
2 |
|
3 |
if(node == NULL) return; |
4 |
|
5 |
while(node->next != NULL) { |
6 |
Print(node->data); |
7 |
node = node->next; |
8 |
}
|
9 |
|
10 |
if(node != NULL) Print(node->data); |
De nuevo, el resultado de estos ejemplos es idéntico. ¿Cuál es mejor? ¿Y por qué? ¿Menos líneas de código significan mejor código? Volveremos a examinar esta pregunta más adelante en este tutorial.
2. ¿Es más pequeño siempre mejor?
En informática, a menudo escuchas la frase "menos es más". En términos generales, si puede resolver un problema en menos líneas de código, mejor. Probablemente le llevará menos tiempo entender una clase de 200 líneas que una clase de 500 líneas. Sin embargo, ¿es esto siempre cierto? Echa un vistazo a los siguientes ejemplos.
1 |
reservation((!room = FindRoom(room_id))) || !room->isOccupied()); |
1 |
room = FindRoom(room_id); |
2 |
if(room != NULL) |
3 |
reservation(!room->isOccupied()); |
¿No estás de acuerdo en que el segundo ejemplo es más fácil de leer y entender? Tienes que ser capaz de optimizar para la legibilidad. Por supuesto, puede agregar algunos comentarios al primer ejemplo para que sea más fácil de entender, pero ¿no es mejor omitir los comentarios y escribir un código que sea más fácil de leer y entender?
1 |
// Determine where to spawn the monster along the Y axis
|
2 |
CGSize winSize = [CCDirector sharedDirector].winSize; |
3 |
int minY = monster.contentSize.width / 2; |
4 |
int maxY = winSize.width - monster.contentSize.width/2; |
5 |
int rangeY = maxY - minY; |
6 |
int actualY = (arc4random() % rangeY) + minY; |
3. Nombrar
Elegir nombres descriptivos para cosas como variables y funciones es un aspecto clave para escribir código legible. Ayuda tanto a tus colegas como a ti mismo a comprender rápidamente el código. Nombrar una variable tmp no le dice nada más que que la variable es temporal por alguna razón, lo cual no es más que una suposición educada. No indica si la variable almacena un nombre, una fecha, etc.
Otro buen ejemplo es nombrar un método stop. No es un mal nombre en sí mismo, pero eso realmente depende de la implementación del método. Si realiza una operación peligrosa que no se puede deshacer, es posible que desee cambiarle el nombre para kill o pause si se puede reanudar la operación. ¿Tienes la idea?
Si estás trabajando con una variable para el peso de las papas, ¿por qué lo llamarías tmp? Cuando vuelva a visitar ese fragmento de código unos días después, no recordará para qué se utiliza tmp.
No estamos diciendo que tmp sea un mal nombre para una variable, porque hay veces en que tmp es perfectamente razonable como nombre de variable. Eche un vistazo al siguiente ejemplo en el que tmp no es una mala elección.
1 |
tmp = first_potato; |
2 |
first_potato = second_potato; |
3 |
second_potato = tmp; |
En el ejemplo anterior, tmp describe lo que hace, almacena temporalmente un valor. No se pasa a una función o método, y no se incrementa o modifica. Tiene una vida útil bien definida y ningún desarrollador experimentado será rechazado por el nombre de la variable. A veces, sin embargo, es simplemente la pereza. Echa un vistazo al siguiente ejemplo.
1 |
NSString *tmp = user.name; |
2 |
tmp += " " + user.phone_number; |
3 |
tmp += " " + user.email; |
4 |
...
|
5 |
[template setObject:tmp forKey:@"user_info"]; |
Si tmp almacena la información del usuario, ¿por qué no se llama userInfo? El nombre correcto de variables, funciones, métodos, clases, etc. es importante al escribir código legible. No solo hace que su código sea más legible, sino que también le ahorrará tiempo en el futuro.
4. Añadir significado a los nombres
Como vimos en el consejo anterior, es importante elegir los nombres con inteligencia. Sin embargo, es igualmente importante agregar un significado a los nombres que usa para las variables, funciones, métodos, etc. Esto no solo ayuda a evitar confusiones, sino que hace que el código que escriba sea más fácil de entender. Elegir un nombre que tenga sentido es casi como agregar metadatos a una variable o método. Elija nombres descriptivos y evite los genéricos. La palabra add, por ejemplo, no siempre es ideal como se puede ver en el siguiente ejemplo.
1 |
bool addUser(User u){ |
2 |
...
|
3 |
}
|
No está claro qué debe hacer addUser. ¿Agrega un usuario a una lista de usuarios, a una base de datos o a una lista de personas invitadas a una fiesta? Compare esto con registerUser o signupUser. Esto tiene más sentido. ¿Derecha? Eche un vistazo a la siguiente lista para tener una mejor idea de a qué nos dirigimos.
| Palabra | Sinónimo | ||||
| hacer | ejecutar, crear, componer, agregar | inicio | lanzar, crear, comenzar, abrir | explotar | detonar, arrancar, reventar |
5. Largo de Nombre
A muchos programadores no les gustan los nombres largos, porque son difíciles de recordar y engorrosos de escribir. Por supuesto, un nombre no debe ser ridículamente largo como newClassForNavigationControllerNamedFirstViewController. Esto es difícil de recordar y simplemente hace que su código sea feo e ilegible.
Como vimos anteriormente, lo opuesto, los nombres cortos, tampoco son buenos. ¿Cuál es el tamaño correcto para una variable o nombre de método? ¿Cómo se decide entre nombrar una variable len, length o user_name_length? La respuesta depende del contexto y la entidad a la que está vinculado el nombre.
Los nombres largos ya no son un problema cuando se utiliza un IDE moderno (Entorno de desarrollo integrado). La finalización del código le ayuda a evitar los errores tipográficos y también hace sugerencias para que recordar nombres sea menos doloroso.
Puede usar nombres cortos (er) si la variable es local. Además, se recomienda usar nombres más cortos para las variables locales para mantener su código legible. Echa un vistazo al siguiente ejemplo.
1 |
NSString *link = [[NSString alloc] initWithFormat:@"https://localhost:8080/WrittingGoodCode/resources/GoodCode/getGoodCode/%@",idCode]; |
2 |
NSURL *infoCode = [NSURL URLWithString:link]; |
6. Nombrando Booleanos
Los booleanos pueden ser difíciles de nombrar, ya que pueden tener un significado diferente según la forma en que lea o interprete el nombre. En el siguiente fragmento de código, read_password puede significar que el programa ha leído la contraseña, pero también puede significar que el programa debe leer la contraseña.
1 |
BOOL readPassword = YES; |
Para evitar este problema, puede cambiar el nombre del booleano anterior a didReadPassword para indicar que la contraseña ha sido leída o shouldReadPassword para mostrar que el programa necesita leer la contraseña. Esto es algo que se ve mucho en Objective-C, por ejemplo.
7. Comentar o no comentar
Agregar comentarios al código es importante, pero es igualmente importante usarlos con moderación. Deben utilizarse para ayudar a alguien a entender su código. Sin embargo, leer los comentarios también lleva tiempo y si un comentario no agrega mucho valor, entonces ese tiempo se pierde. El siguiente fragmento de código muestra cómo no usar los comentarios.
1 |
// This happens when memory warning is received
|
2 |
- (void)didReceiveMemoryWarning { |
3 |
[super didReceiveMemoryWarning]; |
4 |
// Dispose of any resources that can be recreated.
|
5 |
}
|
6 |
|
7 |
// This validate the fields
|
8 |
-(BOOL)validateFields { |
9 |
|
10 |
}
|
¿Te son útiles estos fragmentos de código? La respuesta es probablemente "no". Los comentarios en los ejemplos anteriores no agregan información adicional, especialmente porque los nombres de los métodos ya son muy descriptivos, lo cual es común en Objective-C. No agregue comentarios que expliquen lo obvio. Echa un vistazo al siguiente ejemplo. ¿No es este un uso mucho mejor de los comentarios?
1 |
// Determine speed of the monster
|
2 |
int minDuration = 2.0; |
3 |
int maxDuration = 8.0; |
4 |
int rangeDuration = maxDuration - minDuration; |
5 |
int actualDuration = (arc4random() % rangeDuration) + minDuration; |
Comentarios como este hacen que sea muy fácil navegar una base de código de manera rápida y eficiente. Le evita tener que leer el código y le ayuda a comprender la lógica o el algoritmo.
8. Estilo y consistencia
Cada idioma o plataforma tiene una (o más) guía de estilo e incluso la mayoría de las empresas tienen una. ¿Pones las llaves de un método de Objective-C en una línea separada o no?
1 |
- (void)calculateOffset { |
2 |
|
3 |
}
|
1 |
- (void)calculateOffset |
2 |
{
|
3 |
|
4 |
}
|
La respuesta es que no importa. No hay una respuesta correcta. Por supuesto, hay guías de estilo que puedes adoptar. Lo importante es que su código es consistente en términos de estilo. Si bien esto puede no afectar la calidad de su código, ciertamente afecta la legibilidad y lo más probable es que moleste a sus colegas o a quienquiera que lea su código. Para la mayoría de los desarrolladores, el código feo es el peor tipo de código.
9. Métodos y funciones enfocados
Un error común entre los desarrolladores es tratar de incluir tanta funcionalidad en funciones y métodos. Esto funciona, pero no es elegante y hace que la depuración sea un dolor en el cuello. Su vida, y la de sus colegas, será mucho más fácil si divide los problemas más grandes en pequeños fragmentos y los aborda en funciones o métodos separados. Eche un vistazo al siguiente ejemplo en el que escribimos una imagen en el disco. Esto parece una tarea trivial, pero hay mucho más si quieres hacerlo bien.
1 |
- (BOOL)saveToImage:(UIImage *)image withFileName:(NSString *)fileName { |
2 |
BOOL result = NO; |
3 |
NSString *documents = nil; |
4 |
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); |
5 |
|
6 |
if (paths.count) { |
7 |
documents = [paths objectAtIndex:0]; |
8 |
NSString *basePath = [documents stringByAppendingPathComponent:@"Archive"]; |
9 |
|
10 |
if (![[NSFileManager defaultManager] fileExistsAtPath:basePath]) { |
11 |
NSError *error = nil; |
12 |
[[NSFileManager defaultManager] createDirectoryAtPath:basePath withIntermediateDirectories:YES attributes:nil error:&error]; |
13 |
|
14 |
if (!error) { |
15 |
NSString *filePath = [basePath stringByAppendingPathComponent:fileName]; |
16 |
result = [UIImageJPEGRepresentation(image, 8.0) writeToFile:filePath atomically:YES]; |
17 |
|
18 |
} else { |
19 |
NSLog(@"Unable to create directory due to error %@ with user info %@.", error, error.userInfo); |
20 |
}
|
21 |
}
|
22 |
}
|
23 |
|
24 |
return result; |
25 |
}
|
Si una unidad de código intenta hacer demasiado, a menudo terminas con sentencias condicionales profundamente anidadas, una gran cantidad de comprobación de errores y sentencias condicionales demasiado complejas. Este método hace tres cosas, buscar la ruta del directorio de documentos de la aplicación, buscar y crear la ruta del directorio de archivos y escribir la imagen en el disco. Cada tarea se puede poner en su propio método como se muestra a continuación.
1 |
- (BOOL)saveToImage:(UIImage *)image withFileName:(NSString *)fileName { |
2 |
NSString *archivesDirectory = [self applicationArchivesDirectory]; |
3 |
if (!archivesDirectory) return NO; |
4 |
|
5 |
// Create Path
|
6 |
NSString *filePath = [archivesDirectory stringByAppendingPathComponent:fileName]; |
7 |
|
8 |
// Write Image to Disk
|
9 |
return [UIImageJPEGRepresentation(image, 8.0) writeToFile:filePath atomically:YES]; |
10 |
}
|
1 |
- (NSString *)applicationDocumentsDirectory { |
2 |
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); |
3 |
return paths.count ? [paths objectAtIndex:0] : nil; |
4 |
}
|
1 |
- (NSString *)applicationArchivesDirectory { |
2 |
NSString *documentsDirectory = [self applicationDocumentsDirectory]; |
3 |
NSString *archivesDirectory = [documentsDirectory stringByAppendingPathComponent:@"Archives"]; |
4 |
|
5 |
NSFileManager *fm = [NSFileManager defaultManager]; |
6 |
|
7 |
if (![fm fileExistsAtPath:archivesDirectory]) { |
8 |
NSError *error = nil; |
9 |
[fm createDirectoryAtPath:archivesDirectory withIntermediateDirectories:YES attributes:nil error:&error]; |
10 |
|
11 |
if (error) { |
12 |
NSLog(@"Unable to create directory due to error %@ with user info %@.", error, error.userInfo); |
13 |
return nil; |
14 |
}
|
15 |
}
|
16 |
|
17 |
return archivesDirectory; |
18 |
}
|
Esto es mucho más fácil de depurar y mantener. Incluso puede reutilizar el método applicationDocumentsDirectory en otros lugares del proyecto, que es otro beneficio de dividir problemas más grandes en partes manejables. Probar el código se vuelve mucho más fácil también.
Conclusión
En este artículo, hemos analizado más detenidamente la escritura de código legible mediante la elección inteligente de nombres para variables, funciones y métodos, siendo coherente al escribir código y dividiendo problemas complejos en partes manejables. Si tiene alguna pregunta o comentario, no dude en dejar un comentario a continuación.



