Tinselcity

Es lo que se suele decir, que errar es humano. Pero también es humano tratar de anticiparse a los errores y también tratar de resolverlos y arreglar las cosas. En programación es relativamente frecuente cometer algún error. Parece interesante, entonces, pensar un poco tanto en formas de evitar o anticipar errores, como en formas de solucionarlos.

Errar es humano

Programar, a veces, es complicado. Esto es difícil de rebatir. Hay muchos detalles, muchos factores, demasiadas cosas a tener en cuenta para seguirlas todas a la vez en nuestra cabeza mientras programamos. No solo esto, también podemos confundirnos, pensar que algo es de un modo cuando es de otro, por ejemplo.

Personalmente detesto la actitud que toman algunos programadores frente a esto. Demasiados comentarios jocosos hacen alusión a una especie de impresión de que ni siquiera nosotros mismos tenemos idea de qué estamos haciendo. Pero, ¿cómo podemos afrontar esta realidad de la complejidad y de la casi inevitabilidad de cometer errores o encontrarnos errores, fallos, problemas?

No hay una única solución. Como en tantas otras cosas, lo que mejor funciona es una combinación de técnicas que apliquemos de una u otra forma según la ocasión. Siguiendo la idea de capítulos anteriores, no voy a entrar en el detalle de ver un problema concreto sino que la intención es aprender cómo enfrentarnos a todos ellos.

Contra la complejidad...

Aunque ya he estado hablando de esto en bastantes ocasiones antes, creo que merece la pena repetirlo. Es, de hecho, la mejor forma de combatir la complejidad: Evitarla todo lo que podamos.

Contra la complejidad, aplicamos todo lo hablado anteriormente. Descomponemos los problemas en problemas más pequeños, aislamos las diferentes complejidades de modo que no interfieran unas con otras. Reducimos la complejidad a base de solucionar pequeñas porciones de complejidad, que siempre es más accesible, y eliminando su influencia sobre el resto de complejidades.

No solo esto, sino que además esta descomposición nos permite -en general- encontrar más fácilmente los problemas o fallos. Y además, su alcance queda delimitado -casi siempre- a una cierta pieza o parte del todo.

Pero no es esta la única herramienta que tenemos para combatir la complejidad. Podemos descomponer nuestro código en partes claramente separadas e independientes pero esto no sirve de nada si dentro de esas partes gobierna el caos. Sí, la complejidad estará limitada en su alcance pero seguirá siendo fácil que se comentan errores o fallos. La solución, como es lógico, consiste en que procuremos siempre que todo el código que escribamos esté guiado siempre por buscar la solución más simple, la más clara, la que menos complejidad añada al propio problema original.

Es un problema relativamente común entre programadores con menos experiencia… no tanto con los que están empezando pero sí con los que tienen algo de experiencia pero no mucha… es un problema común, decía, añadir complejidad a las soluciones y problemas. Es una tendencia a pensar en que será bueno añadir ciertas cosas, utilizar estructuras más complejas o algoritmos más sofisticados para solucionar un problema cualquiera. Y esto casi nunca es cierto. Al contrario, debemos intentar pensar siempre en la solución más simple que funcione.

Esto tiene además una ventaja tremendamente importante: Es extremadamente fácil añadir complejidad. Es decir, que, en caso de necesidad, si hemos desarrollado una solución simple y encontramos un problema que nuestra solución no puede resolver, es bastante fácil elaborar más nuestra solución y añadirle los cambios necesarios para que funcione en ese caso. Por el contrario, si hemos desarrollado una solución compleja y sofisticada, casi siempre ocurre que introducir cambios en ella es muchísimo más costoso y tratar de quitar parte de esa complejidad es bastante más costoso que añadirla.

Así que, como primera idea para evitar problemas de complejidad, mantener siempre en la cabeza el principio de buscar la simplicidad de nuestras soluciones y de nuestro código.

Pruebas

Hay muchas técnicas y metodologías que promueven el uso de tests o pruebas, en diferentes formas y modos. Algunos promueven la idea de escribir las pruebas antes que el código, otros promueven ir alternando en un ciclo de test-código que se repite una y otra vez… Diferentes lenguajes y plataformas soportan diferentes estilos de realizar, en general, pruebas. Los hay, por ejemplo, que dan soporte para escribir ciertas comprobaciones en forma de contratos dentro del propio código. Hay herramientas que permiten analizar ciertas propiedades del código. No quiero aquí valorar unas u otras soluciones, sino que tengamos una idea clara de qué aporta como concepto general la idea de las “pruebas”.

La mayoría de la gente tiende a centrarse más en un tipo o dos de pruebas concretas, conocidas como tests unitarios y tests de integración. Yo creo que debemos pensar de una forma más amplia y ver todas estas opciones como “pruebas”… comprobaciones de que nuestro código cumple ciertas condiciones. Esto puede tomar la forma de tests unitarios o de integración, de especificaciones de aceptación, de contratos, de declaraciones de tipos… Todo ello aporta en alguna medida.

Como es natural, somos nosotros mismos los que tenemos que pensar y escribir esas condiciones y hacer las pruebas que verifican que se cumplen. Y por tanto, también es algo que tendremos que tratar con la misma consideración que el resto del código que escribimos. Así, deberemos pensar sobre qué pruebas queremos escribir, sobre el coste que tienen esas pruebas y lo que nos aportan o no. Como ya hemos visto con otras partes del código, esto dependerá de cada proyecto. De su tamaño y complejidad, de su ciclo de vida y duración, de lo que esperemos de su evolución, etc. No es lo mismo un proyecto “de usar y tirar” que uno en el que buscamos una vida larga, uno estable o uno que necesite frecuentes modificaciones y actualizaciones, etc.

Hay que recordar también que las pruebas, sean estas en forma de tests, de contratos, de tipos, etc, únicamente son tan buenas como nosotros las hagamos. Por ejemplo, tener muchos tests no es necesariamente mejor. Cualquier herramienta o técnica que utilicemos será solo tan buena como nosotros la hagamos.

Finalmente, lo que aportan todas estas pruebas es una garantía de que se cumplen ciertas condiciones. Nos ayudan delimitando lo que el código puede hacer o no, e indicándonos lo que el código está haciendo en realidad. Es decir, van a ayudarnos en alguna medida como puntos de apoyo de cosas que sabemos que ocurren como esperamos o indicándonos que no, no están ocurriendo como creíamos. Es una buena herramienta -varias buenas herramientas- que nos ofrece certidumbre allí donde podrían tener alguna duda.

Trazas y Depuración

Estas son dos herramientas potentes pero lamentablemente abusadas en demasiadas ocasiones.

Es muy conveniente saber manejar ambas cosas, tanto el uso de trazas como el de las herramientas de depuración, sobretodo cuando tenemos problemas realmente complejos de seguir o de encontrar. Añadir trazas a nuestro programa nos ofrece una vista de lo que ha hecho cuando lo hemos ejecutado. En el caso de la depuración, esa vista la tenemos mientras se está ejecutando. En ambos casos, esta visión es algo muy interesante y directo y nos puede permitir encontrar ese comportamiento inesperado y dónde ocurre.

Como digo es muy conveniente aprender a usar bien estas dos herramientas. Son herramientas que pueden producir mucha -a veces demasiada- información. Por eso, quizá lo más importante es aprender a identificar los puntos interesantes que queremos observar. Avanzar en un proceso de depuración línea a línea o producir unas trazas que den información de cada línea ejecutada resulta rápidamente tedioso y agotador. En lugar de esto, debemos marcar solamente esas cosas que realmente son relevantes para el fallo que buscamos. Es algo que, en buena medida se aprende con la experiencia y con una aproximación analítica y racional.

Como también decía son herramientas que tiene sentido usar cuando nos encontramos con problemas graves, complicados, difíciles de identificar o de encontrar. Desafortunadamente muchos programadores terminan haciendo un uso demasiado frecuente de ambas. Terminan perdiendo mucho tiempo y acaban por caer en esa actitud de programar a base de probar a ver qué pasa.

Idealmente, si nos aplicamos a pensar y entender, tanto el problema como el código que escribimos, debería ser raro y poco frecuente que necesitáramos cualquiera de las dos. Es más, encontrarnos en una situación en que realmente las necesitáramos debería inmediatamente producirnos cierto terror, porque debe significar que estamos ante un problema gordo. En mis últimos años de programador esta era mi experiencia; muy rara vez tenía que usar un depurador, y cuando ocurría generalmente solía significar que había graves problemas.

Reconozco usar, de vez en cuando, alguna traza para observar algún detalle. Creo que es razonable. Pero espero que alguien que haya llegado hasta aquí leyendo y haya interiorizado la importancia de pensar y comprender lo que hace, termine teniendo la misma tendencia y que sea poco frecuente tener que recurrir, por ejemplo, a la depuración o a tener que instrumentar un montón de trazas para encontrar un problema.


Como resumen de todo lo anterior, y para recalcar la importancia que tiene, recordemos:

El mejor modo de evitar fallos es prevenirlos. La mejor manera de prevenirlos es por un lado siendo conscientes de qué estamos haciendo y por otro buscando la simplicidad todo lo que podamos. Además tenemos algunas herramientas que nos permiten imponer restricciones o hacer comprobaciones sobre nuestro código. Estás pruebas nos ayudan sirviendo de certeza y de indicativo del comportamiento de nuestro código. Finalmente, en caso de que todo lo anterior no sea suficiente1), es muy útil saber utilizar bien las herramientas de depuración para identificar y encontrar los fallos.


1)
y solo en ese caso

Discusión

Escribe el comentario. Se permite la sintaxis wiki: