ES | Clean Code
“Even bad code can function. But if code isn’t clean, it can bring a development organization to its knees. Every year, countless hours and significant resources are lost because of poorly written code. But it doesn’t have to be that way.” – Robert C. Martin
Clean code o código limpio es una serie de prácticas que permiten hacer que tu código más mantenible. Pero mirando lo opuesto, ¿Cómo se causa un Bad Code o código sucio? muchas veces este código es generado por el afán de hacer las cosas, y al final … funcionan. Y muchas veces se escucha el dicho “Si ese código funciona, no lo toques”, así veas que ese código está muy caótico.
En el ciclo de vida de las aplicaciones, un pequeño porcentaje es relacionado al desarrollo de la misma, alrededor de un 30%, pero la etapa de mantenimiento puede llegar a ser hasta el 70% y es lo que más toma tiempo y si el código no es tan claro para ser modificado ralentizará el proceso de desarrollo y la situación se vuelve compleja. Con una serie de pautas y principios, podríamos llegar a escribir código fácil de entender desde el inicio para que sea mantenido y modificado en el futuro de una forma más eficaz.
Que factores negativos podemos percibir al tener un Bad Code:
- Hacer testing es complejo. Resolver bugs se vuelve una actividad tediosa
- La mayoría del tiempo se convierte en intentar entender el código antes de hacer los cambios
- Después de hacer cualquier modificación, es altamente probable que se cree otro bug.
Que cosas podemos hacer para poder considerar un código como Clean Code? Veamoslos a continuación:
- Nonredundant: Regla DRY (Don’t repeat yourself). Básicamente cuando se aplica, las modificaciones de los elementos dentro de la aplicación se vuelven simples y no requieren cambios en elementos no relacionados.
- Pleasant: Las reglas KISS (Keep it Simple, Stupid) y YAGNI (You Ain’t gonna need it) son principios que te permitirán ver un código que te sea fácil de navegar y encontrar lo que sea que estes buscando.
- Minimal Dependencies: Tener pocas dependencias hace que tu código sea fácil de mantener. El hecho que las clases y los métodos sean cortos y el código en si sea bien dividido hacen que leer el código sea directa y sencilla.
- Well Thought Out: Sin workarounds o soluciones confusas. Dedicar el tiempo a encontrar una buena solución que mantenga el lenguaje simple y fácil de seguir.
- Expressive: Que el nombramiento de las variables y todo en general sea significativo, fácilmente diferenciable y puedan expresar lo que quiere decir. Hacer esto ayuda que el código en si se vuelva en la documentación haciendo que la documentación en si sea dedicada a otras cosas.
- Unit and Acceptance Tests: Las pruebas unitarias de código aseguran que el código haga lo que se requiere. Hacer pruebas unitarias hace que el código pueda ser mantenido y extendido sin el miedo de romper algo existente.
- Easily Extended: Asegurarse que el código que se escriba sea pensado para que otro lo modifique. Teniendo esto en mente, asegúrate que tu código sea fácil de entender en el futuro.
- Focused: Cada clase, método y función debe de ser separada y no pueden afectarse entre ellas. Cada objeto tiene su propósito que está completamente encapsulado en su clase y todos los servicios están alineados con ese propósito.
Nota: Las personas de Coding.Stories han creado una página super interesante sobre Clean Code que te aseguro te gustaran. Son historias super cortas donde explican conceptos claros con ejemplos.
Meaningful Naming
El nombramiento es una de las formas más fáciles de expresar el funcionamiento de algo usando Clean Code. Pueden ser variables, funciones, argumentos, clases y hasta paquetes. Cada palabra que uses tendrá una interpretación en la persona que lo lea y todo esto es para mantener el mensaje claro.
Hay algunas reglas para el nombramiento en el desarrollo de software, vamos a mirarlas.
- Reveal Intention: ¿Porque existe? ¿Qué hace? y Como es usado? Si este nombramiento requiere un comentario, el nombre no revela su intensión.
- Don’t be cute: Claridad > Humor. Como dice el modismo popular “Say what you mean. Mean what you say”. Básicamente no andar con rodeos.
- Searchables Names: Los nombres deben de ser fáciles de ubicar en un texto. Si un nombre aparece en varios lugares, es imprescindible darle un nombre fácil de buscar. Si decides dar variables e una sola letra, úsalos como variables locales dentro de métodos cortos, por ejemplo i en un for como abreviación de la palabra índex.
- Avoid Encoding: Evita la notación Húngara, como por ejemplo txtTextBox en vez de textBox. O prefijos de miembros, como por ejemplo m_memberName. Y cosas así.
- One Word, One Meaning: Ser consistente a través del código usado una palabra por concepto abstracto. Esto es para evitar la confusión, para esto tratar de usar palabras que solo tienen un significado.
- Meaningful Distinctions: Necesitas diferenciar el código pero cambias la forma en que se escribe usando por ejemplo un sinónimo porque el otro nombre ya lo estas usando. Haz distinciones que sean significativas, pero sin cambiar la intención del código o la forma de buscarlo.
- Use Pronounceable names: Hace que sea más fácil de recordar. Ej. genExcelwf vs GenerateExcelWithFormat.
Reglas específicas de nombramiento
Length Rules
- Métodos y Clases que usan nombres cortos: Mientras más grande sea el Scope de la función, más corto debería de ser el nombre. Esos nombres largos deberían solamente ser dados a funciones que solamente se van a usar en un solo lugar. Normalmente las clases tienen un scope largo y deberían tener un nombre bien definido y corto.
- Métodos y Clases que usan nombres largos: Cuando la función tenga un Scope pequen`o o si solamente va a ser usado una sola vez, usar un nombre largo definitivamente va a ayudar a la lectura y entendimiento del código. Normalmente estos nombres se le dan a funciones o clases privadas.
- Variables y Parámetros que usan nombres largos: En el campo de los parámetros y variables, es inverso a la regla de las clases y métodos. Aquí el largo del nombre debería ser correspondiente a lo largo del Scope. Cosas como variables globales o constantes deberían tener un nombre largo y descriptivo. Nota: Adicionalmente, se sugiere que las constantes deberían estar en mayúscula y usar sintaxis underscore. Ej
private const int MIN_VERSION_SUPPORT = "1.5"
; - Variables y Parámetros que usan nombres cortos: En este caso solo se debe de usar en métodos pequeños o pequeños bloques de código. Puede ser tan corto como letras simples, pero solo bajo las siguientes circunstancias: Contadores en loops simples, excepciones en bloques de Catch y argumentos de funciones muy cortas.
Grammar Rules
La gramática provee un fundamento importante para que el código sea legible. Asegurando que el mensaje apropiado sea expresado. Dado a la gran extensibidad del idioma, hay unas reglas que ayudan a simplificar este tema.
- Class and Object Names: Usar noun phrase o sustantivos. Evitar el uso de verbos.
- Method Names: Usar Verbos.
- Solution Domains: Usar términos de programación como el nombre del algoritmo o el patrón de diseño usado.
- Problem Domains: Usar términos simples para dar claridad en la identificacion del problema. Evitar usar términos de programación.
Es importante tener en cuenta estas reglas de nombramiento cuando estas escribiendo código. Al inicio puede consumir tiempo, pero esto puede hacer una gran diferencia.
Functions
Escribir funciones limpias. Dependiendo del lenguaje usado, estas se pueden llamar de diferente forma, pero en síntesis es el bloque de código usado para realizar una tarea en específico. Esta función recibe algo, lo procesa y retorna algo. La idea con las funciones poder crear código que sea fácilmente reutilizable y llamado desde varios lados.
Es claro que para que una función siga los lineamientos de Clean Code, estas deben de ser pequeñas. ¿Pero qué significa pequeña cantidad hoy en día?
Anteriormente se decía que una función no debía de pasar las 24 líneas de código debido a que era el tamaño de un monitor en esa época. Pero hoy en día en un monitor pueden caber fácilmente más de 100 líneas de código, así que es bueno hacerse las siguientes preguntas antes de determinar si tu función es suficientemente pequeña.
- Esta es fácil de leer?
- Te es fácil navegar en el flujo del programa?
- Estas aplicando el principio DRY (Don´t Repeat Yourself)?
- Es fácil de hacer pruebas?
Seguramente si has respondido Si a todo, es porque seguramente tu función es lo suficientemente pequeña. Muchas funciones pueden ser efectivamente escritas en algo menos de 5 líneas de código, seguramente eso puede ser un limitante, pero esta limitación es buena.
Impacto en los bloques y la identación
Bloques con sentencias como ‘If’, ‘Else’ y ‘while’, deberían de ser en una line. Esa línea podría ser también un llamado a una función. Además, en una función con 5 líneas de código es imposible de mantener una estructura de anidado profunda, así que el nivel de identación no debería de exceder más de una o dos.
La funciones solamente deberían de hacer una sola cosa.
Una forma fácil de determinar si una función está haciendo más de una cosa es intentar extraer lógica en otra función. Si te es posible extraer líneas de código en otra función, probablemente tu función te haciendo más de una cosa.
La funciones deberían de tener un nivel de abstracción
¿Qué quiere decir esto? Que el concepto esencial de la función no debería de estar combinado con otros niveles de detalle a más bajo nivel. Por ejemplo, si estas escribiendo código bastante detallado, y al mismo tiempo estas escribiendo código de alto nivel. Esta mezcla de cosas causaría una gran confusión a la persona que este leyendo el código.
Es importante escribir código que pueda ser fácilmente entendido y leído por otros. Como si fuera una narrativa, en la que cada función tiene un nivel de abstracción desde la más alta a la más baja. Esto lo llama ‘The Stepdown Rule’ el autor Robert C. Martin.
Un Ejemplo que encontré hace algún tiempo es el presentado en el siguiente artículo. The Stepdown Rule. Aquí muestran una especie de receta en donde muestran código que no sigue la regla.
public void makeBreakfast() {
addEggs();
cook();
wife.give(fryingPan.getContents(20, PERCENT));
self.give(fryingPan.getContents(80, PERCENT)); // huehuehue
}
private void addEggs() {
fridge
.getEggs()
.forEach(egg -> fryingPan.add(egg.open());
}
private void cook() {
fryingPan.mixContents();
fryingPan.add(salt.getABit());
fryingPan.mixContents();
}
¿Algo ni se ve bien aquí, cierto? La función makeBreakfast
no está manteniendo el mismo nivel de abstracción. Incluso si el método es pequeño.
Un ejemplo de cómo podría mejorarse sería:
public void makeBreakfast() {
addEggs();
cook();
serve();
}
private void addEggs() {
fridge
.getEggs()
.forEach(egg -> fryingPan.add(egg.open());
}
private void cook() {
fryingPan.mixContents();
fryingPan.add(salt.getABit());
fryingPan.mixContents();
}
private void serve() {
wife.give(fryingPan.getContents(20, PERCENT));
self.give(fryingPan.getContents(80, PERCENT)); // huehuehue
}
Ahora makeBreakfast
contiene un nivel de abstracción alto, y los métodos addEggs
, cook
y serve
ahora contienen un nivel de abstracción a más bajo nivel.
Las funciones deberían de ser Commands o Querys, no ambas
El principio separación Command-Query señala que el estado de una función debería hacer una de las dos cosas:
- Comando o Command que es básicamente ejecutar una acción
- una Consulta o Query que obtendrá datos.
Todo esto es para evitar funciones confusas que cambien un estado de un objeto y retornen información sobre ese objeto. Funciones con este tipo de comportamiento las vas a encontrar muy comúnmente. Aquí un ejemplo de esta función que retorna ‘true’ o ‘false’ si el atributo no existe.
public boolean set(string atribute, string value);
// Y se usa así
if(set("usernamer", "admin1")){
...
}
Aquí tenemos el método set
, que hace una acción, pero no es muy claro realmente. Una separación de este método set
podría ayudar que el comportamiento sea más claro, como el siguiente ejemplo:
attributeExists("username"); //Query
setAttribute("username", "admin1"); //Command
El método attributeExists
obtiene la data y el método setAttribute
actualiza el objeto. Separar las responsabilidades termina con un código más claro, reutilizable y evita ambigüedades.
Las funciones no deben de tener efectos secundarios
Los efectos secundarios son mentiras en su código. ¡No hagas que tu código sea un mentiroso!
La idea es simple, las funciones deben hacer lo que indica su nombre. Cuando la función hace algo más a lo que se indica, este está ocultado cambios, lo cual hace que el código no sea consistente.
Las funciones deben usar Exceptions, evitar Error Codes
Las excepciones son la mejor forma de notificar errores de comportamiento. ¿Pero por qué? Miremos el siguiente código.
if (deleteChildren(node) == E_OK)
{
...
} else {
logger.log("delete failed");
return E_ERROR;
}
En el código anterior, si el primer if
es falso, vamos a retornar un mensaje de error. Posiblemente la variable E_ERROR tenga algún mensaje predefinido que se presente en la UI.
Pero qué pasa cuando hay muchas validaciones en el código. Como en el siguiente ejemplo:
if (deleteChildren(node) == E_OK) {
if (deleteReferences (node) == E OK) {
if (deleteNode (node) == E_OK) {
logger.log("node deleted");
} else {
logger.log("node not deleted");
}
} else {
logger.log ("delete Reference failed");
}
} else {
logger.log("delete children failed");
return E_ERROR;
}
En el ejemplo anterior se está mostrando una estructura profundamente anidada. Este lo cual es llamado dirty code. Para evitar este tipo de situaciones, el usar excepciones podría permitir simplificar el código. El procesamiento de errores se hace dentro del try/catch y ayuda a tener un código más limpio.
try {
deleteChildren (node) ;
deleteReferences (node) ;
deleteNode (node) ;
}
catch (Exception e) {
logger.log(e.getMessage());
}
Extraer el bloque Try/Catch en funciones
Extraer el contenido del Catch en su propia función generará un código más claro, fácil de leer y evitará confusiones en la estructura del código.
try {
deleteUser (user) ;
registry.deleteReference (user.username) ;
configs.deleteKey (user.username.makeKey ());
}
catch (Exception e) {
logger.log(e.getMessage());
}
En este ejemplo, vemos que hay una combinación del procesamiento del error con el procesamiento normal y eso podría generar confusión. Vamos a refactorizar este bloque:
public void delete(User user) {
try {
deleteUserAndAllRoles(user) ;
}
catch (Exception e) {
logError (e);
}
}
private void deleteUserAndAllRoles (User user) throws Exception {
deleteUser (user) ;
registry.deleteReference (user.username) ;
configs.deleteKey (user.username.makeKey());
}
private void logError(Exception e) {
logger.log(e.getMessage());
}
Recuerdan anteriormente que se hablaba de que una función debería de hacer solamente una cosa. Bueno el manejo de errores cuenta como una sola cosa. En la función delete
, esta función no debería de ser responsable de otra cosa. Si hay un Try/Catch dentro de la función, no debería de existir nada afuera de ese bloque de código.
Hay un ejercicio super interesante que me gustaría recomendar, se llama Balance Recalculation por Coding.stories.
Comentarios
Good Code is self-documenting
Te aseguro que algún día has escuchado algo así como. Un buen código es autodocumentado. Y bueno, esto es cierto, pero en especial ese código limpio que cuenta una historia.
Pero hay una forma de garantizar una documentación adecuada y es por medio de comentarios. Los comentarios son breves resúmenes que explican el código en texto simple y, a menudo, los desarrolladores los consultan cuando se refactoriza el código. Pero hay una ligera diferencia entre un buen y un mal comentario.
Tipos de comentarios permitidos
- Comentarios legales: La licencia o autores del código.
- Comentarios Informativos: Como por ejemplo la descripción de una expresión regular.
- Explicación de interés: Como cuando se quiere detallar la intensión de la decisión.
- Comentario de cosas por hacer. TODO
- Comentarios sobre mejoras
Tipos de comentarios que no son permitidos
- Mal código. Comentarios que intentan argumentar lo que hace el código
- Comentarios redundantes. Comentarios de cosas que claramente son autodescriptivo.
- Comentarios al cerrar una llave. Comentarios en estos lugares es porque el bloque de código es demasiado grande.
- Código comentado. Si se requiere un historial, subir el código a un Source Control. Pero evita dejar código comentado.
- Comentarios ruidosos: Cosas que no da información adicional.
- Comentarios incorrectos: Comentarios que no tienen nada que ver con el método o función.
- Comentarios de cosas que se han hecho. Si se cuenta con un Source Control, esto es innecesario.
Do Not Repeat Yourself (Una vez y sólo una)
En el flujo del desarrollo de software, un programador que normalmente vaya a desarrollar una funcionalidad va a realizar varias actividades alrededor del código mismo, probablemente esa nueva funcionalidad está documentada y hasta tenga pruebas unitarias. Pero como el conocimiento cambia constantemente -Cambio de programadores-, podemos terminar dando soporte a alguna funcionalidad requerida. Cuando se hace esa actividad, se debe de buscar lo que se implementó hace algún tiempo y puede que ese mantenimiento sea desafiante.
La finalidad de este principio es que cuando se requieran hacer cambios de una funcionalidad en específico, solo sea necesario cambiarlo en un solo lugar. Cuando hay que modificar el código en varios lados, esto puede causar mayor cantidad de errores.
Entonces bajo este principio, encontramos las siguientes siglas
- DRY –Don’t Repeat Yourself–. Este principio se centra en reducir la duplicidad de código. El uso del mismo ayuda al mantenimiento, legibilidad del código y ahorra tiempo y dinero.
- WET –Write Every Time–. Así se le llama a lo opuesto del DRY, en esencia es cuando la información está expresada en más de un lugar. Así que hay que buscar todas las ocurrencias y hacer las modificaciones requeridas.
¿Qué tipos de duplicaciones son comunes en el código?
- Duplicación impuesta: De pronto por estándares del proyecto o incluso por el lenguaje de programación o Frameworks.
- Duplicación inadvertida: Es cuando has duplicado código y ni te has dado cuenta. Por eso es importante hacer Code Reviews para que alguien pueda chequear este tipo de cosas
- Duplicación impaciente: Muchas veces pasa a conciencia, pero se deja por falta de tiempo. Este es otro problema que puede ser corregido haciendo Code Reviews regularmente.
- Duplicación entre desarrolladores: Es complejo identificar este aspecto fácilmente. La mayoría de las veces se ha cuenta de estos incidentes con herramientas de análisis de código. Pero también estructurando el proyecto apropiadamente y tener una buena documentación de las APIs, comunicación entre desarrolladores y sesiones de knowledge sharing.
Hay ciertos casos en la que duplicar código puede ser más eficiente o efectivo.
- Performance.
- Reducir el acoplamiento.
- Pruebas unitarias.
- Código generado automáticamente.
Hay varias técnicas de cómo se puede reforzar el principio DRY
- Code Reviews.
- Estructura del código. Un proyecto bien definido, reduce dudas.
- Tener documentación.
- Colaboración y sesiones de compartir conocimiento.
- Herramientas de análisis de Código
- Cultura de equipo.
Manejo de errores o error Handling
Los errores son algo que siempre va a pasar, pero crear errores que sean útiles es algo importante en un código limpio. Existen cuatro reglas básicas para el manejo de errores:
- Encontrar la raíz del error.
- Incluir información útil en el error.
- Manejar los errores apropiadamente.
- Usar Excepciones en vez de códigos de error.
Los primeros tres se enfocan en que hacer cuando el error ocurre, pero el cuarto se enfoca en como deberías de construir tu código efectivamente para manejar los errores.
Usar Excepciones en vez de códigos de error.
Como el título lo dice, las excepciones son preferidas sobre los errores de código porque lo mantienen limpio y mantenible. En el siguiente ejemplo se muestra cómo se manejan los errores con código de error.
public void delete(User user) {
DeleteHandler deleteHandler = deleteUserAndAllRoles(user);
if(deleteHandler != DeleteHandler.Success) {
if(deleteHandler == DeleteHandler.CouldNotDeleteUser) {
logger.log("Could not delete user");
} else if(deleteHandler == DeleteHandler.CouldNotDeleteReference) {
logger.log("Could not delete reference");
} else if(deleteHandler == DeleteHandler.CouldNotDeleteConfig) {
logger.log("Could not delete config");
}
}
else if (deleteHandler == DeleteHandler.Success) {
logger.log("User deleted successfully");
}
else
{
logger.log("Invalid handle for: " + user.toString());
}
}
Posiblemente este código te puede lucir familiar. El método deleteUserAndAllRoles
retorna un DeleteHandler
que es un enum que contiene los diferentes tipos de errores que pueden ocurrir. El Código siguiente al llamado tiene que validar todos los posibles casos de error y posiblemente se te podría olvidar alguno. Por eso es mejor levantar una excepción cuando algo sale mal. Miremos el siguiente ejemplo:
public void delete(User user) {
try {
DeleteHandler deleteHandler = deleteUserAndAllRoles(user);
}
catch (Exception e) {
logger.log(e);
}
}
¿Pero, porque usar los try-Catch-Finally? Porque podrías generar un código más limpio y fácil de leer. Esto permite terminar la ejecución en cualquier punto y continuar con el Catch
.
No retornar ni pasar nulos
Otra forma de generar código más limpio es evitar retornar valores null en las funciones. Un ejemplo que normalmente podemos ver sería el siguiente:
List<Orders> orders = getEmployees();
if(orders != null) {
for(Order order : orders) {
// Do something
}
}
Lo que sucede es que cuando retornamos valores NULL
en nuestras funciones, tenemos que validar que no sean nulos en el código que las llama. Hay veces podemos olvidar validar esos valores y esto puede generar errores. Un ejemplo práctico de como mejorar esto sería:
List<Orders> orders = getEmployees();
for(Order order : orders) {
// Do something
}
public List<Orders> getEmployees() {
// Get orders from database
List<Orders> orders = repository.getOrders();
// If there are no Orders, return an empty list
if(orders == null) {
return new ArrayList<Orders>();
}
return orders;
}
El código anterior esto evita verificaciones nulas y devuelven un código simple y legible.
En el caso inverso, cuando se recibe un valor nulo como parámetro, es algo que se debe de evitar. El final, si no se valida se generara un error genérico, un NullPointerExeption en el caso de c#. En este caso se aconseja usar CustomExceptions para manejar estos casos.
Un ejemplo de cómo manejar este tipo de errores seria:
public double getDiscountPercentage(Customer customer) {
return customer.discountPercentage * 1.5;
}
En este caso, cuando un valor nulo se pase, el código fallara porque se está intentando acceder a una propiedad de un objeto nulo. Una forma elegante de manejar este tipo de situaciones y poder tener más información de cuando falle se puede ver en el siguiente ejemplo:
public double getDiscountPercentage(Customer customer) {
// Check if customer is null
if(customer == null) {
throw new InvalidCustomerException("Customer cannot be null to generate discount");
}
return customer.discountPercentage * 1.5;
}
No hay una buena forma de manejar nulos que se han pasado a métodos accidentalmente, pero si es importante escribir excepciones que me permitan saber que ha pasado, no simplemente una excepción genérica.
Aquí les puedo mostrar un ejemplo de cómo crear una excepción personalizada:
// Metodo que genera la excepcion
private bool checkLogin(){
// validar las credenciales
if(hasNotValidCredentials())
throw new InvalidCredentialsException();
if(hasTooManyLoginAttempts())
throw new TooManyLoginAttemptsException();
if(hasBeenBlocked())
throw new UserBlockedException();
return true;
}
//Uso del Metodo
try{
checkLogin();
}
catch(InvalidCredentialsException e){
// Manejar la excepcion
}
catch(TooManyLoginAttemptsException e){
// Manejar la excepcion
}
catch(UserBlockedException e){
// Manejar la excepcion
}
Hay muchas opciones en el catch y esto rompe el principio Open/Close Principle, una forma para mejorar esto es usando una clase abstracta
public abstract class InvalidLoginException extends RuntimeException { }
public class InvalidCredentialsException extends InvalidLoginException{
protected message = "Invalid Credentials";
protected code = 2050;
}
public class TooManyLoginAttemptsException extends InvalidLoginException{
protected message = "Too Many Login Attempts";
protected code = 2051;
}
// Usar la excepcion con la clase abstracta
try{
checkLogin();
}
catch(InvalidLoginException e){
// Manejar la excepcion
log("Invalid Login: " + e.getMessage());
}
Refactor
Un gran juego para aprender sobre el refactor
https://codingstories.io/stories/605c9c8a8dc7ef03010c8546
Referencias
Clean Code: A Handbook of Agile Software Craftsmanship 1st Edición
de Robert C. Martin (Author)