lunes, 22 de octubre de 2007

Sobre bloques

Que tal,
hace un par semanas que no puedo escribir porque la organización de Smalltalks 2007 me está llevando más tiempo del pensado, pero lo estamos haciendo con mucho entusiasmo así que es tiempo bien invertido.
Lo que quería hacer hoy era reflexionar sobre los bloques de Smalltalk. Como todos saben, los bloques son objetos que representan colaboraciones, o sea, en un lenguaje más básico, código.
Idealmente, los bloques deberían ser polimórficos con los métodos compilados, que son los otros objetos que modelan colaboraciones entre objetos, sin embargo en la realidad no lo son y además poseen algunas pequeñas diferencias que no tiene sentido nombrar ahora porque están fuera del objetivo de este post.
En realidad lo que me interesa es conversar un poco sobre las desventajas de los bloques puesto que las ventajas saltan a la vista (siendo la más importante, desde el punto de vista de "computer science" que representan funciones lambda).
Uno de los problemas surge al momento de enseñar qué son los bloques. Es muy difícil para los que recién empiezan a utilizar Smalltalk (y más si no conocen Lisp o algo similar) entender que las colaboraciones también se modelan con objetos. No entiendan que los bloques son objetos y los ven como construcciones sintácticas. En particular resulta complicado que entiendan que el contexto de evaluación del bloque no es el bloque mismo sino el contexto en el cual fue creado el bloque.
Otra desventaja es la tentación de poner colaboraciones muy complejas o muchas colaboraciones en un bloque. En realidad podemos decir que esto sucede con cualquier método, no solo bloques, pero como supuestamente los bloques existen para poder "escribir código" rápidamente es que parece que nos olvidamos que también deben seguir ciertas reglas básicas. Una cosa es "escribir código" rápidamente cuando se está trabajando en inspectores, workspace, etc., y otra es "escribir código" en un browser, puesto que este perdura.
Es muy común ver este caso cuando se usa el mensaje #select:, donde el motivo de la selección de objetos implica verificar muchas cosas sobre el objeto en cuestión y entonces se empiezan a componer condiciones lógicas con #and: y #or: que no permiten entender a primera vista la intención del #select:. Por ejemplo:

....
trades select: [ :aTrade |
(aTrade isPurchase or: [aTrade isSale])
and: [Date today bettween: aTrade aggrementDate and: aTrade settlementDate]].
....

Este ejemplo adolece de varios males. El primero, como comenté, no se entiende qué se está seleccionando a simple vista, hay que analizarlo detenidamente. El segundo un poco más conceptual, es que seguramente hay alguna característica del objeto en cuestión que debería saber responder él, debería reificarla él y no aquellos que lo usan.
Para resolver el primer caso, siempre mi consejo es no haya más de un envío de mensaje dentro de un bloque, y que dicho mensaje "revele la intención" del bloque. Por ejemplo:
....
trades select: [ :aTrade | self isActive: aTrade ]
....

Un comentario al margen; fijensé que el mensaje se llama #isActive: y no #isTradeActive: puesto que se lee mucho mejor "self isActive: aTrade" que "self isTradeActive: aTrade". El segundo caso comete el error de poner el tipo del colaborador en el nombre del mensaje, un error muy común realizado por aquellos que vienen de los lenguajes "parentisados" puesto que en dichos lenguajes deben poner en el nombre si o si alguna referencia a los colaboradores puesto que no se los lee de manera tan similar al lenguaje natural como Smalltalk.
Volviendo al ejemplo, ahora se puede leer mucho más claro qué objetos se están seleccionando y además en caso de querer modificar el motivo de selección, no habrá problema en hacerlo. Mientras se está iterando la colección, se puede modificar el método #isActive: sin problema, modificando el comportamiento on the fly y sin necesidad de reiniciar la ejecución de select.
Si por el contrario, dejamos el bloque como el primer ejemplo, cada vez que se lo modifique, el debugger reiniciará la ejecución desde el principio del método donde está definido el bloque. Esto es necesario porque el contexto de evaluación dejó de ser válido y es necesario reiniciarlo, aunque se podría argumentar que se modificó el bloque y no el método per-se.
Peor aún si tenemos el siguiente código:
....
trades select: self activeTradeBlock
....
activeTradeBlock

^[ :aTrade | (aTrade isPurchase or: [aTrade isSale])
and: [Date today bettween: aTrade aggrementDate and: aTrade settlementDate]]
....
En este caso, si queremos modificar el bloque desde el debugger, no podremos. En VisualWorks saldrá un error que dice que no se puede modificar el bloque porque su contexto no está en el stack de ejecución. En el caso de VisualAge, no chistará pero tampoco modificará el bloque.
Más confusión trae a los principiantes si van y modifican el método #activeTradeBlock desde un browser mientras están iterando los trades con el debugger puesto que el cambio no surtirá efecto!! y claro, no surtirá efecto porque el objeto instancia de Block que contiene las colaboraciones que se están evaluando ya existe y no se creará uno nuevo hasta que no se evalúe el método #activeTradeBlock nuevamente.
Es por este motivo que también aconsejo poner siempre un solo mensaje en el bloque puesto que si el ejemplo dijera:
....
trades select: self activeTradeBlock
....
activeTradeBlock

^[ :aTrade | self isActive: aTrade ]
....

modificar el comportamiento de #isActive: es factible y se logra la flexibilidad y dinamismo esperado de un ambiente de objetos.
Por último, y yendo al plano conceptual, el encargado de tener la responsabilidad de decidir si está activo o no es el mismo trade, por lo tanto el ejemplo debería quedar:
....
trades select: [ :aTrade | aTrade isActive ]
....

De esta manera no se pierde flexibilidad y además la responsabilidad está bien otorgada. (No siempre se puede delegar la responsabilidad de selección en el objeto que se está analizando, por eso dejé el ejemplo donde se envía el mensaje #isActive:, pero está claro que lo mejor es utilizar este último caso)
Otro problema que traen los bloques se observa trabajando con objetos distribuidos. ¿Qué significa distribuir un bloque es está bindeado a un stack de ejecución particular? En particular, utilizando GemStone se encuentra esta limitación rápidamente si se intenta pasar un bloque como objeto remoto (GemStone provee soluciones, como utilizar un decompilador para recrear el bloque en la imagen remota, pero no siempre dicho decompilador está disponible como por ejemplo en VisualAge). Para estos casos, la mejor solución es crear un objeto que sea polimórfico con el bloque que se utilizaría. Por ejemplo:
....
trades select: IsActiveTradeCondition new
....
IsActiveCondition>>value: aTrade

^aTrade isActive

....
La desventaja que tiene esta opción es que se pierde justamente la facilidad que ofrecen los bloques de no tener que crear un clase cada vez que se quiere modelar colaboraciones o que los mismos sean expresiones lamda, pero se gana en la posibilidad de modificar el comportamiento dinámicamente y en poder distribuir el objeto sin problema.
Un caso concreto de abuso de bloques es el framework de SmallLint. En esta herramienta las reglas pueden ser modeladas con bloques, sin embargo el código de las reglas que provee está escrita en el bloque mismo, produciendo los problemas ya comentados. Me acuerdo haber realizado varios extract method para poder modificar las reglas existentes de manera dinámica puesto que sino, me sucedía lo comentado más arriba.
En conclusión, los bloques son muy útiles, pero al momento de no estar trabajando únicamente con inspectores o el workspace, hay que usarlos con más cuidado y sin olvidar, como siempre, que el código que uno escribe debe poder ser modificado dinámicamente y que seguramente será leído muchas más veces que escrito y por lo tanto cuanto menos esfuerzo de lectura implique, mejor.

No hay comentarios.: