Código limpio y productividad

Código limpio y productividad

¿Qué programador con algo de experiencia no ha tenido que lidiar con código sucio o difícil de comprender y mantener?

Importancia de escribir código limpio.

¿Cuál es nuestra rutina diaria al programar? Revisamos código existente, lo leemos un montón de veces, copiamos, pegamos, reescribimos y en las mejores ocasiones refactorizamos. Regularmente nuestro código es el reflejo de otro y pasamos la mayor parte del tiempo que dedicamos a nuestra profesión leyéndolo. Entonces todo se hace más sencillo a medida que el código es más limpio y legible.

Al iniciar un proyecto nuevo solemos incurrir en un fenómeno que llamo “El ego de Flash”, por aquello del superhéroe de DC Comics cuya habilidad fundamental es la de ser extremadamente veloz. Desarrollamos funcionalidades de nuestro sistema una tras otra y esto genera un ambiente muy favorable en nuestro entorno. Para nuestros jefes y gestores de proyectos nos convertimos en superhéroes, genios capaces de programar y liberar requisitos funcionales a gran velocidad. Nos sentimos en una nube flotando y disfrutamos de un hermoso momento de gloria donde sólo nos interesa programar rápido para seguir llevando a cabo esa racha productiva. Ésto nos hace concentrarnos solamente en el funcionamiento del código, pasando por encima de que esté limpio y saludable. De forma que se relaciona estrechamente con una de las tendencias más preocupantes del gremio: “Si funciona, ¡no lo toques!”.

Nadie escribe código limpio a la primera. Éste con el tiempo termina por quedar excesivamente desorganizado, difícil de entender y por lo tanto de mantener. Aquí cito una de mis frases favoritas de Robert C. Martin, toda una institución en el tema del Clean Code y alguien de quien siempre se hace necesario hablar cuando se abordan estos temas: “The only way to go fast is to go well!”, o sea, la única manera de hacer las cosas rápido es hacer las cosas bien. Si sólo nos concentramos en que nuestro código funcione, apenas habremos cumplido con una pequeña parte de nuestro trabajo. Aunque “El ego de Flash” no es el único causante de que escribamos código sucio y descuidado.

Personalmente me he visto involucrado en proyectos que al paso del tiempo se convierten en desastres difíciles de mantener, dónde para dar solución al caos se ha decidido abandonarlos y comenzar nuevos proyectos sobre la misma base de requisitos. En muchas ocasiones tendemos a pensar que comenzar el mismo proyecto desde cero y a nuestro gusto, lo que llamamos un “greenfield project” - el sueño de todo desarrollador - sería la solución, pero finalmente el nuevo proyecto termina siendo un desastre, si no peor, igual al anterior. Redundando así en un ciclo infinito, en el cual, cuando cada nuevo proyecto adquiere cierta madurez se convierte en un monstruo que cada vez devora más horas de desarrollo para cubrir las funcionalidades más insignificantes. La realidad es que la solución en la mayoría de los casos está en nosotros y nuestra manera de trabajar; que debería estar enfocada en desarrollar código limpio y legible.

Uncle Bob, como coloquialmente se le llama a Robert C. Martin, en su libro “Clean Code: A Handbook of Agile Software Craftmanship”, expone varias reglas o guías que nos pueden ayudar notablemente a mejorar la calidad de nuestro código, miremos brevemente algunas de las más importantes referentes a dos temas: los nombres y las funciones.

Los nombres

Cuando escribimos software nombramos todo: el proyecto, las carpetas, archivos, módulos, clases, métodos, variables. Absolutamente todo lleva una denominación que nosotros, como autores del código, le damos. Los nombres son por mucho lo primero que ilustra o transmite el significado y la intención del código. Es de gran utilidad que sean legibles, pronunciables, explicativos y que describan el problema que se trata de resolver, evitando así las confusiones y desinformación. Si hay que buscar y viajar en el código leyendo una y otra vez para encontrar el significado de una variable o función, evidentemente la intención comunicativa de su nombre, en sí no es buena.

Es aconsejable usar sustantivos para los nombres de clases y variables, así como usar verbos para las funciones. También es preciso evitar a toda costa nombres que tiendan a confundir o causar ruido como: Manager, Processor, Data, Info; pensemos que encontramos tres clases con estos nombres: Project, ProjectData y ProjectInfo. Es imposible saber qué diferencias existen entre estas clases cuando vemos sus nombres, en cambio, nos produce una gran confusión.

Para valores booleanos se debe usar predicados (isLate, isPositive, isUpdated). Por ejemplo:

if(employee.isLate())

employee.applyDiscount();

Los nombres de un solo carácter, con prefijos o sufijos raros que tiendan a despistar son ineficientes. A veces para aclararlos se usan comentarios; en lugar de eso, obviemos los comentarios y nombremos correctamente, por ejemplo:

En lugar de hacer esto al declarar una variable:

int nP; // Number of passed tests

Resulta mucho mejor:

int numberOfPassedTests;

Los nombres pueden estar estrechamente relacionados con el scope (alcance) de lo que se está nombrando.

Para variables, mientras más pequeño es el scope, más pequeños pueden ser sus nombres. Un caso clásico son las variables que funcionan como índices en una iteración (for) que regularmente son nombrados con una sola letra (i, j). Aquí asumimos que el código que va a haber dentro del lazo va a ser de pequeño tamaño. Por lo tanto, donde quiera que se use ese índice se estará cerca de su declaración. Ahora, a medida que el uso de una variable se encuentre más lejos de su declaración, más largo y explicativo deberá ser su nombre, cuestión que donde se maneje la variable, el nombre en sí ya nos indique de qué se trata y no haya que subir, bajar y andar buscando en el código hasta la propia declaración.

Por el lado contrario, para funciones y clases, a medida que el scope es más pequeño y estamos ante funciones privadas dentro de una misma clase, podemos usar nombres más grandes y explicativos, que nos muestren específicamente ese método privado dentro de la clase qué aplicación tiene, por ejemplo: calculateSalaryWithoutTaxes. Sin embargo, para funciones públicas y clases con amplio scope es recomendable usar nombres cortos y sencillos, que sean capaces de ser llamados desde cualquier lugar del sistema con facilidad, ésto no quiere decir que no sean intuitivos, por ejemplo: openFile, saveImage.

Es importante quedarnos con la idea de que los nombres nos sirven para expresarnos. Como si estuviésemos contando una historia. Si alguien puede leer nuestro código como si leyera un cuento de niños y de un plumazo comprender qué se está intentando hacer y para resolver qué problema habremos cumplido con nuestro objetivo.

Las funciones

Las funciones deben seguir un esquema minimalista; siempre o casi siempre una función puede ser más pequeña del tamaño que actualmente tiene. Uncle Bob en su libro “Clean Code: A Handbook of Agile Software Craftmanship” plantea que una función debe tener entre 4 y 6 líneas de código; hasta 10 podría ser un número aceptable, pero ya no más. En otro caso se haría complicado entender lo que hace la misma y es muy posible que una función con más de 10 líneas de código ya esté cumpliendo con más de una responsabilidad.

Las funciones grandes, con varios niveles de anidación e identación, solamente están enmascarando una clase. Una función extensa siempre puede ser convertida en una clase. En efecto, es lo que se debe hacer con ellas. Las funciones grandes solo traen consigo confusión; ni hablar de esas funciones horribles que ni siquiera caben en la pantalla entera del computador y es preciso hacer scroll para visualizar sus diferentes partes.

Es importante que cada función haga solo una cosa. El principio de responsabilidad simple o SRP (Single Responsibility Principle) es uno de los más básicos y propagados en programación, por ende es nuestra responsabilidad seguirlo. Cada módulo (y cuando hablamos de módulo podemos decir que el nivel más básico o molecular de existencia de un módulo es una función) debe tener solo una responsabilidad para la que fue escrito.

El SRP se ve estrechamente relacionado con los principios SOLID, introducidos en los años 2000 por Uncle Bob. Es un conjunto de 5 fundamentos que de aplicarse, hacen que un sistema sea más fácil de extender y mantener. El SRP (Single Responsibility Principle) plantea que un módulo solo debería tener una única responsabilidad. El OCP (Open Closed Principle) indica que las entidades de software deben estar abiertas para extensiones, pero cerradas para modificaciones. El LSP (Liskov Substitution Principle) expone que cualquier objeto debería poder ser reemplazado por una instancia de un subtipo del mismo sin que el desempeño del sistema se vea afectado. El ISP (Interface Segregation Principle) formula que muchas interfaces pueden resultar mejor que una sola interfaz de propósito general (fat interface) implementada por varios objetos. El DIP (Dependency Inversion Principle) propone que se dependa de abstracciones y no de implementaciones directamente, un ejemplo claro es la inyección de dependencias. Considerando la importancia de estos principios pensamos abundar más en ellos en futuros artículos.

Quizás resulte un poco complicado imaginarse o determinar si una función está cubriendo más de una responsabilidad. Sin embargo, hay una fórmula muy fácil para ello: si se puede extraer una función desde dentro de otra, sin dudarlo ni titubear es lo que se debe hacer, porque la función contenedora en ese caso seguramente estará haciendo más de una cosa.

Los argumentos booleanos son un ejemplo clásico de funciones que hacen más de una cosa. Si usamos argumentos booleanos, es prácticamente un hecho que dentro de la función haya una condición que pregunte si el parámetro es verdadero o falso y en dependencia de la respuesta realice una cosa u otra. No usemos argumentos booleanos, una mejor manera de expresarlo sería descomponer nuestra función en varias con diferentes responsabilidades.

Referente a los argumentos, es recomendable usar pocos, una función con más de 3 argumentos puede hacerse difícil de entender. Además, si estamos ante una función que tiene más de tres argumentos y una sola responsabilidad, es muy posible que los argumentos estén estrechamente relacionados. Entonces se hace válido pensar en una clase que encapsule el comportamiento que tienen estos argumentos en común. Así, en lugar de pasar varios argumentos, pasar a la función solamente la clase como parámetro. Ni siquiera pensemos entonces en lo complicado que puede resultar entender el desempeño de una función que tiene argumentos de salida.

Se debe cumplir con el principio de Separación de comandos y consultas, más conocido como Command-query separation (CQS) del inglés. Se trata de separar las funciones que por algún motivo modifican el estado de un objeto de aquellas que obtienen información. Es decir, en una función que se hace una consulta a información del objeto no debería modificarse el propio objeto.

Usemos la Ley de Demeter o Principio del menor conocimiento: una función no tiene por qué saber la estructura completa que tiene el sistema, ni el objeto en el que está contenida. Solamente debe estar limitada al conocimiento y la lógica referentes a su responsabilidad.

Es plausible no usar iteraciones o lazos complicados donde haya estructuras anidadas o se haga un uso excesivo de returns o breaks dentro de las mismas. Las sentencias switch muchas veces son identificadas como la mala aplicación del polimorfismo. Analicemos siempre usar relaciones polimórficas antes de crear sentencias switch dentro de nuestras funciones para evaluar numerosos casos.

Los errores se deben gestionar usando excepciones, no condicionales. De ser posible, una función se debe encargar de la lógica y otra función de contener el bloque de captura de la excepción (try - catch). Desde esta última dentro del try se debe llamar a la primera, que realiza la lógica y lanza la excepción. Intentemos separar la lógica de negocio de la captura y el manejo de errores.

No debemos preguntarle a un objeto antes de ejecutar una acción; digámosle al objeto que realice una acción y dejémoslo que se gane la vida (que haga su trabajo), el objeto en sí debe ser capaz de intentar realizar la acción que le fue solicitada y devolver un resultado o una excepción si encontró alguna dificultad en el camino.

Finalizando

Pudiera ser complicado hasta cierto punto definir qué es código limpio; quizás resulte mucho más fácil saber qué es código sucio. Entonces ¿vemos algo raro y feo? ¿nos tardamos 30 minutos en descifrar lo que hace una función? ¿encontramos código sucio? Debemos refactorizarlo, limpiarlo, no tenemos justificación para no hacerlo. La base de escribir código limpio está en mejorarlo constantemente. Existe una regla llamada “The boy scout rule” para niños exploradores que plantea lo siguiente: “Always leave the campground cleaner than you found it.”, en español sería: “Siempre deja el campamento más limpio que como lo hallaste”, aplicable 100% a nuestra profesión. Si todos intentamos cada vez que hacemos push o checkin al código, subir una mejor versión del mismo, y cada vez que nos sea posible dejarlo más limpio que como lo encontramos, las cosas serían bastante diferentes.

Referencias:

Morgan Reyes Álvarez

Digital Solutions Expert en KION ITS Iberia

5 años

Resumen muy interesante de los aspectos más importantes en el desarrollo y mantenimiento de software. El código que escribimos en una aplicación, es el núcleo fundamental de todo producto de software, por lo cual resulta de vital importancia emplear los principios y patrones de diseño adecuados para garantizar un producto flexible, robusto y escalable.

Inicia sesión para ver o añadir un comentario.

Otros usuarios han visto

Ver temas