Hoy quiero hablarles de una de las más aceptadas metodologías de desarrollo en el ámbito industrial (e incluso en el académico), aquella conocida como "test driven development". En este enfoque, lo primero en diseñarse son los casos de prueba que se desea que pase exitosamente un cierto programa y luego se implementa dicho programa de forma que satisfaga estas expectativas. El proceso puede ser iterativo, incorporando nuevos casos y posiblemente alterando la implementación realizada acorde a estos. Esta estrategia tiene una grandiosa ventaja: Permite al programador estar más claro desde el principio en lo que quiere implementar y las propiedades que cumple. Además le otorga un mecanismo para descubrir fallas en su implementación o en su intuición (siempre puede haber errores en la definición de casos de prueba también). Incluso hay muy buenas herramientas que permiten realizar pruebas unitarias con facilidad. Lo que es una lástima... es que está severamente limitado.
¿Cómo que está limitado? El enfoque del "test driven development" está basado en un conjunto de prueba que sirve como heurística para la totalidad de las posibles entradas al sistema implementado. ¿Pero como asegurar que es una buena heurística? Un programa tan sencillo como un factorial tiene como dominio a la totalidad de los números enteros. Incluso restringiendo el mismo a un lenguaje de programación con enteros de 32 bits, existen 2^32 posibles entradas para el programa. Hacer 100 o hasta 1000 pruebas es totalmente insignificante. Para programas apenas un poco más complejos, las posibilidades se hacen completamente inmanejables. Claro, estas pruebas pueden diseñarse para englobar características de un gran conjunto de posibles entradas que, junto a la "buena intención" del programador, pueden dar una confianza aceptable en la calidad de la solución. ¿Pero cómo asegurar que se considera la totalidad de las opciones? Esto puede mejorarse incluso haciendo que el diseño de pruebas sea realizado por un equipo diferente e independiente del equipo implementador (de lo contrario, las pruebas estarán influenciadas por las propias expectativas de la solución ya planificada). Pero aún con todo esto, no parece ser suficiente.
Me imagino dos reacciones razonables al párrafo anterior: "Si, la estrategia tiene fallas, pero es mejor que nada" y la clásica "Bueno, ¿y tu que propones?" (no pude evitarlo, jajaja). La primera tiene toda la razón. Una estrategia que haga consciente al programador de lo que quiere implementar antes de empezar a echar código es mucho más responsable que la alternativa "dale play y si explota ahí vemos". Pero hay una opción que es mucho mejor y con eso aprovecho y respondo a la pregunta de la segunda reacción: el "contract driven development" o "design by contract" como lo llamó Bertrand Meyer (el pana que creó el lenguaje Eiffel). Sin embargo esto no es nuevo, viene de una larga línea de grandes pensadores de las ciencias de la computación en donde destacan personalidades como Edsger Dijkstra, Tony Hoare y hasta el mismísimo Alan Turing.
Este enfoque de diseño por contratos está basado en la filosofía de que todo programa puede especificarse formalmente. Esto es, las condiciones que se esperan al inicio, al final e invariantemente durante la ejecución del mismo pueden ser descritas por medio de alguna lógica formal. Esto se extiende a instrucciones, expresiones, tipos de datos, subrutinas, etc. Estas especificaciones o contratos se establecen antes de comenzar a implementar las soluciones y cumplen con dos objetivos importantes:
- Los programadores tienen una idea clara de lo que deben implementar, las garantías con las que cuenta su programa (la forma del dominio) y lo que se espera del mismo al finalizar su ejecución. Esto guiará una implementación mucho más sólida y clara.
- Una vez implementada la solución, la misma puede verificarse formalmente vía una demostración rigurosa que asegura, sin cabida a dudas, que la implementación cumple con lo establecido en su contrato.
Sin embargo, esta solución tampoco es perfecta y tiene una buena cantidad de fallas significativas:
- El contrato aún es tan bueno como la intuición de quien lo diseñó. Lo que se quiere puede estar mal especificado o, incluso, puede que no se esté claro siquiera cuál problema quiere resolverse realmente.
- El tiempo dedicado para diseñar contratos y la posterior verificación de los mismos es mucho más grande que en otras estrategias de desarrollo. Los resultados tangibles pueden verse sustancialmente atrasados.
- El contrato, similar a los casos de prueba, debe ser elaborado por un equipo independiente al implementador. La diferencia reside en que un contrato requiere de mucha más experiencia y formación que un caso de prueba, lo cual eleva las exigencias académicas y de personal necesarias para llevar a cabo un proyecto.
¿Pero entonces todo hay que demostrarlo? No necesariamente. Idealmente, si, eso daría el 100% de confianza. Pero el tiempo es un factor innegable y a veces de verdad no alcanza para todo el rigor involucrado en una prueba completa. Existen herramientas de verificación que permiten hacer algunas demostraciones de forma automática. Estas herramientas no son generales, ya que el problema de hacer una correspondencia de una implementación con un contrato se sabe indecidible. Sin embargo, pueden ahorrar bastante tiempo y esfuerzo en un principio y dar ciertas garantías sobre el comportamiento de un programa.
Hay que tomar en cuenta que tampoco es la intención que se elimine el testing por completo. Las máquinas no se comportan exactamente igual a la teoría; hay fallas inherentes a la implementación física de sus componentes. Además, pruebas concernientes al desempeño, a la resistencia ante muchas consultas simultáneas e incluso a la aceptación de la solución son aún necesarias, pero se delegarían a un siguiente paso del desarrollo.
Hay que tomar en cuenta que tampoco es la intención que se elimine el testing por completo. Las máquinas no se comportan exactamente igual a la teoría; hay fallas inherentes a la implementación física de sus componentes. Además, pruebas concernientes al desempeño, a la resistencia ante muchas consultas simultáneas e incluso a la aceptación de la solución son aún necesarias, pero se delegarían a un siguiente paso del desarrollo.
Esto era lo que quería compartir por ahora (otro post más gallo aún, jajaja). Esto de los métodos formales no sólo es una rama fascinante de la computación, sino una que creo necesaria conocer para ofrecer soluciones confiables y de calidad. Como siempre, cualquier opinión es más que bienvenida. ¡Hasta la próxima! :)