lunes, 21 de mayo de 2012

Emparchar vs Reescribir: ¿qué hacer con un diseño limitado?

Rara vez, al comenzar un proyecto personal, el diseñador/programador sabe exactamente a donde va a llegar. Puede tener una idea bastante aproximada de hacia donde quiere ir, y hasta de qué podría llegar a cambiar, pero inevitablemente algo va a pasar que lo va a obligar a plantearse la posibilidad de rediseñar medio programa. Solo un experto iluminado con gran experiencia puede proponer desde el comienzo el diseño adecuado en un proyecto más o menos complejo. En general, el programa muta, el programador madura, y las cosas cambian. La mayoría de los mortales, empezamos con un diseño que por alguna razón en algún momento pareció correcto (o tal vez no pareció del todo correcto, pero si pareció ser la mejor de las posibles opciones), y luego vemos como el proyecto, si sobrevive al embate inicial, va cambiando lenta O dolorosamente: lentamente cuando las cosas se hacen bien, dolorosamente cuando se hacen rápido.

Es decir, a la hora de implementar algo no previsto en el diseño inicial de un sistema, podemos tomarnos todo el tiempo necesario replantear el diseño inicial para hacerlo más flexible, recodificar lo que sea necesario recodificar para cumplir con el nuevo diseño, actualizar la documentación, etc. y luego agregar pacíficamente la nueva funcionalidad; o bien podemos empezar a parchear el código poco flexible que escribimos en un primer momento para tener rápidamente al menos un prototipo andando. Por lo general, cuando el cambio no es tan grande, voy por la segunda alternativa, hasta que llega un día en que mi código es 90% parches y 10% del pobre diseño original, y se hace imposible seguir añadiendo cosas (o al menos hacerlo sin romper lo que ya estaba andando).
Ese es el punto de la vida de todo programador en el que se dice a sí mismo: "pues bien, enfrentémoslo, llegó el día de reescribir todo". Y a partir de allí empieza una tarea titánica que resulta a la vez lenta Y dolorosa, y puede hasta derivar en el fin del proyecto si no se tiene la motivación suficiente. Lenta porque requiere mucho mucho trabajo, y dolorosa porque en el proceso se pierden funcionalidades que luego deben ser reimplementadas y testeadas otra vez. A nadie le gusta dar un paso hacia atrás. El usuario final no ve lo elegante u horripilante del diseño de clases por ejemplo, pero sí ve que en algún punto no solo se dejaron de agregar nuevas funcionalidades, sino que desaparecieron muchas que ya estaban andando.

Creo que no hay forma de evitar esto. Al principio planteé que un gran cambio iba a generar un proceso lento O doloroso, y luego expliqué que elegir el camino doloroso lleva a la larga a un proceso lento Y doloroso. Entonces ¿porqué no hacer las cosas bien y elegir el camino lento de entrada? ya que de todas formas vamos a pasar por un proceso lento, pero al menos así parece que evitamos la parte dolorosa. Pues bien, resulta que repensar el diseño a la primera de cambio puede llevarnos, sin la experiencia de haber cometido una cantidad de errores suficiente, a un segundo diseño que otra vez durará poco. Veo a la estrategia de emparchar para agregar funcionalidades como un camino por medio del cual puedo ir avanzando en la definición del problema que mi software quiere resolver y madurar mi entendimiento acerca de lo que necesito para hacerlo, de modo que cuando llegue el día en que decida replantear el diseño, tirar así todo, y reescribir lineas y lineas de código, tenga una visión más amplia e ideas más claras. Luego de 2 o 3 de estas grandes iteraciones uno llega a un buen diseño (o se cansa de reescribir :). Pero es de esperar que de a poco se tienda a un proyecto más estable y pulido. En general se traduce en código más limpio, mejor modularización y reutilización, clases y métodos más cortos, mejor y mayor documentación, etc.

Me pasó en los tres proyectos. PSeInt lo escribí cuando apenas empecé a programar en C++, y por ello recién empezaba a conocer la orientación a objetos, no tenía idea de cómo funcionaba la STL, y menos había oído hablar de autómatas, máquinas estado, y parseo serio, ni siquiera sabía bien para qué quería punteros. Por eso, el código fue (y en gran parte sigue siendo) sencillamente un asco. Una parte del intérprete la reescribí (la evaluación de expresiones y el manejo de variables en memoria), sin llegar a un diseño perfecto, pero ganando mucha claridad y flexibilidad, otra parte sigue siendo el desastre original cada vez más emparchado (el análisis sintáctico), y los otros módulos son más simples y/o recientes así que no presentan tanto problema. En ZinjaI, lo que cambió, más que mi forma de programar y conocimientos, fue el objetivo y alcance del proyecto. El diseño original sigue soportando parches, pero en algún momento tendré que repensar muchas cosas. Sin embargo, tengo esperanzas de que el cambio no será tan violento y se podrá implementar gradualmente. Finalmente, con MotoGT el cambio ya empezó, hace mucho que no agrego nada nuevo porque estoy reescribiendo clases completas y simplificando las cosas (además de fundamentalmente porque no tengo tiempo), pensando en hacerlo más flexible para que evolucione mejor en el futuro, y en que en algún momento pasaré de sfml-1.6 a sfml-2 (biblioteca que en el cambio de versión parece haber sufrido uno de estos procesos de los que hablo), y tener las cosas bien separadas y documentadas va a hacer que la transición sea mucho más fácil.

En conclusión, creo que los buenos diseños son hijos de la experiencia y el ingenio más que de la teoría, aunque la teoría es imprescindible para poder aprovechar esa combinación de experiencia e ingenio. Algunos programadores virtuosos son capaces de evitar estos errores; mientras que otros los cometemos, los aceptamos, y tratamos de evolucionar un poquito a través a de ellos. Lo importante es seguir aprendiendo sin perder el entusiasmo.


No hay comentarios:

Publicar un comentario