No raw loops, no raw pointers. Estilo y arquitectura
Publicado el 7 de febrero de 2021
Una de las charlas de informática más famosas y referenciadas que hay, al menos en el mundillo de C++, es la charla de Sean Parent de 2013 titulada
C++ seasoning
. La charla es brillante y está llena de un conocimiento profundo sobre arquitectura de software que es perfectamente extrapolable fuera del lenguaje C++, fruto de los más de 20 años de experiencia de Parent, y cubre cuatro metas, expresadas de forma negativa, que según el presentador un proyecto debería intentar alcanzar. Siete años después de la charla, y siendo su legado bastante patente, resulta llamativo que la primera sección de la charla, la que cubre los algoritmos y la meta de “no raw loops”, haya calado mucho más profundamente que el resto, y me cuesta no preguntarme por qué.
“No raw loops” es brillante, como concepto y como eslogan. Lo que Parent defiende es que una función nunca debería tener dentro un bucle for o while, a menos que el objetivo de esa función sea abstraer ese bucle. El ejemplo que pone lo ilustra a la perfección. Un complicado fragmento de chromium con un bucle y varias condiciones dentro, bastante difícil de seguir, que en realidad lo único que está haciendo es buscar un elemento y rotar la secuencia para moverlo a una posición. Según la regla de “no raw loops”, los bucles deberían estar en funciones cuyo único propósito es hacer ese bucle, a los que llamaremos algoritmos. Luego, podemos construir algoritmos más complejos mediante la composición de otros algoritmos más simples. Como podemos ver en el ejemplo, el complicado código de chromium se vuelve muchísimo más simple cuando damos a sus pasos los nombres de
find_if
y
rotate
, en lugar de tener que adivinar lo que el bucle está haciendo.
Antes
Después
Pero la verdadera genialidad de este cambio es lo barato que es para la ganancia que supone, porque sólo afecta a la implementación de la función que está siendo escrita o modificada. Uno puede empezar a aplicar esta regla en el código nuevo que escribe en cualquier proyecto y pasar a escribir mejor código independientemente del entorno. Uno puede empezar a reescribir funciones ya existentes una a una sin en ningún momento romper el proyecto o tener que plantearse grandes rediseños de la arquitectura. Es posible que sólo parte de un equipo aplique esta regla y obtener ganancias de ello sin afectar al resto ni ser afectado por el resto. “No raw loops” es un cambio puramente estilístico, que sin embargo tiene un enorme impacto sobre la calidad del código escrito, y creo que su éxito radica precisamente en lo barato que es de adoptar.
Las otras metas propuestas también son muy interesantes. Con “No raw synchronization primitives” Parent sostiene que los primitivos de sincronización, como mutexes, variables atómicas, variables de condición o semáforos, deberían estar siempre encapsulados en constructos de más alto nivel de abstracción, como colas sincronizadas o tareas, con los que interactuaría el código de usuario. Es decir, que una clase arbitraria cuyo objetivo no fuera la sincronización nunca debería tener un mutex dentro, por ejemplo. Con “no incidental data structures” lo que defiende es que no deberían existir en un programa estructuras de datos que no estén representadas por una clase. Todo programa tiene datos. Y estos datos están estructurados de alguna forma. El programador tendría entonces la responsabilidad de hacer de esta estructura algo fácilmente observable en el código, sin dejar que degenere en un complejo grafo de objetos heterogéneos con conexiones arbitrarias, que es la estructura a la que tiende por defecto todo sistema orientado a objetos si se le deja crecer con poca supervisión. Esta meta va muy ligada a “no raw pointers”, que dice que en un programa no deberían existir punteros que impliquen posesión y no estén encapsulados en clases cuyo único objetivo sea encapsular ese puntero. Lo revolucionario de Parent frente a propuestas parecidas de esta idea es que él incluye
unique_ptr
y
shared_ptr
en los tipos de punteros con implicación de posesión que deberían estar encapsulados, porque la cuestión en discusión no es la seguridad de recursos sino la regularidad de los tipos y la capacidad de razonar localmente.
Con estas tres últimas metas, el marrón viene cuando uno quiere empezar a aplicarlas en su proyecto de 20 años de antigüedad y en estado bastante asalvajado, y se da cuenta de que va a ser difícil hacer que su nuevo código interopere fácilmente con los sistemas existentes. Mover un código no preparado para trabajar con tareas a un sistema de tareas es una tarea costosa que seguramente requerirá la reescritura de buena parte. Es algo que difícilmente se puede aplicar poco a poco y ver ganancia inmediata. Sucede algo parecido con las estructuras de datos incidentales. Un diseño que ha sido concebido como un conjunto de objetos que interactúan y se envían mensajes muy difícilmente aceptará cambios no triviales en la dirección de escribir tipos regulares y estructuras de datos bien definidas. La razón es que estos cambios son arquitecturales. La arquitectura es la parte del código que es más difícil de cambiar. Es la base sobre la que se construye el resto. Es la definición de las relaciones entre los distintos componentes del programa. También es la encargada de definir cuál es el camino de menor resistencia. Un sistema orientado a objetos bien diseñado presenta muchas facilidades para añadir a esta estructura de datos incidental nuevos objetos, mientras que programar en cualquier otro estilo frecuentemente requerirá esfuerzos y dificultades extra, que es precisamente aquello que se intenta evitar.
Hay una diferencia esencial entre “no raw loops” y las otras tres metas propuestas por Sean Parent en
C++ seasoning
. Mientras que “no raw loops” es un cambio meramente estilístico, el resto son grandes cambios arquitecturales que requieren profundos cambios en la mayoría de sistemas existentes y la necesidad de ser respetados por todo el equipo para poder dar rédito. Por lo tanto, es improbable que muchos de los programadores, atados por proyectos antiguos con opiniones muy fuertes en cuanto a cómo se hacen las cosas, veamos estas ideas coger fuerza en nuestro día a día. Sin embargo, tal vez sería interesante que proyectos nuevos que empiezan a escribirse hoy y que pueden convertirse en los proyectos viejos de dentro de 20 años las tuvieran en cuenta. Otra de las cosas que podemos aprender de esto es que no es necesario que un cambio repercuta en la arquitectura para tener un gran impacto en la calidad del código. Si algo debería generar en nosotros la genialidad de "no raw loops" es el deseo de encontrar nuevos cambios estilísticos que supongan una gran mejora en el código que escribimos.
Logo of RSS.