lunes, 1 de octubre de 2007

¿Cómo asegurar la evolución de un sistema?" - Parte II - Visitor

Que tal,
algo que estuve haciendo últimamente tiene que ver con cómo asegurar en un ambiente dinámicamente tipado la implementación correcta del patrón Visitor.
Los problemas que este patrón puede generar son:
1) La clase root de la jerarquía de clases que se visita no implementa el mensaje #accept: como subclassResponsibility
2) Los visitors no implementan todos los mensajes que envían lo objetos que se pueden visitar
3) Lo contrario de la 2), los visitors implementan mensajes que no se envían desde los objetos que se visitan

Aquellos programadores defensores del tipado estático arremeterían contra los lenguajes dinámicos argumentando que estos errores no sucederían en dichas herramientas. La verdad es que a simple vista pareciera que no sucederían, sin embargo no es tan así.
En el caso del punto 1) , este error se vería evidenciado solo al momento de utilizar un Visitor sobre dicha jerarquía y únicamente si la variable del objeto que se quiere visitar está "claseada" (tipada) con la clase root que acepta visitors. Este error puede por lo tanto, pasar inadvertido por bastante tiempo (a menos que se esté utilizando TDD)
El punto 2) es donde se hace efectivo el chequeo de tipos puesto que no sería factible que un objeto visitado pueda enviar un mensaje no definido en el visitor que recibe como colaborador. Sin embargo el punto 3), que es el caso que típicamente se da en la evolución de un sistema, no es evidenciado como error por un sistema de tipos. Este caso se da al borrar por ejemplo, una clase de la jerarquía que es visitada.
Como podemos ver, hay ciertos aspectos del patrón Visitor que un sistema de tipos no puede asegurar. Es por ello que muchas veces se ha hablado de reificar los patrones como "first class objects", hacerlos de alguna manera, parte del metamodelo.
Sin embargo, cómo puse más arriba, hay 3 condiciones que se deben cumplir para que un visitor esté correctamente implementado, es por ello que implementé 3 test utilizando SUnit que verifican dichas condiciones: (Estos test los llamamos test de "Estándares de Código" y solamente se corren antes de cerrar una "unidad de trabajo")
1) Toda clase que defina el mensaje #accept: debe tener una superclase que implemente el #accept: como subclassResponsibility o indicar cual es la clase root de la jerarquía que acepta el visitor (esta última parte se debe a aquellas clases que quedan mal clasificadas por diversos motivos, pero que aún así deben poder ser visitadas por el mismo visitor que las clases que se encuentran bien clasificadas). Es importante que este test no falle puesto que de hacerlo puede haber algún objeto instancia de una clase de la jerarquía visitada que no implemente el mensaje #accept:. Este test hace uso de otro test que verifica que todo mensaje definido como subclassResponsibility es implementado en las subclasses concretas.
2) Todo objeto que sepa responder a un mensaje #visitXXXX:, debe saber responder a todos los mensajes #visitXXX: que son enviados por los objetos que pertenecen a la jerarquía que define la clase root que acepta el visitor
3) Los mensajes #visitXXX: que sabe responder un objeto deben ser igual al protocolo que define el Visitor. Si hay algún otro mensaje #visitXXX: implementado por un objeto que no corresponde a los visitors que dicho objeto implementa, quiere decir que el método que implementa ese mensaje es candidato a ser eliminado.
Para implementar estos test reifique algunos conceptos como VisitorProtocol y VisitorImplementation, de tal manera que se puede crear el VisitorProtocol de esta manera:

visitorProtocol := VisitorProtocol for: aClassRootAcceptingVisitor

y luego colaborar con visitorProtocol de la siguiente manera:

visitorProtocol isValid <-- Indica si el protocolo es válido, i.e., no tiene mensajes #visitXXX: repetidos
visitorProtocol implementors <-- Devuelve todas las clases que implementan el protocolo

y con un VisitorImplementor se puede:

visitorImplementor isValid <-- Indica si este implementor implementa correctamente el visitor
visitorImplementor missingImplementors <-- Devuelve los mensajes que no están implementados por este visitor
visitorImplementor implementMissingImplementors <-- Crea un implementación default para todos aquellos mensajes que no están implementados
visitorImplementor extraImplementors <-- Devuelve los mensajes que sobran en la implementación
visitorImplementor deleteExtraImplementors <-- Borra los mensajes que sobran en la implementación

Lo interesante de esta implementación es que mientras se está desarrollando y experimentando con un modelo, se pueden romper estas reglas, de hecho se hace constantemente y solo son verificadas en el momento oportuno. Y como toda regla, puede tener sus excepciones, algo imposible de hacer con un sistema de tipos cerrado.
Conclusiones:
1) Reificar las reglas de desarrollo que define el grupo de trabajo en el que se encuentran
2) Implementar test que verifiquen dichas reglas
3) Hacer estos test de tal manera que solo corran en el momento oportuno, que no sean una molestia

En definitiva, hacer que los mismos objetos se responsabilicen de su evolución (en este caso a través de 1, 2 y 3).

2 comentarios:

Diego Fernández dijo...

A mi me siguen quedando dudas acerca de que tan bueno es el patron Visitor.

Lo copado de este pattern es que el objeto que recibe el #accept: es quien decide como se debe recorrer su propia estructura.

La parte que no me copa es justo la que trae los problemas que comentas en el post, la del double dispatch con el #visitXXX :(
Lo que no me queda claro es cuantas veces el nombre "XXX" del #visitXXX no depende de la clase en si, supongamos esta jerarquia:

DocumentElementBehavior (define accept:)
Paragraph>>accept: aVisitor
aVisitor visitParagraph: self


Ahora agrego una sub-clase de DocumentElementBehavior, que es un nuevo tipo de parrafo:

DocumentElementBehavior subclass: #MySuperSpecificParagraph
MySuperSpecificParagraph>>accept: aVisitor
aVisitor visitParagraph: self

De esta forma mi nueva sub-clase es compatible con los visitors existentes de la jerarquia de DocumentElementBehavior.
Pero que pasa si en algún otro contexto quiero diferenciar el comportamiento del visitor para un MySuperSpecificParagraph, bueno la opción que tengo es agregar un nuevo visitXXX:

MySuperSpecificParagraph>>accept: aVisitor
aVisitor visitMySuperSpecificParagraph: self

Y tocar todos los visitors existentes, para que en #visitMySuperSpecificParagraph: o bien no hagan nada o bien hagan self visitParagraph: aMySuperSpecificParagraph (suponiendo que MySuperSpecificParagraph es polimorfico a un Paragraph y que los quiero tratar igual en este contexto... lo cual no es un decisión menor).

El problema es que cuando la jerarquia de DocumentElementBehavior tiende a ser grande hay una proliferación enorme de visitXXX que hay que mantener (jejeje te imaginaras por que elegi el nombre MCPDocumentElementBehavior.. ejem quiero decir DocumentElementBehavior ;) ).

Me parece que otra forma de resolver ese double dispatch es el tipico "canHandle" (hey! debería haber un patron con ese nombre):

Paragraph>>accept: aVisitor
aVisitor visit: self

MyVisitor>>visit: aDocumentElement
(self handlerFor: aDocumentElement) value: aDocumentElement

MyVisitor>>handlerFor: aDocumentElement
^handlers detect: [:each | each canHandle: aDocumentElement ] ifNone: [self nullHandler]

Eso reduciria la proliferación de #visitXXX, y evitaria posibles DNUs... al costo que ahora detectar la ausencia de un "handler" para un DocumentElement especifico es más dificil, ouch!

Hernan Wilkinson dijo...

Que haces Diego!, gracias por el comentario... te respondo lo que pienso acá:
---- Comentario de Diego ---
Lo que no me queda claro es cuantas veces el nombre "XXX" del #visitXXX no depende de la clase en si, supongamos esta jerarquia:
---- Fin deComentario de Diego ---

Para mi dependen completamente del nombre de la clase. O sea, este patrón tiene por intención recorrer objetos pertenecientes a una jerarquía de clases y tratarlos de manera distinta según que tipo de objetos son (o lo que significa, de que clase son instancia)

---- Comentario de Diego ---
DocumentElementBehavior (define accept:)
Paragraph>>accept: aVisitor
aVisitor visitParagraph: self

Ahora agrego una sub-clase de DocumentElementBehavior, que es un nuevo tipo de parrafo: DocumentElementBehavior subclass:
#MySuperSpecificParagraph MySuperSpecificParagraph>>accept: aVisitor
aVisitor visitParagraph: self
---- Fin de Comentario de Diego ---
Desde mi punto de vista está mal. El mensaje visit a enviar debería ser visitMySuperSpecificParagraph, no visitParagraph porque ese mensaje se usa para los objetos instancia de la otra clase... de lo contrario no hay manera de distinguir el tipo de un objeto, objetivo principal de este patrón.

---- Comentario de Diego ---
De esta forma mi nueva sub-clase es compatible con los visitors existentes de la jerarquia de DocumentElementBehavior.
---- Fin Comentario de Diego ---

En realidad lo que hay que hacer es modificar los visitors para que tengan en cuenta el nuevo mensaje. El no hacerlo es patear un problema para más adelante.
---- Comentario de Diego ---
Pero que pasa si en algún otro contexto quiero diferenciar el comportamiento del visitor para un MySuperSpecificParagraph, bueno la opción que tengo es agregar un nuevo visitXXX:

MySuperSpecificParagraph>>accept: aVisitor
aVisitor visitMySuperSpecificParagraph: self

Y tocar todos los visitors existentes, para que en #visitMySuperSpecificParagraph: o bien no hagan nada o bien hagan self visitParagraph: aMySuperSpecificParagraph (suponiendo que MySuperSpecificParagraph es polimorfico a un Paragraph y que los quiero tratar igual en este contexto... lo cual no es un decisión menor).
------ fin de comentario ----
Eso es lo que haría siempre. O sea, agregar una clase a la jerarquía implica enviar un nuevo mensaje visitXXX y por lo tanto modificar todos los visitors que recorren esa jerarquía

---- Comentario de Diego ---
El problema es que cuando la jerarquia de DocumentElementBehavior tiende a ser grande hay una proliferación enorme de visitXXX que hay que mantener (jejeje te imaginaras por que elegi el nombre MCPDocumentElementBehavior.. ejem quiero decir DocumentElementBehavior ;) ).
---- Fin de Comentario de Diego ---
Si, me imagino jaja.
Pero bue, no queda otra, es una característica de este patrón. El problema no es que haya mucho mensajes sino no olvidarse de tener en cuenta ninguno... y para eso es la herramienta que hice. Si hay que analizar cada caso de la jerarquía no hay otra, no se puede evitar hacerlo.

---- Comentario de Diego ---
Me parece que otra forma de resolver ese double dispatch es el tipico "canHandle" (hey! debería haber un patron con ese nombre):

Paragraph>>accept: aVisitor
aVisitor visit: self

MyVisitor>>visit: aDocumentElement
(self handlerFor: aDocumentElement) value: aDocumentElement

MyVisitor>>handlerFor: aDocumentElement
^handlers detect: [:each | each canHandle: aDocumentElement ] ifNone: [self nullHandler]

Eso reduciria la proliferación de #visitXXX, y evitaria posibles DNUs...
---- Fin Comentario de Diego ---
No veo la ventaja... o sea, como decidir el handler hay que mantenerlo si se agregan nuevas clases también y hay que asegurarse de hacerlo. Además también hay que tener en cuenta los nuevos casos, no se puede safar de esto... hmm, no se, no veo que simplifique el problema...

Un abrazo,
Hernán