Código bonito, código feo, código que funciona

Cuando uno empieza a programar (bueno, y algunos también después de muchos años) trata el código como algo con lo que hay que pelearse, que hay que golpear a martillazos hasta que conseguimos que, más o menos, encaje en el hueco que tiene que encajar.

Es una época en la que no te preocupas mucho de cómo ha quedado el código, aparecen fragmentos redundantes, código duplicado, variables con nombres poco descriptivos, comentarios irrelevantes o desactualizados… En definitiva, lo que podríamos considerar la antítesis del clean code que promulgan Uncle Bob y compañía.

Con el tiempo, toca mantener ese código, modificándolo para añadirle nuevas funcionalidades o corregir errores. Los martillazos que usamos para encajar el código de cualquier manera ya no nos parecen tan buena idea y empezamos a convencernos de que es importante cuidar la forma en que escribimos el código, y que conformarnos con la primera cosa que más o menos parece que funciona tal vez no sea la mejor solución.

El efecto rebote

En ocasiones, este afán por huir del nefasto código que escribíamos inicialmente nos lleva a prestar demasiada atención a la forma del código, recreándonos en aspectos estéticos, que además siempre tienen un componente muy subjetivo y temporal (las modas van y vienen).

Si vemos un par de comentarios, automáticamente pensamos que el código es poco claro y por eso están ahí. Si vemos un método con más de 5-6 líneas, enseguida tenemos la tentación de refactorizarlo. Si vemos un par de if’s anidados, sospechamos, y si vemos un switch, directamente se nos ponen los pelos de punta pensando en por qué no se habrá usado polimorfismo con una jerarquía de clases.

Lo que generalmente no vemos es que el código no sólo nos cuenta una historia sobre lo que hace, sino que también tiene su propia historia. A todos nos gusta leer código autoexplicativo que nos va contando lo que hace como si fuese un buen libro, pero todo el código se escribe por algún motivo concreto y en un momento determinado, y ese contexto es el que muchas veces acaba marcando el aspecto del código.

¿Ese estilo de nombrar las variables que hoy parece tan feo? Tal vez cuando se escribió el código hace 7 años era lo más estandarizado. ¿El if incomprensible que aparece al principio del método? A lo mejor está ahí para evitar un bug que surgió en producción y que ahora, tiempo después, ya nadie recuerda.

Código que funciona

Pero además de factores histórico-estéticos, también es posible que, simplemente, el código funcione bien. Hay algo que he aprendido con el tiempo, y es a respetar el código que funciona.

Hay código que funciona, lleva mucho tiempo en producción, ha pasado por muchas manos, muchos bugs, y ha llegado a un punto en que es estable, eficiente y esa aparente dificultad de mantenimiento no importa porque ya no hay que tocarlo más. Ya está terminado. Tiene muchas cicatrices y no es tan bonito como cuando lo diseñaron, pero está curtido en mil batallas y es capaz de resolver perfectamente el problema para el que fue construido.

Si repaso el código que he escrito, puedo encontrar código así sin muchos problemas. Y la tentación de reescribirlo es grande. Muy grande. Si cuando ves código que escribiste hace un tiempo no te entran ganas de reescribirlo, malo, no estás evolucionando; pero si cada vez que ves código que no te gusta lo reescribes, peor, no estás madurando.

Reescribir un código que funciona y que no te está estorbando sólo porque es feo suele ser una mala idea. Para empezar, tiene un coste asociado porque es tiempo que dedicas a rehacer algo que funciona en lugar de a implementar nuevas funcionalidades, pero además es probable que tu conocimiento del problema no sea tan profundo como el que hay acumulado en ese código y acabes (re)introduciendo bugs corregidos hace tiempo.

Antes de que los paladines del código limpio se lancen con sus herramientas de refactoring, sus tests de regresión y sus videos de Uncle Bob a por mi, y por si no ha quedado claro, aquí estoy hablando de reescribir código perfectamente válido y funcional sólo porque «te parece feo», o dicho de otro modo: hay razones perfectamente válidas para refactorizar y reescribir código, pero que no te guste su aspecto no es una de ellas.

Un caso actual

La semana pasada se anunció la liberación del código de .NET Framework (o más concretamente una parte, CoreFx), y aunque ese código llevaba tiempo accesible (más o menos), ahora que está en Github, nuestro amor por la pornografía nos ha llevado a muchos a echarle a un vistazo. Las reacciones han sido variadas, pero en generar hay partes que sorprenden por no ser todo lo bonitas o consistentes que a uno le gustaría.

Llevo bastante tiempo usando liberías de código abierto y estoy acostumbrado a leer su código, y no es la primera vez que veo un código que no me gusta, pero que funciona estupendamente bien. El antiguo código de protobuf-net, era horrible, pero el rendimiento y la estabilidad de la librería es sensacional. El código de NHibernate tampoco es el más bonito del mundo, pero es una librería que uso a diario y funciona muy bien.

Conclusiones

Como siempre digo, el código importa, pero el contexto más. Cuando te encuentres con un código que lleva mucho tiempo en producción aportando valor, no sólo te fijes en el aspecto que tiene. Seguramente detrás de él haya una historia que explique por qué está escrito de esa forma, y lo que sin duda ya ha demostrado es que es capaz de cumplir con su cometido.

Puede que ese código no sea el más sencillo de entender o mantener, pero una vez que se ha estabilizado y otro ya ha pagado el precio de esa complejidad adicional a la hora de mantenerlo, lo mejor que puedes hacer es aprovecharlo en lugar de empeñarte en luchar guerras que ya están ganadas.

8 comentarios en “Código bonito, código feo, código que funciona

  1. «Elegance is not a dispensable luxury but a factor that decides between success and failure.»

    No me gusta hablar en terminos esteticos del codigo porque me parece que es asociar un criterio subjetivo a otras consideraciones para luego usarlo para quitarle importancia a esos otros criterios.
    En este caso esos otros criterios (mas alla del «clean code») es la sencillez y la mantenibilidad: seguramente muchas de esas «cicatrices» son debidas a que el codigo en origen era malo (no «feo») y los parches lo han podido hacer peor (no «mas feo») y con mas probabilidades de que el siguiente parche provoque mas bugs.
    Para mi refactorizar tiene dos ventajas que valorar ante el riesgo de reproducir bugs previos:
    1.- Hacer mas facil el mantenimiento del codigo existente y el añadirle nuevas funcionalidades de forma mas segura
    2.- Aprender de forma mas profunda los detalles del codigo original y su devenir posterior (incluso a nivel funcional), con lo que tus cambios y añadidos presentes y futuros pueden ser mas eficaces
    Por supuesto necesitas una red de seguridad y ser cauteloso a la hora de meterte en el barro, para valorar hasta donde te puedes meter y si merece la pena o no. Todo en funcion del tiempo que tengas.
    Pero siempre hay tiempo para algun cambio, aunque sea pequeño, para mejorar la vida del sufridor de ese codigo legacy, ya seas tu mismo o el siguiente Sísifo que le toque subir la piedra.
    Y no hablo de hacer la piedra más bonita, sino más ligera.

  2. Estoy de acuerdo en lo que comentas, especialmente en que probablemente esas cicatrices venga de un mal diseño original.

    La cuestión es que hay veces que realmente no necesitas tocar el código porque lleva N años funcionando y, aunque su diseño no sea el mejor, puedes permitirte el lujo de tratarlo como una caja negra que «just works».

    Empezar a «mejorarlo» (por no entrar en el aspecto puramente estético) simplemente de forma preventiva (por si en algún momento hay que tocarlo), me parece que no siempre es la mejor opción. Asumes riesgos (aunque puedes mitigarlos con una red de seguridad, como tú dices) y la ganancia no está muy clara.

  3. Hola Juanma,

    Me ha gustado mucho el símil de evolucionar vs madurar, has dado en el clavo!

    Por cierto, sé que has estado trasteando con el código de .NET que han liberado. A grandes rasgos ¿Qué quieres sacar de ello? Lo digo porque seguro que son buenos motivos los que te empujan a ello, y yo sin embargo no encuentro casi ninguno, pero claro «algo me he perdido seguro» :) Es decir, ni tengo costumbre ni he «respirado» mucho open source, la verdad, y como dices tú, será la comunidad .NET la que tendrá que tirar del carro con el open source, pero realmente ¿Qué es tirar del carro en el contexto open-source? Yo no me veo haciendo un pull-request a .NET Core… ¿Qué hacer con ese código? ¿Curiosidad? ¿Aprender de él? Poco más se me ocurre en «mi contexto».

    Espero haber enfocado bien la pregunta, gracias!

  4. Si puedes tratarlo como una caja negra e integrarlo en algo nuevo casi nunca merece la pena ponerte a cambiarlo.
    Pero si tienes que tocarlo para arreglar un bug o añadir una funcionalidad al codigo mismo me suele merecer la pena refactorizarlo a medida que lo entiendo y convertir (y guardarlo asi) mi analisis en codigo (extrayendo metodos por ejemplo o cambiando los nombres de los existentes). La refactorizacion suele estar localizada alrededor de la parte a arreglar o aumentar. Si el arreglo o el añadido hay que duplicarlo por que el codigo original lo esta tambien suele ser una buena oportunidad para deshacer esa duplicidad.

    Ademas suele dar la «casualidad» que el codigo malo (o feo?) no se suele dejar tratar como una caja negra. ;-)

  5. Hola Sergio,

    De momento, sólo lo estaba leyendo por curiosidad y porque me gusta ese tipo de pornografía.

    Sobre qué espero sacar de él o el rol de «la comunidad» (qué poco me gusta esa palabra), daría para escribir un post entero ;-)

  6. ¡Hola!

    Una diferenciación a la que no veo que se le de importancia es al nivel abstracto del que se hable.

    Para mi hay una diferencia atroz entre el nivel de arquitectura (no me refiero al diseño, sino al nivel «de vista de pájaro» con el que uno ve/analiza/trabaja un sistema ya construido) y el nivel detallado de la implementación («el for» que hay dentro de una función).

    Para mi hay aspectos clave como el uso de excepciones para propagar ciertos estados, mantener singletones, inyectar, etc… que son realmente las importantes, las que deben hacerse bien (en base al problema a resolver) y las que merecen la pena refactorizar completamente si es preciso.

    Por contra para mi, que una función tenga 4 o 40 líneas, use switch, return o si me apuras hasta gotos, me importa bastante poco, siempre que su definición a nivel más global quede claro (y haga lo que debe, claro).

    De hecho, para mi, es muchísimo más importante minimizar la superficie a nivel global (de arriba a abajo en cada capa) y evitar un amasijo de clases, interfaces, delegados, etc… que el hecho de tener una única función de 50 líneas (esa función hace lo que hace y nada dentro de ella importa un carajo fuera; ni aunque esté usando un algoritmo que pueda estar ya disponible en el framework, api, etc…).

    Si tengo problemas con una función de (por decir) 100 líneas, en el peor de los casos la codifico de nuevo, si tengo problemas con el amasijo de dependencias entre proyectos… De hecho, yo suelo usar con frecuencia variables función («Func<A> f = a => …;») si puedo y sólo pertenece al ámbito del problema que resuelve la función. ¿Si es feo o bonito?, pues no lo se, pero a mi me resulta más fácil revisar el código «de arriba a abajo» (con «arriba» menos superficie) que no todo «desparramado» (todo declarado «ahí arriba»).

    Ya lo de si poner más o menos comentarios, nombres más o menos largos, me parece un aspecto puramente decorativo (que lógicamente afecta a la lectura, pero no más que el tipo de letra usado o el tamaño del monitor).

  7. De acuerdo con JoseJuan, aunque cambiar a nivel arquitectonico (de arriba a abajo) suele ser mas complicado que la implementacion.
    En cuanto al la superficie del codigo, en tanto en cuanto la relacion entre profundidad y anchura del arbol que representa al programa (tanto estatico/sintactico como en ejecucion) sea equilibrada y «limpia» (usando los ambitos sintacticos/modulos/visibilidades) si que prefiero que el tamaño de cada uno de ellos no sea excesivo. Lo de excesivo varia segun en que nivel estemos…
    Anque es verdad que a veces se desmenuza demasiado el codigo y ese equilibrio se rompe.

  8. Yo solamente puedo decir que atacar al código añejo me ha provocado dolores de cabeza más que satisfacciones. Normalmente te sale el tiro por la culata. A no ser que haya disponible un conjunto extenso de pruebas o un plan muy premeditado de rediseño (que no es lo más normal ni mucho menos), el código «legacy» es territorio comanche. Si funciona es mejor pensar que es una caja negra.

    Lo malo es cuando hay que arreglar un bug. Mi estrategia en esos casos suele ser la de «la línea recta»: consiste en contestar a la pregunta «¿cuál es el cambio mínimo que tengo que hace para que esto funcione?». Aunque depende mucho de lo enrevesado que sea el código.

    En definitiva, creo que es una virtud ver código legacy, tener ganas de ponerlo pagas arriba y, si funciona correctamente, dejarlo estar porque una vez que nos hayamos metido en la jungla con el machete, será casi imposible salir. Una cosa lleva a la otra, y al final hemos dejado bonito un 5% que es muy probable que acabe rompiendo funcionalidades del sitio más insospechado.

    Es por eso que creo en TDD.

Comentarios cerrados.