Artículo escrito por Carlos García Hernando.
Vamos a empezar con un pequeno “disclaimer”:
Cuando preguntamos (o nos preguntan) por algún aspecto o detalle de una aplicación o una pieza de software, a más de uno le sonará este tipo de respuestas:
- Mmmmm… La verdad es que, ahora que lo veo, no entiendo por qué está esto así… déjame que lo revise y te digo…
- Me cuesta entenderlo… ¡y eso que lo he programado yo!
- En principio se pensó como una solución temporal, pero se ha quedado así…
- Hubo que implementar una solución de emergencia… y claro… cuesta entenderlo.
Seguro que más de uno las ha utilizado o escuchado a lo largo de su carrera. Y que, al analizar alguna aplicación, en mayor o en menor medida ha tenido que lidiar con ocurrencias de todo tipo.
“He visto cosas que vosotros no creeríais”
¿Qué hace que un programa sea sencillo de seguir y de entender? ¿Cuáles son las características que debe tener un programa para que sea fácil de modificar, corregir y extender? ¿Cuáles son las acciones que debemos evitar? ¿Existe un modo correcto de hacer las cosas, o por el contrario el número de soluciones válidas a un problema es igual al número de posibles desarrolladores que implementen dicha solución?
El tema da para escribir un libro… y se han escrito. Robert C. Martin tuvo un superventas con su “Clean Code” y existen numerosos libros sobre el tema. Pero en este artículo, me limitaré a dar algunas pinceladas que, bajo mi punto de vista, son el origen que desencadena en soluciones de mala calidad.
En ocasiones la desidia, el desinterés y la falta de celo profesional, son el principal caldo de cultivo para generar código infierno. Desarrolladores desmotivados con un interés nulo por lo que están haciendo, que se limitan a copiar código de un sitio para pegarlo en el otro, en lugar de entender lo que están implementando.
Por supuesto, amigo lector, este no es nuestro caso. Entonces, ¿por qué seguimos generando código infernal? Ese que, al retomarlo a la vuelta de vacaciones, es imposible de entender. Bien, bajo mi punto de vista, la razón principal reside en el modo en que nos enfrentamos a la resolución de un problema.
Conductores nocturnos
En el desarrollo de aplicaciones, en algunas ocasiones se actúa como cuando circulamos por carreteras de noche. Únicamente somos capaces de ver hasta donde llega la luz de los faros. No somos capaces de anticiparnos o prever problemas más allá de la zona alumbrada.
Ya sea por desidia, pereza o falta de capacidad, frecuentemente no se tiene una visión clara global de lo que se tiene entre manos. Como en la carretera, únicamente se atiende a lo que tenemos delante de las narices. Sabemos que queremos ir del punto A, al punto B, pero no nos paramos a trazar un plan para llegar a la meta, simplemente se avanza en el desarrollo. Y muchas veces se llega a un punto donde hay que deshacer lo andado porque no nos lleva a ninguna parte. No tener una idea global y completa de lo que está hecho y de lo que se quiere hacer es sembrar vientos que más adelante se recogerán como tempestades.
WTF! por minuto
El modo en el que entendemos lo que nos rodea se basa en abstracciones. Somos capaces de desenvolvernos en lugares en los que nunca hemos estado, descomponiendo los elementos de dicho entorno en conceptos abstractos que nos son familiares.
Supongo que habréis visto la película «El apartamento» de Billy Wilder. El protagonista (Jack Lemmon) vive en un apartamento que presta a sus jefes para… bueno, tampoco destripemos el argumento. Vale, Jack les presta las llaves del apartamento y cada uno de estos jefes hace uso y disfrute del piso. Aunque sea la primera vez que el jefe acceda al apartamento, no es necesario que nadie le tenga que explicar cómo funcionar en el lugar. El apartamento es un ente abstracto y se compone de una serie de elementos que son en cierto modo universales. Aunque estemos entrando por primera vez sin que nadie nos diga nada, sabremos que probablemente exista un interruptor cerca de la puerta de la entrada que encenderá la luz de la habitación. Sabemos que habrá una cocina, y que en la cocina existirá un fregadero para lavar los platos. En el fregadero existirá un grifo, etc. Y por lo contrario, que se encienda o se apague una luz cuando se abre un grifo, no entra dentro de lo esperado.
En el desarrollo de software ocurre exactamente lo mismo. Cuando examinamos una aplicación, debemos ser capaces de identificar los distintos elementos que nos son familiares. Aquí un DAO, allá un controlador, ahí un mapeo, aquí un servicio, allí entidades que modelan el negocio, etc. Y todos estos elementos deben tener cierto orden, de modo que nos permitan identificar claramente las distintas capas de datos, negocio, etc.
Volviendo al ejemplo de la película, en una de las escenas Jack prepara unos espaguetis y para escurrirlos utiliza una raqueta.
Bueno… el uso de esta raqueta sin duda es algo que funciona. Consigue el objetivo propuesto que es escurrir los espaguetis, pero es una mala decisión. Un mal diseño. Si uno de sus jefes toma prestado el apartamento y decide hacer unos espaguetis, no sabrá cómo escurrirlos. Cuando vea la raqueta en el fregadero no entenderá que hace ahí. Es en definitiva un WTF!
En el libro comentado anteriormente, a modo de broma (o quizá no), mencionan que la única métrica válida para evaluar el código es el número de WTF! por minuto que suelta un desarrollador al examinar el código de una aplicación. Estas situaciones se reproducen muy a menudo. Se hace un mal uso de tecnologías o se emplean con una finalidad distinta a la que se supone que deben atender. Esto ocurre por querer emplear elementos que le son familiares al desarrollador (por haberlas utilizado anteriormente en otros desarrollos), o simplemente por no tener un conocimiento profundo de lo que se está haciendo.
El código que generamos, más allá de implementar una solución a un problema, es el modo en el que nos comunicamos con el resto de desarrolladores que tendrán que revisar y extender la aplicación. No programamos para nosotros, sino para los demás. En este sentido el código tiene que ser confiable. Las funciones o clases deben hacer lo que (razonablemente) se espera de ellas, con una finalidad clara y concreta. Una vez completada una solución debemos apartarnos un poco, mirar desde fuera lo implementado y ver si le hemos dado cierto sentido abstracto que sea comprensible por si mismo.
Frecuentemente, se toman decisiones de diseño que alteran el orden lógico del desarrollo por razones que nada tienen que ver con la solución a aportar. Se introducen tecnologías con calzador porque están de moda, aunque no apliquen, se matan moscas a cañonazos como crear piezas de generación de código automático que a la larga acoplan el propio desarrollo al mantenimiento de estos mecanismos, etc.
Otros aspectos que ayudan a conseguir código infernal son los siguientes:
- Soluciones de emergencia → Elementos hardcode y excepciones en el código que se introducen con la promesa de ser corregidas en un futuro y que al final perduran a lo largo de los años. Seguro que se puede dejar mejor.
- Los “por si acasos” → Esos elementos de código que nadie ha pedido y que se introducen por si acaso hay que utilizarlo en un futuro. Siempre es mejor implementar lo mínimo para cumplir con los requisitos.
- Los adornos y ocurrencias → Como he comentado, el código es el modo en el que nos comunicamos con otros desarrolladores. No te adornes, ni busques la solución más espectacular. Busca la implementación más limpia y directa. Deja la elegancia para el sastre.
- No refactorizar → ¿Puedo simplificar este código? ¿Se entiende bien? Después de implementar una solución hay que refactorizar, y después de eso, hay que volver a refactorizar.
- El miedo a tocar → “Esta parte funciona, mejor no tocar…” En general no es una buena estrategia. Si está mal hecha, hay que tocar, hay que mejorar el código y dejarlo mejor que cómo te lo has encontrado.
- Falta de convenciones de desarrollo → Un equipo de desarrollo que trabaje codo con codo debe fijar unas directivas y convenciones de desarrollo que permitan que todos los desarrollos estén alineados y tengan un estilo común. Estas convenciones deben de ir más allá de pautas de estilo y nombrado. Deben definir patrones y recetas para resolver aquellos aspectos que se repiten entre distintos desarrollos.
- Dogmatismo → Hay que evitar ser demasiado cuadriculado, tomar cualquier técnica o metodología en términos absolutos y no estar abierto a ponerlas en cuestión; o no atreverse a cuestionar las propias decisiones. Hay que valorar los puntos de vista del resto del equipo.
Los niveles de abstracción
Dado un determinado problema, normalmente vamos descomponiendo los elementos que van a formar parte de la solución en elementos cada vez más pequeños. Se parte del elemento principal, que va a contener el flujo de aplicación, y vamos desgranando en elementos cada vez más simples. Digamos que aplicamos la técnica de refinamientos sucesivos.
Por ejemplo, imaginemos que tenemos que implementar un método que realiza una operación compleja, por ejemplo un cálculo que requiere información de distintos subsistemas. Dicho elemento podría invocar a distintas piezas, como la búsqueda de usuarios, consulta de productos, etc. A su vez, la búsqueda de usuarios se podría descomponer en consulta de permisos, datos ubicación, etc. y así sucesivamente.
Calculo complejo
- Gestor usuarios
- Consulta datos usuarios
- Validación Perfiles
- Registro acceso
- …
- Gestión Pedidos
- Listado productos
- Lista stock
- …
- Historial
- … etc, etc.
Uno de los errores más comunes que se comenten en el desarrollo de aplicaciones es no manejar bien estos distintos niveles de abstracción. Quedémonos con la siguiente idea: Las piezas más pequeñas de software deben abstraer de los detalles de implementación a las partes más grandes.
¿Qué quiere decir esto? Bien, sigamos con el ejemplo. Una forma de implementar esta funcionalidad puede ser devolver toda la información al nivel principal y que sea el nivel principal el que “se sirva” y tome la información que necesite. Esto complica el desarrollo, ya que la parte más grande, la más compleja, tendrá que lidiar con la lista de usuarios, la lista de perfiles, stock, productos, historiales, etc.
Cuando estamos enfocados en una pieza pequeña de software, es sencillo conocer el modelo y los detalles. Si vamos aumentando el nivel de abstracción la cosa se complica, manejamos más modelos simultáneamente y es complicado manejar todos los detalles. Hay que hacer un esfuerzo por identificar la información mínima de cada una de las piezas más pequeñas, que necesitará la parte más grande de modo que únicamente devolvamos el mínimo de información necesaria. Si para el cálculo necesitamos conocer el id de usuario, devolveremos al nivel principal el id únicamente y no toda la lista de usuarios. Si necesitamos los detalles del un producto, devolveremos únicamente ése y no toda la lista… y así sucesivamente.
El camino más directo para conseguir código infumable, es delegar en la parte más compleja, que es la que está más arriba en el nivel de abstracción, los detalles que deberían estar resueltos en las partes más simples.
Conclusión
Supongo que conoces el cuento “El traje nuevo del emperador” de Hans Christian Andersen, en el que el emperador camina orgulloso de su traje que únicamente es visible para la gente competente y que sabe hacer bien su trabajo. Hasta que un niño grita -¡Pero si va desnudo! Siempre me gustó ese cuento. Todos nos identificamos con el niño… !¿cómo no hacerlo?! La triste realidad es que probablemente, como el resto, hubiéramos formado parte de la gente que soltaba alabanzas, nos hubiéramos dejado llevar por la corriente y no nos hubiéramos atrevido a gritar que el rey va desnudo.
Hemos hablado del número de WTF! por minuto, sin duda, una métrica muy subjetiva. Podemos correr el riesgo de identificar como WTF! todo aquel código que no sea nuestro. ¿Qué nos ha ido bien? ¿Qué cosas no han funcionado? ¿Qué vamos a evitar? ¿Qué vamos a fomentar? Debemos auto-evaluarnos constantemente por si, como el emperador, vamos con el culo al aire.
He pretendido centrar la atención en algunos aspectos a tener en cuenta en el desarrollo de soluciones. Soy consciente de que no es lo mismo que estemos hablando de una solución distribuida, que de un desarrollo standalone, no es lo mismo una aplicación batch, que un desarrollo en Spark. Pero independientemente de la tecnología, debemos aprender a pensar en abstracto y a tener una idea clara del sistema o la solución que queremos desarrollar.
En resumen, lo complicado es hacer soluciones simples. Complicar las cosas está al alcance de todo el mundo.