¿Cuál es el punto del método accept () en el patrón Visitor?

Se habla mucho sobre desacoplamiento de los algoritmos de las clases. Pero, una cosa queda a un lado no explicada.

Usan visitantes como este

abstract class Expr { public  T accept(Visitor visitor) {visitor.visit(this);} } class ExprVisitor extends Visitor{ public Integer visit(Num num) { return num.value; } public Integer visit(Sum sum) { return sum.getLeft().accept(this) + sum.getRight().accept(this); } public Integer visit(Prod prod) { return prod.getLeft().accept(this) * prod.getRight().accept(this); } 

En lugar de llamar a visit (element) directamente, Visitor le pide al elemento que llame a su método de visita. Contradice la idea declarada de desconocimiento de clase sobre los visitantes.

PS1 Por favor, explique con sus propias palabras o señale la explicación exacta. Porque dos respuestas que obtuve se refieren a algo general e incierto.

PS2 Supongo que como getLeft() devuelve la Expression básica, la visit(getLeft()) llamada visit(getLeft()) daría como resultado la visit(Expression) , mientras que getLeft() llamada visit(this) dará como resultado otra invocación de visita más apropiada. Por lo tanto, accept() realiza la conversión de tipo (también conocido como casting).

Coincidencia de patrón de PS3 Scala = patrón de visitante en esteroide muestra cuánto más simple es el patrón de visitante sin el método de aceptación. Wikipedia agrega a esta afirmación : al vincular un documento que muestra “que los métodos de accept() son innecesarios cuando la reflexión está disponible, introduce el término ‘Walkabout’ para la técnica”.

Las construcciones de visit / accept del patrón de visitante son un mal necesario debido a la semántica de los lenguajes C (C #, Java, etc.). El objective del patrón de visitante es utilizar el doble despacho para enrutar su llamada como es de esperar al leer el código.

Normalmente, cuando se utiliza el patrón de visitante, se trata de una jerarquía de objetos donde todos los nodos se derivan de un tipo de Node base, al que se hace referencia en lo sucesivo como Node . Instintivamente, lo escribiríamos así:

 Node root = GetTreeRoot(); new MyVisitor().visit(root); 

Aquí radica el problema. Si nuestra clase MyVisitor se definió como la siguiente:

 class MyVisitor implements IVisitor { void visit(CarNode node); void visit(TrainNode node); void visit(PlaneNode node); void visit(Node node); } 

Si, en tiempo de ejecución, independientemente del tipo real de esa root , nuestra llamada iría a la visit(Node node) sobrecarga visit(Node node) . Esto sería cierto para todas las variables declaradas de tipo Node . ¿Por qué es esto? Debido a que Java y otros lenguajes tipo C solo consideran el tipo estático , o el tipo que la variable se declara como, del parámetro cuando se decide a qué sobrecarga llamar. Java no da el paso extra para preguntar, por cada llamada a un método, en tiempo de ejecución, “Está bien, ¿cuál es el tipo dynamic de root ? Oh, ya veo. Es un TrainNode . Veamos si hay algún método en MyVisitor que acepte un parámetro de tipo TrainNode … “. El comstackdor, en tiempo de comstackción, determina cuál es el método que se llamará. (Si Java efectivamente inspeccionó los tipos dynamics de los argumentos, el rendimiento sería bastante terrible).

Java nos proporciona una herramienta para tener en cuenta el tipo de tiempo de ejecución (es decir, dynamic) de un objeto cuando se llama a un método: envío de métodos virtuales . Cuando llamamos a un método virtual, la llamada va a una tabla en la memoria que consta de punteros de función. Cada tipo tiene una tabla. Si un método en particular es anulado por una clase, la entrada de la tabla de funciones de esa clase contendrá la dirección de la función anulada. Si la clase no anula un método, contendrá un puntero a la implementación de la clase base. Esto aún genera una sobrecarga de rendimiento (cada llamada de método básicamente eliminará dos punteros: uno que apunta a la tabla de funciones del tipo y otro a la función), pero aún es más rápido que tener que inspeccionar los tipos de parámetros.

El objective del patrón de visitantes es lograr un doble despacho : no solo se considera el tipo de objective de la llamada ( MyVisitor , a través de los métodos virtuales), sino también el tipo de parámetro (¿qué tipo de Node estamos viendo)? El patrón Visitor nos permite hacer esto mediante la combinación de visit / accept .

Al cambiar nuestra línea a esto:

 root.accept(new MyVisitor()); 

Podemos obtener lo que queremos: a través del envío de métodos virtuales, ingresamos la llamada accept () correcta según lo implementado por la subclase: en nuestro ejemplo con TrainElement , ingresaremos la implementación de accept() TrainElement :

 class TrainNode extends Node implements IVisitable { void accept(IVisitor v) { v.visit(this); } } 

¿Qué sabe el comstackdor en este momento, dentro del scope de la TrainNode de TrainNode ? Sabe que el tipo estático de this es un TrainNode . Esta es una importante porción adicional de información que el comstackdor no conocía en el scope de nuestro llamador: allí, todo lo que sabía sobre la root era que era un Node . Ahora el comstackdor sabe que this ( root ) no es solo un Node , sino que en realidad es un TrainNode . En consecuencia, la línea que se encuentra dentro de accept() : v.visit(this) , significa algo completamente diferente. El comstackdor ahora buscará una sobrecarga de visit() que toma un TrainNode . Si no puede encontrar uno, comstackrá la llamada a una sobrecarga que toma un Node . Si ninguno de los dos existe, obtendrá un error de comstackción (a menos que tenga una sobrecarga que tenga object ). La ejecución entrará así en lo que siempre habíamos intentado: la implementación de la visit(TrainNode e) . No se necesitaron moldes y, lo que es más importante, no se necesitó ninguna reflexión. Por lo tanto, la sobrecarga de este mecanismo es bastante baja: solo consiste en referencias de puntero y nada más.

Tiene razón en su pregunta: podemos usar un reparto y obtener el comportamiento correcto. Sin embargo, a menudo, ni siquiera sabemos qué tipo es Node. Tome el caso de la siguiente jerarquía:

 abstract class Node { ... } abstract class BinaryNode extends Node { Node left, right; } abstract class AdditionNode extends BinaryNode { } abstract class MultiplicationNode extends BinaryNode { } abstract class LiteralNode { int value; } 

Y estábamos escribiendo un comstackdor simple que analiza un archivo fuente y produce una jerarquía de objetos que cumple con la especificación anterior. Si estuviéramos escribiendo un intérprete para la jerarquía implementada como Visitante:

 class Interpreter implements IVisitor { int visit(AdditionNode n) { int left = n.left.accept(this); int right = n.right.accept(this); return left + right; } int visit(MultiplicationNode n) { int left = n.left.accept(this); int right = n.right.accept(this); return left * right; } int visit(LiteralNode n) { return n.value; } } 

Casting no nos llevaría muy lejos, ya que no conocemos los tipos de left o right en los métodos de visit() . Nuestro analizador probablemente también devuelva un objeto de tipo Node que apunta también a la raíz de la jerarquía, por lo que tampoco podemos lanzarlo con seguridad. Entonces nuestro sencillo intérprete puede verse así:

 Node program = parse(args[0]); int result = program.accept(new Interpreter()); System.out.println("Output: " + result); 

El patrón de visitante nos permite hacer algo muy poderoso: dada una jerarquía de objetos, nos permite crear operaciones modulares que operan sobre la jerarquía sin necesidad de poner el código en la clase de la jerarquía. El patrón de visitante se usa ampliamente, por ejemplo, en la construcción de comstackdores. Dado el árbol de syntax de un progtwig en particular, se escriben muchos visitantes que operan en ese árbol: la verificación de tipos, las optimizaciones, la emisión de códigos de máquina generalmente se implementan como diferentes visitantes. En el caso del visitante de optimización, incluso puede generar un nuevo árbol de syntax dado el árbol de entrada.

Tiene sus desventajas, por supuesto: si agregamos un nuevo tipo a la jerarquía, también necesitamos agregar un método visit() para ese nuevo tipo en la interfaz IVisitor , y crear implementaciones (completas) en todos nuestros visitantes. . También necesitamos agregar el método accept() también, por los motivos descritos anteriormente. Si el rendimiento no significa mucho para usted, existen soluciones para escribir visitantes sin la necesidad de accept() , pero normalmente implican una reflexión y, por lo tanto, puede incurrir en una sobrecarga considerable.

Por supuesto que sería una tontería si esa fuera la única forma en que se implementara Accept.

Pero no lo es.

Por ejemplo, los visitantes son realmente muy útiles cuando se trata de jerarquías, en cuyo caso la implementación de un nodo no terminal podría ser algo como esto

 interface IAcceptVisitor { void Accept(IVisit visitor); } class HierarchyNode : IAcceptVisitor { public void Accept(IVisit visitor) { visitor.visit(this); foreach(var n in this.children) n.Accept(visitor); } private IEnumerable children; .... } 

¿Lo ves? Lo que describes como estúpido es la solución para atravesar jerarquías.

Aquí hay un artículo mucho más extenso y profundo que me hizo entender al visitante .

Editar: para aclarar: el método de Visit del visitante contiene la lógica que se aplicará a un nodo. El método Accept del nodo contiene la lógica sobre cómo navegar a los nodos adyacentes. El caso en el que solo se envía por duplicado es un caso especial en el que simplemente no hay nodos adyacentes para navegar.

El propósito del patrón Visitor es garantizar que los objetos sepan cuándo el visitante finalizó con ellos y se fue, por lo que las clases pueden realizar la limpieza necesaria posteriormente. También permite a las clases exponer sus elementos internos “temporalmente” como parámetros de “ref”, y saber que las partes internas ya no estarán expuestas una vez que el visitante se haya ido. En los casos en que no es necesaria la limpieza, el patrón del visitante no es muy útil. Las clases que no hacen ninguna de estas cosas pueden no beneficiarse del patrón de visitante, pero el código que se escribe para usar el patrón de visitante se podrá utilizar con clases futuras que pueden requerir limpieza después del acceso.

Por ejemplo, supongamos que uno tiene una estructura de datos que contiene muchas cadenas que deben actualizarse atómicamente, pero la clase que contiene la estructura de datos no sabe con precisión qué tipos de actualizaciones atómicas se deben realizar (por ejemplo, si un hilo quiere reemplazar todas las apariciones de ” X “, mientras que otro subproceso quiere reemplazar cualquier secuencia de dígitos con una secuencia que sea numéricamente una más alta, las operaciones de ambos hilos deberían tener éxito, si cada hilo simplemente leyó una cadena, realizó sus actualizaciones y la escribió de nuevo, el segundo hilo volver a escribir su cadena sobrescribiría la primera). Una forma de lograr esto sería hacer que cada hilo adquiera un locking, realice su operación y libere el locking. Desafortunadamente, si los lockings se exponen de esa manera, la estructura de datos no tendría forma de evitar que alguien adquiera un locking y nunca lo libere.

El patrón Visitor ofrece (al menos) tres enfoques para evitar ese problema:

  1. Puede bloquear un registro, llamar a la función suministrada y luego desbloquear el registro; el registro podría bloquearse para siempre si la función suministrada cae en un bucle infinito, pero si la función suministrada devuelve o lanza una excepción, el registro se desbloqueará (puede ser razonable marcar el registro inválido si la función arroja una excepción; bloqueado probablemente no sea una buena idea). Tenga en cuenta que es importante que si la función llamada intenta adquirir otros lockings, se produzca un punto muerto.
  2. En algunas plataformas, puede pasar una ubicación de almacenamiento que contiene la cadena como un parámetro ‘ref’. Esa función podría entonces copiar la cadena, calcular una nueva cadena basada en la cadena copiada, intentar CompareExchange la cadena anterior a la nueva y repetir todo el proceso si falla CompareExchange.
  3. Puede hacer una copia de la cadena, llamar a la función suministrada en la cadena, luego usar CompareExchange para intentar actualizar el original y repetir todo el proceso si falla CompareExchange.

Sin el patrón de visitante, realizar actualizaciones atómicas requeriría exponer lockings y arriesgarse a fallar si el software de llamada no sigue un estricto protocolo de locking / deslocking. Con el patrón Visitor, las actualizaciones atómicas se pueden realizar de forma relativamente segura.

Las clases que requieren modificación deben implementar el método ‘aceptar’. Los clientes llaman a este método de aceptación para realizar alguna acción nueva en esa familia de clases, ampliando así su funcionalidad. Los clientes pueden usar este método de aceptación para realizar una amplia gama de acciones nuevas al pasar una clase de visitante diferente para cada acción específica. Una clase de visitante contiene múltiples métodos de visita anulados que definen cómo lograr esa misma acción específica para cada clase dentro de la familia. A estos métodos de visita se les pasa una instancia para trabajar.

Los visitantes son útiles si agrega, altera o elimina funcionalidades con frecuencia a una familia de clases estable porque cada elemento de funcionalidad se define por separado en cada clase de visitante y las clases en sí mismas no necesitan cambios. Si la familia de clases no es estable, el patrón de visitante puede ser de menor utilidad, porque muchos visitantes necesitan cambiar cada vez que se agrega o elimina una clase.

Un buen ejemplo es en la comstackción de código fuente:

 interface CompilingVisitor { build(SourceFile source); } 

Los clientes pueden implementar un JavaBuilder , RubyBuilder , XMLValidator , etc. y la implementación para recostackr y visitar todos los archivos fuente en un proyecto no necesita cambiar.

Este sería un mal patrón si tiene clases separadas para cada tipo de archivo fuente:

 interface CompilingVisitor { build(JavaSourceFile source); build(RubySourceFile source); build(XMLSourceFile source); } 

Todo se reduce al contexto y qué partes del sistema quieres que sean extensibles.