¿Cómo no matar un proyecto con malas prácticas de codificación?
Bartosz Slysz
Software Engineer
Muchos programadores que comienzan su carrera consideran poco importante el tema de la denominación de variables, funciones, archivos y otros componentes. Como resultado, su lógica de diseño suele ser correcta: los algoritmos se ejecutan rápidamente y producen el efecto deseado, mientras que apenas pueden leerse. En este artículo, intentaré describir brevemente por qué debemos guiarnos a la hora de nombrar los distintos elementos del código y cómo no ir de un extremo a otro.
¿Por qué descuidar la fase de denominación prolongará (en algunos casos, enormemente) el desarrollo de su proyecto?
Supongamos que usted y su equipo se están apoderando del código de otros programadores. En proyecto que heredas se desarrolló sin ningún tipo de cariño: funcionaba bien, pero cada uno de sus elementos podría haberse escrito de una forma mucho mejor.
Cuando se trata de la arquitectura, en el caso de la herencia de código casi siempre desata el odio y la ira de los programadores que la recibieron. A veces se debe al uso de tecnologías moribundas (o extintas), a veces a la forma equivocada de concebir la aplicación al principio del desarrollo, y a veces simplemente a la falta de conocimientos del programador responsable.
En cualquier caso, a medida que pasa el tiempo del proyecto, es posible llegar a un punto en el que los programadores se vuelvan locos con las arquitecturas y las tecnologías. Al fin y al cabo, toda aplicación necesita reescribir algunas partes o simplemente cambios en partes concretas al cabo de un tiempo: es natural. Pero el problema que encanecerá a los programadores es la dificultad para leer y comprender el código que han heredado.
Especialmente en casos extremos, cuando las variables se nombran con letras sueltas sin sentido y las funciones son un repentino brote de creatividad, en ningún caso es coherente con el resto de la aplicación, sus programadores pueden volverse locos. En tal caso, cualquier análisis del código que pudiera ejecutarse rápida y eficazmente con una denominación correcta requiere un análisis adicional de los algoritmos responsables de producir el resultado de la función, por ejemplo. Y tal análisis, aunque discreto, hace perder una enorme cantidad de tiempo.
Implementar nuevas funcionalidades a lo largo de diferentes partes de la aplicación significa pasar por la pesadilla de analizarlo, después de algún tiempo tienes que volver al código y analizarlo de nuevo porque sus intenciones no están claras, y el tiempo anterior dedicado a intentar comprender su funcionamiento fue una pérdida porque ya no recuerdas cuál era su propósito.
Y así, nos vemos absorbidos por un tornado de desorden que reina en la aplicación y consume lentamente a todos los participantes en su desarrollo. Los programadores odian el proyecto, los jefes de proyecto odian explicar por qué su tiempo de desarrollo empieza a aumentar constantemente, y el cliente pierde la confianza y se enfada porque nada sale según lo previsto.
¿Cómo evitarlo?
Reconozcámoslo: hay cosas que no podemos saltarnos. Si hemos elegido ciertas tecnologías al principio del proyecto, tenemos que ser conscientes de que con el tiempo dejarán de estar soportadas o cada vez menos programadores dominarán tecnologías de hace unos años que poco a poco se van quedando obsoletas. Algunas librerías en sus actualizaciones requieren cambios más o menos comprometidos en el código, que a menudo suponen una vorágine de dependencias en la que te puedes atascar aún más.
Por otro lado, no es un escenario tan negro; por supuesto - las tecnologías están envejeciendo, pero el factor que definitivamente ralentiza el tiempo de desarrollo de los proyectos que las involucran es en gran medida el código feo. Y por supuesto, tenemos que mencionar aquí el libro de Robert C. Martin - se trata de una biblia para los programadores, donde el autor presenta una gran cantidad de buenas prácticas y principios que deben seguirse para crear código que se esfuerza por la perfección.
Lo básico a la hora de nombrar variables es transmitir de forma clara y sencilla su intención. Parece bastante sencillo, pero a veces mucha gente lo descuida o lo ignora. Un buen nombre especificará qué se supone que almacena exactamente la variable o qué se supone que hace la función - no puede nombrarse de forma demasiado genérica, pero por otro lado, no puede convertirse en una larga babosa cuya mera lectura suponga todo un reto para el cerebro. Después de algún tiempo con código de buena calidad, experimentamos el efecto de inmersión, en el que somos capaces de organizar subconscientemente el nombramiento y el paso de datos a la función de tal manera que el conjunto no deja ilusiones sobre qué intención lo impulsa y cuál es el resultado esperado de llamarlo.
Otra cosa que se puede encontrar en JavaScript, entre otras cosas, es un intento de sobreoptimizar el código, lo que en muchos casos lo hace ilegible. Es normal que algunos algoritmos requieran un cuidado especial, lo que a menudo refleja que la intención del código puede ser un poco más enrevesada. Sin embargo, los casos en los que necesitamos optimizaciones excesivas son extremadamente raros, o al menos aquellos en los que nuestro código está sucio. Es importante recordar que muchas optimizaciones relacionadas con el lenguaje tienen lugar en un nivel de abstracción ligeramente inferior; por ejemplo, el motor V8 puede, con suficientes iteraciones, acelerar significativamente los bucles. Lo que hay que subrayar es el hecho de que vivimos en el siglo XXI y no escribimos programas para la misión Apolo 13. Tenemos mucho más margen de maniobra. Tenemos mucho más margen de maniobra en el tema de los recursos: están ahí para ser utilizados (preferiblemente de forma razonable :>).
A veces, dividir el código en partes da mucho de sí. Cuando las operaciones forman una cadena cuya finalidad es realizar acciones responsables de una modificación concreta de los datos, es fácil perderse. Por lo tanto, de una manera sencilla, en lugar de hacer todo en una sola cadena, puede descomponer las partes individuales del código que son responsables de una cosa en particular en elementos individuales. Esto no sólo dejará clara la intención de las operaciones individuales, sino que también le permitirá probar fragmentos de código que son responsables de una sola cosa, y que pueden reutilizarse fácilmente.
Algunos ejemplos prácticos
Creo que la representación más precisa de algunas de las afirmaciones anteriores será mostrar cómo funcionan en la práctica: en este párrafo, intentaré esbozar algunas malas prácticas de código que más o menos pueden transformarse en buenas. Señalaré lo que perturba la legibilidad del código en algunos momentos y cómo evitarlo.
La pesadilla de las variables de una sola letra
Una práctica terrible que, por desgracia, es bastante común, incluso en las universidades, es nombrar las variables con una sola letra. Es difícil no estar de acuerdo en que a veces es una solución bastante conveniente: evitamos pensar innecesariamente en cómo determinar el propósito de una variable y, en lugar de utilizar varios o más caracteres para nombrarla, nos limitamos a utilizar una sola letra, por ejemplo, i, j, k.
Paradójicamente, algunas definiciones de estas variables están dotadas de un comentario mucho más largo, que determina lo que el autor tenía en mente.
Un buen ejemplo sería representar la iteración sobre un array bidimensional que contiene los valores correspondientes en la intersección de columna y fila.
const array = [[0, 1, 2], [3, 4, 5], [6, 7, 8]];
// bastante mal
for (let i = 0; i < array[i]; i++) {
for (let j = 0; j < array[i][j]; j++) {
// aquí está el contenido, pero cada vez que se usan i y j tengo que volver atrás y analizar para qué se usan
}
}
// sigue siendo malo pero divertido
let i; // fila
let j; // columna
for (i = 0; i < array[i]; i++) {
for (j = 0; j < array[i][j]; j++) {
// aquí está el contenido, pero cada vez que se usan i y j tengo que volver atrás y comprobar comentarios para qué se usan
}
}
// mucho mejor
const rowCount = array.length;
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
const fila = array[filaIndex];
const columnCount = fila.longitud;
for (let columnIndex = 0; columnIndex < columnCount; columnIndex++) {
const columna = fila[indicecolumna];
// ¿alguien tiene dudas sobre qué es qué?
}
}
Sobreoptimización furtiva
Un buen día, me encontré con un código muy sofisticado escrito por un ingeniero de software. Este ingeniero había descubierto que el envío de permisos de usuario como cadenas que especificaban acciones concretas podía optimizarse enormemente utilizando algunos trucos a nivel de bits.
Probablemente tal solución estaría bien si el objetivo fuera Commodore 64, pero el propósito de este código era una simple aplicación web, escrita en JS. Ha llegado el momento de superar esta peculiaridad: Digamos que un usuario sólo tiene cuatro opciones en todo el sistema para modificar contenidos: crear, leer, actualizar, borrar. Es bastante natural que enviemos estos permisos en forma JSON como claves de un objeto con estados o un array.
Sin embargo, nuestro ingenioso ingeniero se dio cuenta de que el número cuatro es un valor mágico en la presentación binaria y lo resolvió de la siguiente manera:
Toda la tabla de capacidades tiene 16 filas, he enumerado sólo 4 para transmitir la idea de la creación de estos permisos. La lectura de los permisos es la siguiente:
Lo que ve arriba no es Código WebAssembly. No quiero que se me malinterprete aquí: este tipo de optimizaciones son algo normal para sistemas en los que ciertas cosas necesitan ocupar muy poco tiempo o memoria (o ambas cosas). Las aplicaciones web, por otro lado, definitivamente no son un lugar donde tales sobre-optimizaciones tengan total sentido. No quiero generalizar, pero en el trabajo de los desarrolladores front-end rara vez se realizan operaciones más complejas que alcancen el nivel de abstracción de bits.
Simplemente no es legible, y un programador que pueda hacer un análisis de un código así seguramente se preguntará qué ventajas invisibles tiene esta solución y qué se puede dañar cuando el equipo de desarrollo quiere reescribirlo a una solución más razonable.
Es más - sospecho que enviar los permisos como un objeto ordinario permitiría a un programador leer la intención en 1-2 segundos, mientras que analizar todo esto desde el principio llevará al menos unos minutos. Habrá varios programadores en el proyecto, cada uno de ellos tendrá que encontrarse con este trozo de código - tendrán que analizarlo varias veces, porque después de algún tiempo se olvidarán de la magia que está pasando allí. ¿Merece la pena guardar esos pocos bytes? En mi opinión, no.
Divide y vencerás
Desarrollo web está creciendo rápidamente y no hay indicios de que nada vaya a cambiar pronto en este sentido. Hay que admitir que la responsabilidad de los desarrolladores de front-end ha aumentado considerablemente en los últimos tiempos: se han hecho cargo de la parte de la lógica responsable de la presentación de los datos en la interfaz de usuario.
A veces esta lógica es sencilla, y los objetos proporcionados por la API tienen una estructura simple y legible. A veces, sin embargo, requieren diferentes tipos de mapeo, ordenación y otras operaciones para adaptarlos a diferentes lugares de la página. Y aquí es donde podemos caer fácilmente en el pantano.
Muchas veces, me he sorprendido a mí mismo haciendo prácticamente ilegibles los datos de las operaciones que estaba realizando. A pesar del uso correcto de los métodos de array y de nombrar adecuadamente las variables, las cadenas de operaciones en algunos puntos casi perdían el contexto de lo que quería conseguir. Además, algunas de estas operaciones a veces necesitaban ser usadas en otro lugar, y a veces eran lo suficientemente globales o sofisticadas como para requerir escribir tests.
Lo sé, lo sé: no se trata de un trozo de código trivial que ilustre fácilmente lo que quiero transmitir. Y también sé que las complejidades computacionales de los dos ejemplos son ligeramente diferentes, aunque en 99% de los casos no necesitamos preocuparnos por ello. La diferencia entre los algoritmos es sencilla, ya que ambos preparan un mapa de ubicaciones y propietarios de dispositivos.
El primero prepara este mapa dos veces, mientras que el segundo sólo una. Y el ejemplo más sencillo que nos muestra que el segundo algoritmo es más portable reside en el hecho de que necesitamos cambiar la lógica de creación de este mapa para el primero y, por ejemplo, hacer la exclusión de ciertas localizaciones u otras cosas raras llamadas lógica de negocio. En el caso del segundo algoritmo, modificamos únicamente la forma de obtener el mapa, mientras que el resto de modificaciones de datos que se producen en las líneas posteriores permanecen inalteradas. En el caso del primer algoritmo, tenemos que modificar cada intento de preparar el mapa.
Y esto es sólo un ejemplo: en la práctica, hay muchos casos en los que necesitamos transformar o refactorizar un determinado modelo de datos en toda la aplicación.
La mejor forma de evitar estar al día de los distintos cambios del negocio es preparar herramientas globales que nos permitan extraer información de interés de forma bastante genérica. Aún a costa de esos 2-3 milisegundos que podemos perder a costa de la desoptimización.
Resumen
Ser programador es una profesión como cualquier otra: cada día aprendemos cosas nuevas y a menudo cometemos muchos errores. Lo más importante es aprender de esos errores, mejorar en nuestra profesión y no repetirlos en el futuro. No se puede creer en el mito de que el trabajo que hacemos será siempre impecable. Pero sí puedes, basándote en las experiencias de los demás, reducir los fallos en consecuencia.
Espero que la lectura de este artículo le ayude a evitar al menos algunas de las malas prácticas de codificación que he experimentado en mi trabajo. Si tiene alguna pregunta sobre las mejores prácticas de código, puede ponerse en contacto con Tripulación The Codest para consultar tus dudas.