Se habla mucho de desvincular los algoritmos de las clases. Pero, una cosa queda de lado y no se explica.
Usan visitante como este
abstract class Expr {
public <T> T accept(Visitor<T> 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 (elemento) 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 Explique con sus propias palabras o señale una explicación exacta. Porque dos respuestas que obtuve se refieren a algo general e incierto.
PS2 Mi conjetura: dado que getLeft()
devuelve el básico Expression
, la llamada visit(getLeft())
daría como resultado visit(Expression)
, mientras que la getLeft()
llamada visit(this)
resultará en otra invocación de visita más apropiada. Entonces, accept()
realiza la conversión de tipo (también conocido como casting).
Coincidencia de patrones de PS3 Scala = Patrón de visitante en esteroides 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 artículo que muestra "que los accept()
métodos son innecesarios cuando la reflexión está disponible; introduce el término 'Paseo' para la técnica".
Respuestas:
Las construcciones
visit
/ del patrón de visitanteaccept
son un mal necesario debido a la semántica de los lenguajes similares a C (C #, Java, etc.). El objetivo del patrón de visitante es utilizar el envío doble para enrutar su llamada como esperaría al leer el código.Normalmente, cuando se utiliza el patrón de visitante, se involucra una jerarquía de objetos en la que todos los nodos se derivan de un
Node
tipo base , al que en adelante nos referiremos comoNode
. Instintivamente, lo escribiríamos así:Aquí radica el problema. Si nuestra
MyVisitor
clase se definió de la siguiente manera:Si, en tiempo de ejecución, independientemente del tipo real que
root
sea, nuestra llamada entraría en sobrecargavisit(Node node)
. Esto sería cierto para todas las variables declaradas de tipoNode
. ¿Por qué es esto? Porque Java y otros lenguajes similares a C solo consideran el tipo estático , o el tipo en el que se declara la variable, del parámetro al decidir a qué sobrecarga llamar. Java no da un paso adicional para preguntar, para cada llamada de método, en tiempo de ejecución, "Bien, ¿cuál es el tipo dinámico deroot
? Oh, ya veo. Es unTrainNode
. Veamos si hay algún método en elMyVisitor
que acepte un parámetro de tipoTrainNode
... ". El compilador, en tiempo de compilación, determina cuál es el método que se llamará. (Si Java efectivamente inspeccionara los tipos dinámicos 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, dinámico) de un objeto cuando se llama a un método: el envío de métodos virtuales . Cuando llamamos a un método virtual, la llamada en realidad va a una tabla en la memoria que consta de punteros de función. Cada tipo tiene una mesa. 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 todavía incurre en una sobrecarga de rendimiento (cada llamada de método básicamente eliminará la referencia a dos punteros: uno que apunta a la tabla de funciones del tipo y otro a la función en sí), pero aún es más rápido que tener que inspeccionar tipos de parámetros.
El objetivo del patrón de visitante es lograr un doble despacho : no solo se considera el tipo de destino de la llamada (
MyVisitor
, a través de métodos virtuales), sino también el tipo de parámetro (¿qué tipoNode
estamos viendo)? El patrón de visitante nos permite hacer esto mediante la combinaciónvisit
/accept
.Cambiando nuestra línea a esto:
Podemos obtener lo que queremos: a través del envío del método virtual, ingresamos la llamada de accept () correcta tal como la implementa la subclase; en nuestro ejemplo con
TrainElement
, ingresaremosTrainElement
la implementación deaccept()
:Lo que hace el compilador de conocimientos en este punto, dentro del alcance de
TrainNode
'saccept
? Sabe que el tipo estático dethis
es unTrainNode
. Este es un fragmento adicional importante de información que el compilador no conocía en el alcance de nuestro llamador: allí, todo lo que sabíaroot
era que era un archivoNode
. Ahora el compilador sabe quethis
(root
) no es solo unNode
, sino que en realidad es unTrainNode
. En consecuencia, la única línea que se encuentra dentroaccept()
:v.visit(this)
significa algo completamente diferente. El compilador ahora buscará una sobrecarga de lavisit()
que requiere unTrainNode
. Si no puede encontrar uno, compilará la llamada a una sobrecarga que requiere unNode
. Si no existe ninguno, obtendrá un error de compilación (a menos que tenga una sobrecargaobject
). La ejecución entrará así en lo que habíamos pretendido todo el tiempo:MyVisitor
la implementación devisit(TrainNode e)
. No se necesitaron yesos y, lo más importante, no se necesitó reflexión. Por lo tanto, la sobrecarga de este mecanismo es bastante baja: solo consta de referencias de puntero y nada más.Tiene razón en su pregunta: podemos usar un yeso y obtener el comportamiento correcto. Sin embargo, a menudo, ni siquiera sabemos qué tipo de Node es. Tome el caso de la siguiente jerarquía:
Y estábamos escribiendo un compilador simple que analiza un archivo fuente y produce una jerarquía de objetos que se ajusta a la especificación anterior. Si estuviéramos escribiendo un intérprete para la jerarquía implementada como Visitante:
Fundición que no nos llegue muy lejos, ya que no sabemos los tipos de
left
oright
en losvisit()
métodos. Lo más probable es que nuestro analizador también devuelva un objeto de tipoNode
que apunta a la raíz de la jerarquía, por lo que tampoco podemos convertirlo de forma segura. Entonces, nuestro intérprete simple puede verse así: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 propia clase de la jerarquía. El patrón de visitante se usa ampliamente, por ejemplo, en la construcción de compiladores. Dado el árbol de sintaxis de un programa en particular, se escriben muchos visitantes que operan en ese árbol: la verificación de tipos, las optimizaciones y la emisión de código de máquina se implementan generalmente como visitantes diferentes. En el caso del visitante de optimización, incluso puede generar un nuevo árbol de sintaxis dado el árbol de entrada.
Tiene sus inconvenientes, por supuesto: si agregamos un nuevo tipo en la jerarquía, también necesitamos agregar un
visit()
método para ese nuevo tipo en laIVisitor
interfaz y crear implementaciones stub (o completas) en todos nuestros visitantes. También necesitamos agregar elaccept()
método también, por las razones descritas anteriormente. Si el rendimiento no significa mucho para usted, existen soluciones para escribir a los visitantes sin necesidad deaccept()
, pero normalmente implican reflexión y, por lo tanto, pueden generar una gran sobrecarga.fuente
accept()
método se vuelve necesario cuando se infringe esta advertencia en el visitante.Por supuesto, sería una tontería si esa fuera la única forma en que se implementa Accept.
Pero no lo es.
Por ejemplo, los visitantes son realmente útiles cuando se trata de jerarquías, en cuyo caso la implementación de un nodo no terminal podría ser algo como esto
¿Lo ves? Lo que usted describe como estúpido es la solución para atravesar jerarquías.
Aquí hay un artículo mucho más largo y profundo que me hizo entender al visitante .
Editar: Para aclarar: el
Visit
método del visitante contiene lógica que se aplicará a un nodo. ElAccept
método del nodo contiene lógica sobre cómo navegar a los nodos adyacentes. El caso en el que solo realiza un envío doble es un caso especial en el que simplemente no hay nodos adyacentes hacia los que navegar.fuente
El propósito del patrón Visitante es asegurar que los objetos sepan cuando el visitante terminó con ellos y se fue, para que las clases puedan realizar cualquier limpieza necesaria después. También permite a las clases exponer sus componentes internos "temporalmente" como parámetros 'ref', y saber que los componentes internos ya no estarán expuestos una vez que el visitante se haya ido. En los casos en los que no es necesaria una limpieza, el patrón de visitantes 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 está escrito para usar el patrón de visitante se podrá usar 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 deben realizarse (por ejemplo, si un hilo quiere reemplazar todas las apariciones de " X ", mientras que otro hilo quiere reemplazar cualquier secuencia de dígitos con una secuencia numéricamente uno más alta, las operaciones de ambos hilos deberían tener éxito; si cada hilo simplemente lee una cadena, realiza sus actualizaciones y la vuelve a escribir, el segundo hilo escribir de nuevo su cadena sobrescribiría la primera). Una forma de lograr esto sería hacer que cada hilo adquiera un bloqueo, realice su operación y libere el bloqueo. Desafortunadamente, si las cerraduras se exponen de esa manera,
El patrón de visitante ofrece (al menos) tres enfoques para evitar ese problema:
Sin el patrón de visitante, realizar actualizaciones atómicas requeriría exponer bloqueos y arriesgarse a fallar si el software de llamada no sigue un protocolo estricto de bloqueo / desbloqueo. Con el patrón de visitante, las actualizaciones atómicas se pueden realizar de forma relativamente segura.
fuente
Todas las clases que requieren modificación deben implementar el método 'accept'. Los clientes llaman a este método de aceptación para realizar alguna acción nueva en esa familia de clases, extendiendo así su funcionalidad. Los clientes pueden utilizar este método de aceptación única para realizar una amplia gama de nuevas acciones pasando una clase de visitante diferente para cada acción específica. Una clase de visitante contiene varios métodos de visita anulados que definen cómo lograr esa misma acción específica para cada clase dentro de la familia. Estos métodos de visita pasan a una instancia en la que trabajar.
Los visitantes son útiles si con frecuencia agrega, modifica o quita funcionalidad a una familia estable de clases porque cada elemento de funcionalidad se define por separado en cada clase de visitante y las clases en sí no necesitan cambiarse. Si la familia de clases no es estable, entonces el patrón de visitantes puede ser de menor utilidad, porque muchos visitantes necesitan cambiar cada vez que se agrega o quita una clase.
fuente
Un buen ejemplo es la compilación del código fuente:
Los clientes pueden implementar una
JavaBuilder
,RubyBuilder
,XMLValidator
, etc, y la implantación de la recogida y visitar todos los archivos de origen en un proyecto no necesita cambio.Este sería un mal patrón si tiene clases separadas para cada tipo de archivo fuente:
Todo se reduce al contexto y a qué partes del sistema desea que sean extensibles.
fuente