¿Es útil el patrón de visitante para los lenguajes de tipo dynamic?

El patrón de visitante permite escribir operaciones en objetos sin extender la clase de objeto. Por supuesto. Pero, ¿por qué no solo escribir una función global, o una clase estática, que manipula mi colección de objetos desde el exterior? Básicamente, en un lenguaje como java, se necesita un método accept() por razones técnicas; pero en un lenguaje en el que puedo implementar el mismo diseño sin un método accept() , ¿el patrón de Visitante se vuelve trivial?

Explicación: en el patrón de visitante, las clases visitables (entidades) tienen un método .accept() cuyo trabajo es llamar el método .accept() del visitante en sí mismos. Puedo ver la lógica de los ejemplos de java: el visitante define un método .visit(n) para cada tipo visitable que soporta, y el truco .accept() debe usarse para elegir entre ellos en tiempo de ejecución. Pero los lenguajes como python o php tienen escritura dinámica y no sobrecargan los métodos. Si soy un visitante, puedo llamar a un método de entidad (por ejemplo, .serialize() ) sin saber el tipo de entidad o incluso la firma completa del método. (Ese es el problema del “doble despacho”, ¿verdad?)

Sé que un método de aceptación podría pasar datos protegidos al visitante, pero ¿qué sentido tiene? Si los datos se exponen a las clases de los visitantes, es efectivamente parte de la interfaz de la clase, ya que sus detalles son importantes fuera de la clase. La exposición de datos privados nunca me pareció el punto del patrón de visitante, de todos modos.

Entonces, parece que en python, ruby ​​o php puedo implementar una clase similar a un visitante sin un método de aceptación en el objeto visitado (y sin reflexión), ¿verdad? Si puedo trabajar con una familia de objetos heterogéneos y llamar a sus métodos públicos sin ninguna cooperación de la clase “visitada”, ¿esto todavía merece ser llamado el “patrón de visitante”? ¿Hay algo en la esencia del patrón que me falta, o simplemente se reduce a “escribir una nueva clase que manipule sus objetos desde el exterior para llevar a cabo una operación”?

PD. He visto mucha discusión sobre SO y otros sitios, pero no pude encontrar nada que aborde esta pregunta. Punteros de bienvenida.

    Esta respuesta se realiza con un desconocimiento de PHP, etc., pero el Visitador normalmente necesita llamar a más de un solo método (usted mencionó “serializar”) en las entidades. Cuando se llama al método Visit () en el visitante concreto, el visitante es capaz de ejecutar un código diferente para cada subtipo de entidad. No veo en qué se diferencia eso de un lenguaje de tipo dynamic (aunque me encantaría recibir algunos comentarios).

    Otra buena ventaja de Visitor es que proporciona una separación limpia del código que se ejecuta en cada entidad a partir del código que enumera las entidades. Esto me ha ahorrado una seria duplicación de código en al menos un proyecto grande.

    Además, he usado Visitante en idiomas que no tenían sobrecarga de métodos. Simplemente reemplace Visit (TypeN n) con VisitN (TypeN n).


    Seguimiento de comentarios.

    Este es un código psuedo para visitantes, y no sé cómo lo haría sin la cooperación del objeto visitado (al menos sin un interruptor de locking):

     abstract class ScriptCommand { void Accept(Visitor v); } abstract class MoveFileCommand { string TargetFile; string DestinationLocation; void Accept(Visitor v) { v.VisitMoveFileCmd(this); // this line is important because it eliminates the switch on object type } } abstract class DeleteFileCommand { string TargetFile; void Accept(Visitor v) { v.VisitDeleteFileCmd(this); // this line is important because it eliminates the switch on object type } } // etc, many more commands abstract class CommandVisitor { void VisitMoveFileCmd(MoveFileCommand cmd); void VisitDeleteFileCmd(DeleteFileCommand cmd); // etc } // concrete implementation class PersistCommandVisitor() inherits CommandVisitor { void VisitMoveFileCmd(MoveFileCommand cmd) { // save the MoveFileCommand instance to a file stream or xml doc // this code is type-specific because each cmd subtype has vastly // different properties } void VisitDeleteFileCmd(DeleteFileCommand cmd) { // save the DeleteFileCommand instance to a file stream or xml doc // this code is type-specific because each cmd subtype has vastly // different properties } } 

    La infraestructura del visitante permite el manejo de una amplia gama de subtipos de comando sin un caso de selección, swithc, si no.

    En lo que respecta al visitante que maneja la enumeración, creo que te estás limitando así. Eso no quiere decir que no pueda participar una clase colaboradora (un Visitador de visitas abstracto).

    Por ejemplo, tenga en cuenta que este visitante desconoce el orden de enumeración:

     class FindTextCommandVisitor() inherits CommandVisitor { string TextToFind; boolean TextFound = false; void VisitMoveFileCmd(MoveFileCommand cmd) { if (cmd.TargetFile.Contains(TextToFind) Or cmd.DestinationLocation.Contains(TextToFind)) TextFound = true; } void VisitDeleteFileCmd(DeleteFileCommand cmd) { // search DeleteFileCommand's properties } } 

    Y esto permite reutilizarlo así:

     ScriptCommand FindTextFromTop(string txt) { FindTextCommandVisitor v = new FindTextCommandVisitor(); v.TextToFind = txt; for (int cmdNdx = 0; cmdNdx < CommandList.Length; cmdNdx++) { CommandList[cmdNdx].Accept(v); if (v.TextFound) return CommandList[cmdNdx]; // return the first item matching } } 

    y el enumerar de manera opuesta con el mismo visitante:

     ScriptCommand FindTextFromBottom(string txt) { FindTextCommandVisitor v = new FindTextCommandVisitor(); v.TextToFind = txt; for (int cmdNdx = CommandList.Length-1; cmdNdx >= 0; cmdNdx--) { CommandList[cmdNdx].Accept(v); if (v.TextFound) return CommandList[cmdNdx]; // return the first item matching } } 

    En el código real, crearía una clase base para el enumerador y luego una subclase para manejar los diferentes escenarios de enumeración, mientras se pasa la subclase de Visitante concreta para desacoplarlos por completo. Esperemos que pueda ver el poder de mantener la enumeración separada.

    El lugar donde el Visitante es particularmente útil es donde el Visitante necesita activar el tipo de Visitees, y por cualquier razón, no desea codificar ese conocimiento en las Visitees (piense en las architectures de complementos). Considere el siguiente código de Python:

    Estilo de visitante

     class Banana(object): def visit(self, visitor): visitor.process_banana(self) class Apple(object): def visit(self, visitor): visitor.process_apple(self) class VisitorExample(object): def process_banana(self, banana): print "Mashing banana: ", banana def process_banana(self, apple): print "Crunching apple: ", apple 

    (Tenga en cuenta que podríamos comprimir la lógica visitee con una clase base / mixin).

    Comparar con:

    Estilo no visitante

     class NonVisitorVisitor(object): def process(self, fruit): verb = {Banana: "Mashing banana: ", Apple: "Crunching apple: "}[type(fruit)] print verb, fruit 

    En el segundo ejemplo, los frutos no necesitan ningún soporte especial para el “visitante”, y el “visitante” se encarga de la ausencia de lógica para el tipo dado.

    Por el contrario, en Java o C ++, el segundo ejemplo no es realmente posible, y el método de visita (en las visitas) puede usar un nombre para referirse a todas las versiones del método de proceso; el comstackdor elegirá la versión que se aplica al tipo que se está pasando; y el visitante puede proporcionar fácilmente una implementación predeterminada para la clase raíz para el tipo de visitas. También es necesario tener un método de visita en las visitas porque la variante del método (por ejemplo, process(Banana b) vs process(Apple a) ) se selecciona en el momento de la comstackción en el código generado para el método de visit las visit .

    En consecuencia, en lenguajes como Python o Ruby donde no hay un envío de tipos de parámetros (o, más bien, el progtwigdor tiene que implementarlo ellos mismos), no es necesario el patrón de visitante. Alternativamente, se podría decir que el patrón de visitante se implementa mejor sin el envío a través de los métodos de visitee.

    En general, en lenguajes dynamics como Python, Ruby o Smalltalk, es mejor que las clases “visitee” lleven toda la información necesaria (aquí, el verbo aplicable) y, si es necesario, proporcionen enlaces para apoyar al “visitante”, como como patrones de comando o estrategia, o use el patrón de No visitante que se muestra aquí.

    Conclusión

    El no visitante es una forma limpia de implementar la lógica de cambio de tipo, a pesar de que el cambio de tipo explícito suele ser un olor de código. Recuerde que la forma de hacerlo de Java y C ++ también es un cambio explícito en el visitante; La elegancia del patrón en esos idiomas es que evita tener una lógica de conmutación explícita en las visitas, lo que no es posible en lenguajes dynamics con variables sin tipo. En consecuencia, el patrón de Visitador en la parte superior es malo para los lenguajes dynamics porque reproduce el pecado que el patrón de Visitante en los idiomas estáticos trata de evitar.

    Lo que sucede con el uso de patrones es que, en lugar de reproducir de forma servil los diagtwigs UML, debe comprender lo que intentan lograr y cómo logran esos objectives con la maquinaria lingüística concretamente en consideración. En este caso, el patrón para lograr los mismos méritos parece diferente y tiene un patrón diferente de llamadas. Si lo hace, le permitirá adaptarlos a diferentes idiomas, pero también a diferentes situaciones concretas dentro del mismo idioma.

    Actualización: aquí hay un artículo de Ruby sobre la implementación de este patrón: http://blog.rubybestpractices.com/posts/aaronp/001_double_dispatch_dance.html

    El doble despacho me parece bastante forzado; Podrías eliminarlo, por lo que puedo decir.

    Creo que estás usando patrón de visitante y doble despacho indistintamente. Cuando tu dices,

    Si puedo trabajar con una familia de objetos heterogéneos y llamar a sus métodos públicos sin ninguna cooperación de la clase “visitada”, ¿esto todavía merece ser llamado el “patrón de visitante”?

    y

    escribe una nueva clase que manipule tus objetos desde el exterior para llevar a cabo una operación “?

    Estás definiendo qué es el doble despacho. Claro, el patrón de visitante se implementa por doble despacho. Pero hay algo más en el patrón en sí.

    • Cada visitante es un algoritmo sobre un grupo de elementos (entidades) y se pueden conectar nuevos visitantes sin cambiar el código existente. Principio abierto / cerrado.
    • Cuando se agregan nuevos elementos con frecuencia, es mejor evitar el patrón de visitante

    Tal vez, depende del idioma.

    El patrón de visitante resuelve problemas de jerarquía múltiple y doble en idiomas que no cuentan con múltiples despachos . Toma Ruby, Lisp y Python. Todos son lenguajes de tipo dynamic, pero solo CLOS-Lisp implementa múltiples despachos en el estándar. Esto también se llama multimethods y Python y Ruby pueden implementarlo aparentemente mediante el uso de extensiones.

    Me gusta este curioso comentario en wikipedia que dice que:

    El sistema de objetos de Lisp [CLOS] con su envío múltiple no reemplaza el patrón de visitante, sino que simplemente proporciona una implementación más concisa del mismo en la que el patrón desaparece.

    En otros idiomas, incluso los de tipo estático, debe evitarse la ausencia de métodos múltiples. El patrón de visitante es una de esas formas.

    El patrón de visitante para mí significaba agregar nuevas funciones a los objetos según su tipo. Aparentemente, tener si / else escalas para realizar operaciones de tipo específico es malo (me gustaría una explicación para esto :(). En Python, pude hacer esto, sin todo el dtwig de doble despacho, por Monkeypatching (otra mala idea) cierto Funciona como métodos de clase.

    Le pregunté acerca de esto aquí .

    En el siguiente ejemplo, suponga que hay una clase base ASTNode y una gran jerarquía de clases debajo de ella ( ASTVar , ASTModule , ASTIf , ASTConst , etc.). Estas clases solo tienen sus atributos de datos específicos y métodos triviales.

    Luego, asum que el código de clase está bloqueado (o tal vez la funcionalidad está separada de los datos). Ahora, tengo métodos que se asignan dinámicamente a las clases. Tenga en cuenta que en el ejemplo siguiente, el nombre de la llamada al método de iteración / recursión (stringify) es diferente del nombre de la función ( nodeType _stringify).

     def ASTNode__stringify(self): text = str(self) for child in self.children: text += ", { " + child.stringify() + " }" return text def ASTConst__stringify(self): text = str(self) for child in self.children: text += ", [ " + child.stringify() + " ]" return text def ASTIf__stringify(self): text = str(self) text += "__cond( " + self.op1.stringify() + ")" text += "__then { " + self.op2.stringify() + "}" text += "__else {" + self.op3.stringify() + "}" return text 

    Puedo extender las clases (posiblemente, una sola vez durante el inicio del módulo) con funcionalidad siempre que quiera (¿mala idea?).

     # mainModule1.py def extend_types(): # ASTNode and all derived class get this method ASTNode.stringify = ASTNode__stringify ASTConst.stringify = ASTConst__stringify ASTIf.stringify = ASTIf__stringify 

    Ahora, llamar a my_root_node.stringify() llamaría apropiadamente a los métodos secundarios correctos (recursivamente), sin verificar explícitamente el tipo.

    ¿No es esta técnica similar a agregar métodos a los prototipos de Javascript ( patrón de visitante en JS )?

    ¿No era este el objective de Visitor Pattern? Extensión de los tipos de código bloqueado? Seguramente, la necesidad de usar doble despacho ( VisitorObject.visit(ConcreteObject) es llamado por ConcreteObject.Accept(VisitorObject) ) no sería necesaria en python, que se tipifica dinámicamente. Probablemente, alguien formalizará esto para idiomas tipificados dinámicamente, y tendremos un nuevo patrón a la mano, o no. Después de todo, los patrones se descubren, no se inventan (no recuerdo dónde leí esto).

    Patrón de visitante hacer 2 cosas:

    • Permite el polymorphism ad hoc (la misma función, pero hace diferentes cosas a diferentes “tipos”).
    • Permite agregar un nuevo algoritmo de consumo sin cambiar el proveedor de datos.

    Puede hacer el segundo en idiomas dynamics sin Visitante ni información de tipo de tiempo de ejecución. Pero primero requiere algún mecanismo explícito, o patrón de diseño como Visitante.