Recuerdo haber tenido una interesante discusión con un compañero de oficina hace unos años. Estábamos debatiendo si era necesario seguir las convenciones de código generalmente aceptadas por un lenguaje.
La mayoría de los lenguajes de programación poseen una serie de convenciones de código que los autores o principales contribuidores de dicho lenguaje sugieren seguir. A modo de ejemplo tenemos las convenciones de python (de las PEP), java (por Oracle) o ruby (de la gema rubocop).
Siempre que sea posible, es conveniente conocer el nombre de aquellas cosas que utilizamos a diario. Puede llegar a pensarse que es un lujo esnobista, pero muchas veces es un atajo en la comunicación. Imaginemos esta situación:
Y contrastémosla con la siguiente:
Ese diálogo imaginario puede suceder en persona, en una llamada, o incluso en revisiones de código asincrónicas. Puede optimizarse la comunicación mediante el uso adecuado de nombres.
Aquí presento algunas convenciones de nombre que tienen nombre propio (¡plop!), para ilustrar con ejemplos:
La lista no termina aquí, de hecho voy a dejarles un enlace a la sección específica de la Wikipedia sobre nombres y convenciones, donde se puede profundizar un poco más. No es mi interés realizar un recuento exhaustivo de convenciones, sino establecer un trasfondo para aquella discusión profesional.
"Existen solamente dos cosas difíciles en las ciencias de la computación: invalidar el cache y nombrar cosas."
Asignar nombres es una de las tareas más difíciles del software, y esto tiene que ver con varios aspectos:
La anterior lista surge de mi experiencia personal, seguramente se pueden agregar muchas razones más. Ahora bien, ¿qué rol cumplen las convenciones de código?
Reconozcámoslo: cada decisión que tomamos nos demanda utilizar un poco de fuerza de voluntad. Es por ello que optimizamos nuestro mundo a través de rutinas, métodos y convenciones. Las convenciones de nombre ayudan a reducir la infinita cantidad de opciones que tenemos al momento de asignar un nombre. Solamente debemos seleccionar un vocablo adecuado, sin preocuparnos en absoluto por el uso de mayúsculas, guiones, etc.
¿Cómo debo nombrar una variable que alberga el total de ventas de una sucursal? Teniendo las convenciones adecuadas, probablemente no haya más dudas que nombrarla total_ventas
o total_ventas_para_sucursal
. Y la decisión entre esos nombres dependerá exclusivamente de la necesidad de especificar el contexto de dicho total.
Nuestro cerebro comienza a tomar velocidad con la lectura e interpretación de palabras conforme avanzamos y ganamos confianza en el código y el entendimiento que tenemos del mismo. La presencia de variables o clases nombradas de forma inesperada, nos harán perder el ritmo y preguntarnos… ¿por qué esto es distinto? Un ejemplo en ruby:
# mal, corta el flujo de lectura
TOTAL_TAXES = taxes_calculator.calculate(productsInCart, taxes)
# bien, estimula el flujo de lectura
total_taxes = taxes_calculator.calculate(products_in_cart, TAXES)
La primera sentencia asigna una variable cuyo nombre está regido por las reglas de las constantes. Entonces comienzo a preguntarme… ¿estamos definiendo una constante? ¿este valor tiene sentido más allá de este uso?. Luego,
productsInCart
tiene un nombre que no espero leer de esa manera. Me planteo si viene de un sistema externo, o si hay alguna razón para que así sea. Es probable incluso que vaya al sitio donde se define dicha variable para comprender la causa. Por último, inadvertidamente utilizo una constantetaxes
que al ser nombrada como una variable normal, no transmite su carácter de referencia inmutable para todo el sistema.
En la segunda sentencia, se evitan todos estos problemas: las variables y constantes tienen la convención que transmite su característica fundamental, y ninguna variable es nombrada de modos anómalos que me hagan pensar de más.
Puede parecer superfluo, e incluso vano, pero la utilización de convenciones favorece a la mentalidad de “conjunto armónico”. Sabemos que las clases, por ejemplo, estarán nombradas con PascalCase, y eso esperamos encontrar. Al recorrer una clase tras otra vemos, con cierta satisfacción, que la clase está nombrada tal cual esperamos, y que es el único componente en todo el archivo que se nombra de esa manera.
Esta sensación es análoga a la que tenemos al ver un patrón de baldosas. Una vez que nuestro cerebro interpretó el patrón con el que fueron colocadas, encontrar una que haya sido mal ubicada o que debió ser reemplazada, genera una disonancia cognitiva: arruina la armonía que percibíamos.
Al escuchar una escala musical, podemos predecir cómo seguirá una vez capturado e interpretado el patrón. Cualquier nota que se salga de dicho patrón, romperá la armonía auditiva.
Cuando una variable se nombra en SCREAMING_SNAKE_CASE, no hay dudas de que es una constante. Algunos lenguajes incluso lo refuerzan mediante su intérprete o compilador: no debe cambiar de valor.
Si en cambio no existiera dicha convención, toda variable podría en realidad ser una constante: su uso es ambiguo, y debemos utilizar otros medios para reconocerlo: comentarios, documentación o incluso llamarla constante_pi
, cuando bien podría haberse llamado simplemente PI
. Otro ejemplo, en java:
// mal
final int minutosPorHora = 60; // es una constante
final int constanteMinutosPorHora = 60;
// bien
final int MINUTOS_POR_HORA = 60;
"Muchos de los detalles de la programación son de algún modo arbitrarios (...) La forma específica en que se responden algunas preguntas es menos importante que el hecho de que se respondan consistentemente cada vez. Las convenciones le ahorran a los programadores el problema de responder las mismas preguntas una y otra vez."
Las convenciones llenan los huecos donde puede haber más de una respuesta. ¿Quién dice que es preferible indentar el código con dos o cuatro espacios? Es una decisión arbitraria, y una discusión en la que es mejor no tomar partido. Lo importante, como establece Steve McConnell en la cita anterior, es que dicha convención se respete y aplique siempre del mismo modo.
Cuando las preguntas polémicas (tabulaciones contra espacios, por ejemplo) tienen respuestas establecidas y acordadas, el equipo de desarrollo puede dejar de preocuparse por ello y enfocarse en lo que realmente importa: ofrecer una solución de software.
No quiero que se malinterprete: la calidad interna del código es importante, pero ésta se definirá, en el caso de las convenciones, por acuerdos arbitrarios justificados en mayor o menor medida. A veces dichos acuerdos tienen justificaciones correctas y sensatas, y se adoptan sin más discusión. Otras veces, no tanto. Es allí donde los linters juegan su rol uniformador: para selectos acuerdos y convenciones polémicas no dejan lugar a dudas ni divergencias.
Muchas veces las convenciones no reemplazan atributos de calidad de código. Por más que utilicemos convenciones y las reforcemos mediante linters, no alcanza sólo con eso: tenemos que adoptar buenas prácticas de programación.
El uso de notación Húngara en un contexto de programación orientada a objetos es inapropiado [Thomas & Hunt, 2019]: la codificación del tipo de dato en el nombre de la variable genera una asociación entre variable y tipo que juega en contra del polimorfismo.
Robert Martin, en su libro Clean Code [Martin, 2009] afirma que las convenciones de nombres son beneficiosas, pero que siempre que fuera posible es preferible reforzarlas mediante la estructura. En sus palabras:
"Los switch/case con enumeraciones bien nombradas son inferiores a las clases base con métodos abstractos. Nadie está forzado a implementar las sentencias switch/case del mismo modo cada vez; pero las clases base fuerzan a las clases concretas para tener todos los métodos abstractos implementados."
Para dar un ejemplo personal, me he cansado de encontrar ifs
escritos “al revés” solamente porque la convención de código impide escribir if not x
, y en su lugar se prefiere if x
para luego intercambiar el bloque del if
por el del else
.
if (!started) {
// throws exception
} else {
// do stuff. Many lines of stuff
}
Muchos linters automáticos invierten el código:
if (started) {
// do stuff. Many lines of stuff
} else {
// throws exception
}
Es probable que la idea comunicativa al construir el bloque de código haya sido destacar la situación errónea al principio del método y atenderla lo antes posible. Sin embargo, el linter esconde el lanzamiento de la excepción detrás de muchas líneas de código, donde ya se perdió el contexto de la condición aplicada.
Comencé la nota contando una anécdota personal, y es justo que cuente cómo terminó. Luego de unos momentos de discusión y de exposición de argumentos, no llegamos a un acuerdo. Cabe destacar que no trabajamos nunca en el mismo equipo con esta persona.
Yo argumentaba que es necesario seguir las convenciones, por razones parecidas a las que mencioné en la nota. Él argumentaba que el código funciona de todos modos, y que las convenciones no importan si uno trabaja solo en el proyecto. Es entendible: para él, aprender las convenciones de un lenguaje no representaba ninguna ventaja, dado que sabía otras convenciones de memoria y las utilizaba consistentemente.
Recuerdo haberme retirado de esa discusión pensando “claro, no las necesita”. Sin embargo, replantearme la situación para escribir este artículo me hace reafirmar mi postura: yo creo que incluso un solo developer puede beneficiarse de las convenciones, porque no dejarán de aportarle uniformidad al código. Además, no bien necesite ayuda en el proyecto, alguien más deberá aprenderse las convenciones anteriormente utilizadas si no son las estándares del lenguaje de programación. O peor aún: convivirán dos convenciones distintas.
Después de todo, las convenciones son eso: acuerdos que se hacen. Las convenciones generales de un lenguaje de programación son importantes, pero aún más lo son las que acuerden los miembros de un equipo de desarrollo. Con fundamentos y motivos, por supuesto, pero acordadas.