Sobrevivir sin un depurador

Después de leer un post de Pedro Hurtado sobre las dificultades para depurar aplicaciones en node.js, he estado pensando hasta qué punto es necesario un buen depurador para desarrollar aplicaciones complejas (doy por hecho que para escribir un «Hola Mundo» te puedes apañar sin nada).

Está claro que el depurador es una herramienta básica en el desarrollo de software y tener un buen depurador puede agilizar mucho la resolución de ciertos problemas. Mi duda es si contar con un depurador es un factor decisivo a la hora de decidir si merece la pena desarrollar sobre una plataforma o no.

Los que ya llevamos unos añitos en esto hemos convivido con herramientas de depuración muy limitadas (cuando las había), e incluso hoy en día hay situaciones en que contar con un depurador de verdad no es posible y tienes que apañarte con lo que hay, como el caso de PhoneGap.

¿Una cuestión cultural?

Muchos desarrolladores de .NET están muy acostumbrados a depender exclusivamente del depurador a la hora de resolver problemas en sus aplicacioens. Pulsan F5 en el Visual Studio, ejecutan la aplicación y empiezan a poner puntos de ruptura e ir paso a paso hasta que encuentran el problema.

Usar un depurador es una forma de encontrar problemas en el código, pero no es la única y, muchas veces, ni siquiera es la manera más eficiente de solventar problemas. Muchas veces basta con examinar el código para saber qué es lo que está fallando, pero hay cierta tendencia a sacar la artillería pesada, lanzar el depurador y empezar a rastrear valores de variables.

En otras comunidades la dependencia del depurador es, aparentemente, mucho menor. En lenguajes como Ruby, Python e incluso Javascript es a veces más práctico aprovechar las características del propio lenguaje para depurar la aplicación. Me refiero principalmente a la posibilidad de ejecutar código arbitrario dentro de una sesión interactiva, algo que en C# no es muy habitual (aunque es posible).

Los que trabajamos en el mundo Microsoft estamos acostumbrados a disponer de herramientas muy potentes e integradas para todo, pero hay otras vías que pueden resultar igualmente productivas (incluso, para algunos, más productivas). Es algo parecido a lo que comentaba en mi post sobre TypeScript.

No siempre se puede depurar

En la vida real hay situaciones en que usar un depurador no es posible (o es muy complicado) y es mejor estar preparado para ello.

En aplicaciones multihebra utilizar un depurador es complicado porque la mayoría de los problemas son debidos a carreras críticas y el mero hecho de conectar el depurador y colocar puntos de ruptura modifica los tiempos de ejecución, haciendo que a veces no sea posible reproducir el bug mientras el depurador está conectado.

Cuando se trata de fallos en el interfaz de usuario, no es cómodo usar un depurador porque cuando «salta» el depurador cambia el foco de las ventanas, lo que puede dar lugar a que se lancen eventos que modifiquen el escenario que estamos depurando.

En dispositivos móviles para usar un depurador normalmente hace falta tenerlos conectados a un PC, lo que dependiendo del dispositivo limita mucho el tipo de pruebas que se pueden realizar. Es cierto que hoy en día existen emuladores muy completos para realizar este tipo de pruebas, pero aun así los emuladores no son perfectos y muchas veces el comportamiento del dispositivo real en condiciones reales varía mucho con respecto al emulador.

Limitando la dependencia del depurador

Cuando empecé a programar mis primeras aplicaciones en las prácticas de la carrera, uno de los errores que cometía con más frecuencia era empezar a escribir mucho código y no probarlo hasta el final. Podía pasarme una hora programando sin llegar a ejecutar nada y claro, cuando empezaba a probar, aquello fallaba por mil sitios distintos y era complicado saber qué estaba fallando y por qué.

Para evitar eso, es importante realizar el desarrollo de forma incremental, buscando la forma de comprobar cada poco tiempo si lo que hemos desarrollado hasta el momento funciona correctamente.

Una buena forma de hacer esto es utilizar tests automáticos (da igual que sea aplicando TDD o no), que nos permitan comprobar rápidamente si lo que hemos implementado realmente funciona. Suele ser mucho más rápido ejecutar un test para ejercitar parte del código que lanzar la aplicación completa, conectar un depurador y manejar la aplicación hasta llegar a la parte que queremos comprobar.

Otra opción para limitar la dependencia del compilador es diseñar desde el principio un sistema que nos permita trazar y monitorizar la aplicación. Podemos usar los típicos Debug.WriteLine o console.log, pero también podemos buscar soluciones más completas que nos permitan controlar el nivel de información que queremos generar, el formato, etc.

Hay que tener en cuenta además que casi ninguna aplicación funciona de forma aislada y muchas veces podemos aprovechar los puntos de integración con sistemas externos para conseguir información de lo que está pasando en nuestra aplicación.

Por ejemplo, en una aplicación web típica, podremos usar herramientas como Fiddler para inspeccionar las peticiones web, es decir, la interacción con el browser, o SQL Profiler para monitorizar la interacción con la base de datos. Se trata de herramientas externas al lenguaje que estemos usando y, por tanto, aplicables a cualquier plataforma.

También es muy útil emplear técnicas de design by contract, aunque sea de una forma muy básica. Aquello que te contaban en la carreras sobre precondiciones, postcondiciones e invariantes y que parecía algo demasiado formal, resulta de mucha ayuda a la hora de desarrollar aplicaciones sólidas en las que puedes encontrar fácilmente lo que está fallando sin necesidad de recurrir a un depurador.

El mero hecho de validar que los parámetros de entrada de los métodos se ajustan a lo esperado, unido al stack trace que muestran casi todos los lenguajes modernos cuando se produce un error, hace que sea fácil saber en qué punto ha pasado algo raro, ahorrándonos mucho tiempo de depuración.

Además, estas técnicas son útiles una vez que el sistema está en producción, donde muchas veces no podremos contar con un depurador para resolver problemas, y antes o después nos tocará ayudar a dar soporte.

Conclusiones

A mi depurar no me gusta: me parece aburrido, lento y tedioso. Cuando tengo que recurrir al depurador porque no puedo ver el error inspeccionando el código, siempre pienso que hay otro problema mayor: si necesito depurar es porque el código que está fallando es demasiado complicado y en ese caso tengo un problema de diseño.

Es algo parecido al uso de comentarios, que los veo como el último recurso al que recurrir cuando no consigues escribir código claro.

No obstante, el hecho de poder apañarte sin un depurador no quiere decir que no sea una herramienta extremadamente valiosa. Se puede hacer fuego con yesca y pedernal (o eso dicen), pero con cerillas es mucho más sencillo.

En cualquier caso, y por volver al post que mencionaba al principio de Pedro, creo que a veces es aplicable aquello de allá donde fueres, haz lo que vieres. A fin de cuentas, hay mucha gente desarrollando sobre node.js sin usar Visual Studio y se les ve muy felices.

Cuando cambias de lenguaje o plataforma, las técnicas de desarrollo a las que estamos acostumbrados a .NET no son las únicas y, seguramente, tampoco las mejores. Lo bueno es que saltar entre plataformas siempre va a resultar enriquecedor aunque sólo sea por conocer otros puntos de vista.