El libro del Dr. Yan Hong "JAVA and Patterns" comienza con una descripción del patrón Visitante:
El patrón de visitante es el patrón de comportamiento de los objetos. El propósito del patrón de visitante es encapsular algunas operaciones que se aplican a ciertos elementos de la estructura de datos. Una vez que es necesario modificar estas operaciones, la estructura de datos que acepta esta operación puede permanecer sin cambios.
El concepto de despacho.
El tipo cuando se declara una variable se denomina tipo estático de la variable (tipo estático), y algunas personas llaman al tipo estático tipo aparente (tipo aparente) y el tipo real del objeto al que hace referencia la variable también se denomina tipo aparente; tipo real de la variable (Tipo real). Por ejemplo:
Copie el código de código de la siguiente manera:
Lista lista = nula;
lista = nueva ArrayList();
Se declara una lista de variables, su tipo estático (también llamado tipo obvio) es Lista y su tipo real es ArrayList.
La selección de métodos según el tipo de objeto es el envío. Se divide en dos tipos, a saber, envío estático y envío dinámico.
El envío estático se produce en el momento de la compilación y el envío se produce en función de información de tipo estático. El envío estático no nos es ajeno. La sobrecarga de métodos es el envío estático.
El envío dinámico ocurre durante el tiempo de ejecución y el envío dinámico reemplaza dinámicamente un método.
despacho estático
Java admite el envío estático mediante la sobrecarga de métodos. Usando como ejemplo la historia de Mozi montando a caballo, Mozi podría montar un caballo blanco o un caballo negro. El diagrama de clases de Mozi y caballo blanco, caballo negro y caballo es el siguiente:
En este sistema, Mozi está representado por la clase Mozi. El código es el siguiente:
clase pública Mozi {
paseo público vacío (caballo h) {
System.out.println("equitación");
}
paseo público vacío (WhiteHorse wh) {
System.out.println("montando un caballo blanco");
}
paseo público vacío (BlackHorse bh) {
System.out.println("Montar el caballo oscuro");
}
público estático vacío principal (String [] argumentos) {
Caballo wh = nuevo WhiteHorse();
Caballo bh = nuevo BlackHorse();
Mozi mozi = nuevo Mozi();
mozi.ride(wh);
mozi.ride(bh);
}
}
Obviamente, el método ride() de la clase Mozi está sobrecargado por tres métodos. Estos tres métodos aceptan parámetros de Horse, WhiteHorse, BlackHorse y otros tipos respectivamente.
Entonces, ¿qué resultados imprimirá el programa cuando se ejecute? El resultado es que el programa imprime las mismas dos líneas de "caballo". En otras palabras, Mozi descubrió que lo único que montaba eran caballos.
¿Por qué? Las dos llamadas al método ride() pasan parámetros diferentes, a saber, wh y bh. Aunque tienen diferentes tipos reales, sus tipos estáticos son todos iguales, que son del tipo Caballo.
El envío de métodos sobrecargados se basa en tipos estáticos y este proceso de envío se completa en tiempo de compilación.
despacho dinámico
Java admite el envío dinámico mediante la anulación de métodos. Usando como ejemplo la historia de un caballo comiendo pasto, el código es el siguiente:
Copie el código de código de la siguiente manera:
caballo de clase pública {
comer vacío público(){
System.out.println("Caballo comiendo hierba");
}
}
Copie el código de código de la siguiente manera:
clase pública BlackHorse extiende Caballo {
@Anular
comer vacío público () {
System.out.println("Caballo oscuro comiendo hierba");
}
}
Copie el código de código de la siguiente manera:
Cliente de clase pública {
público estático vacío principal (String [] argumentos) {
Caballo h = nuevo BlackHorse();
calor();
}
}
El tipo estático de la variable h es Horse y el tipo real es BlackHorse. Si el método eat() en la última línea anterior llama al método eat() de la clase BlackHorse, entonces lo que se imprime arriba es "Caballo negro comiendo hierba", por el contrario, si el método eat() anterior llama a eat(; ) método de la clase Caballo, entonces lo que se imprime es "el caballo come hierba".
Por lo tanto, el núcleo del problema es que el compilador de Java no siempre sabe qué código se ejecutará durante la compilación, porque el compilador solo conoce el tipo estático del objeto, pero no conoce el tipo real del objeto ni el método; La llamada se basa en los tipos reales del objeto, no en los tipos estáticos. De esta manera, el método eat() en la última línea anterior llama al método eat() de la clase BlackHorse e imprime "caballo negro comiendo hierba".
tipo de despacho
El objeto al que pertenece un método se denomina receptor del método. El receptor del método y los parámetros del método se denominan colectivamente volumen del método. Por ejemplo, el código de copia de la clase Test en el siguiente ejemplo es el siguiente:
Prueba de clase pública {
impresión pública vacía (cadena de cadena) {
System.out.println(cadena);
}
}
En la clase anterior, el método print() pertenece al objeto de prueba, por lo que su receptor también es el objeto de prueba. El método print() tiene un parámetro llamado str y su tipo es String.
Dependiendo de en cuántos tipos de cantidades se pueda basar el envío, los lenguajes orientados a objetos se pueden dividir en lenguajes de envío único (Uni-Dispatch) y lenguajes de envío múltiple (Multi-Dispatch). Los lenguajes de envío único seleccionan métodos según el tipo de una instancia, mientras que los lenguajes de envío múltiple seleccionan métodos según el tipo de más de una instancia.
Tanto C++ como Java son lenguajes de envío único, y ejemplos de lenguajes de envío múltiple incluyen CLOS y Cecil. Según esta distinción, Java es un lenguaje dinámico de envío único, porque el envío dinámico de este lenguaje solo tiene en cuenta el tipo de receptor del método, y es un lenguaje estático de envío múltiple, porque este lenguaje distribuye métodos sobrecargados. Se tienen en cuenta el tipo de receptor del método y los tipos de todos los parámetros del método.
En un lenguaje que admite el envío único dinámico, hay dos condiciones que determinan a qué operación llamará una solicitud: una es el nombre de la solicitud y el tipo real del receptor. El envío único limita el proceso de selección de métodos de modo que solo se puede considerar una instancia, que suele ser el receptor del método. En el lenguaje Java, si se realiza una operación en un objeto de tipo desconocido, la prueba de tipo real del objeto solo ocurrirá una vez. Esta es la característica del envío único dinámico.
doble despacho
Un método decide ejecutar código diferente según los tipos de dos variables. Esto es "doble envío". El lenguaje Java no admite el envío múltiple dinámico, lo que significa que Java no admite el envío doble dinámico. Pero mediante el uso de patrones de diseño, el doble despacho dinámico también se puede implementar en el lenguaje Java.
En Java, se pueden lograr dos despachos mediante dos llamadas a métodos. El diagrama de clases es el siguiente:
Hay dos objetos en la imagen, el de la izquierda se llama Oeste y el de la derecha se llama Este. Ahora el objeto Oeste llama primero al método goEast() del objeto Este y se pasa a sí mismo. Cuando se llama al objeto Este, inmediatamente sabe quién es la persona que llama según los parámetros pasados, por lo que se llama a su vez al método goWest () del objeto "llamante". A través de dos llamadas, el control del programa se entrega a dos objetos por turno. El diagrama de secuencia es el siguiente:
De esta manera, hay dos llamadas a métodos: el control del programa se pasa entre los dos objetos, primero del objeto Oeste al objeto Este y luego de regreso al objeto Oeste.
Pero simplemente devolver el balón no resuelve el problema de la doble distribución. La clave es cómo utilizar estas dos llamadas y la función dinámica de despacho único del lenguaje Java para activar dos despachos únicos durante este proceso de paso.
El envío único dinámico en el lenguaje Java ocurre cuando una subclase anula un método de una clase principal. En otras palabras, tanto Oeste como Este deben ubicarse en su propia jerarquía de tipos, como se muestra a continuación:
código fuente
El código de copia de la clase West es el siguiente:
clase abstracta pública Oeste {
vacío abstracto público goWest1 (SubEast1 este);
vacío abstracto público goWest2 (SubEast2 este);
}
El código de copia de la clase SubWest1 es el siguiente:
la clase pública SubWest1 se extiende hacia el oeste{
@Anular
vacío público goWest1 (SubEast1 este) {
System.out.println("SubWest1 + " + east.myName1());
}
@Anular
vacío público goWest2 (SubEast2 este) {
System.out.println("SubWest1 + " + east.myName2());
}
}
Suboeste Clase 2
Copie el código de código de la siguiente manera:
la clase pública SubWest2 se extiende hacia el oeste{
@Anular
vacío público goWest1 (SubEast1 este) {
System.out.println("SubWest2 + " + east.myName1());
}
@Anular
vacío público goWest2 (SubEast2 este) {
System.out.println("SubWest2 + " + east.myName2());
}
}
El código de copia de la clase Este es el siguiente:
clase abstracta pública Este {
vacío abstracto público goEast (Oeste oeste);
}
El código de copia de la clase SubEast1 es el siguiente:
la clase pública SubEast1 se extiende hacia el este{
@Anular
vacío público goEast (Oeste oeste) {
west.goWest1(esto);
}
Cadena pública miNombre1(){
devolver "SubEste1";
}
}
El código de copia de la clase SubEast2 es el siguiente:
la clase pública SubEast2 se extiende hacia el este{
@Anular
vacío público goEast (Oeste oeste) {
west.goWest2(esto);
}
Cadena pública miNombre2(){
devolver "SubEste2";
}
}
El código de copia de la clase de cliente es el siguiente:
Cliente de clase pública {
público estático vacío principal (String [] argumentos) {
//combinación 1
Este este = nuevo SubEste1();
Oeste oeste = nuevo SubWest1();
este.goEast(oeste);
//combinación 2
este = nuevo SubEste1();
oeste = nuevo SubOeste2();
este.goEast(oeste);
}
}
Los resultados de la ejecución son los siguientes. Copie el código. El código es el siguiente.
Suboeste1 + Subeste1
Suboeste2 + Subeste1
Cuando el sistema se está ejecutando, primero se crean los objetos SubWest1 y SubEast1, y luego el cliente llama al método goEast () de SubEast1 y pasa el objeto SubWest1. Dado que el objeto SubEast1 anula el método goEast() de su superclase East, en este momento se produce un envío único dinámico. Cuando el objeto SubEast1 recibe la llamada, obtendrá el objeto SubWest1 del parámetro, por lo que inmediatamente llama al método goWest1() de este objeto y se pasa a sí mismo. Dado que el objeto SubEast1 tiene derecho a elegir qué objeto llamar, en este momento se realiza otro envío de método dinámico.
En este momento, el objeto SubWest1 ha obtenido el objeto SubEast1. Al llamar al método myName1 () de este objeto, puede imprimir su propio nombre y el nombre del objeto SubEast. El diagrama de secuencia es el siguiente:
Dado que uno de estos dos nombres proviene de la jerarquía oriental y el otro de la jerarquía occidental, su combinación se determina dinámicamente. Este es el mecanismo de implementación del doble despacho dinámico.
La estructura del patrón de visitante.
El patrón de visitante es adecuado para sistemas con estructuras de datos relativamente indeterminadas. Desacopla el acoplamiento entre la estructura de datos y las operaciones que actúan sobre la estructura, permitiendo que el conjunto de operaciones evolucione con relativa libertad. A continuación se muestra un diagrama simplificado del patrón de visitantes:
Cada nodo de la estructura de datos puede aceptar una llamada de un visitante. Este nodo pasa el objeto de nodo al objeto de visitante y el objeto de visitante, a su vez, realiza las operaciones del objeto de nodo. Este proceso se denomina "doble envío". El nodo llama al visitante, pasa y el visitante ejecuta un algoritmo contra este nodo. A continuación se muestra un diagrama de clases esquemático para el patrón Visitante:
Los roles involucrados en el modo visitante son los siguientes:
● Rol de visitante abstracto (Visitante) : declara una o más operaciones de método para formar la interfaz que todos los roles de visitante específicos deben implementar.
● Rol de visitante concreto (ConcreteVisitor) : implementa la interfaz declarada por el visitante abstracto, es decir, cada operación de acceso declarada por el visitante abstracto.
● Rol de nodo abstracto (Nodo) : declara una operación de aceptación y acepta un objeto visitante como parámetro.
● Rol ConcreteNode : implementa la operación de aceptación especificada por el nodo abstracto.
● Rol de objeto de estructura (ObjectStructure) : tiene las siguientes responsabilidades, puede atravesar todos los elementos de la estructura si es necesario, proporciona una interfaz de alto nivel para que los objetos visitantes puedan acceder a cada elemento si es necesario, puede diseñarse como un objeto compuesto o; Una colección, como Lista o Conjunto.
código fuente
Como puede ver, el rol de visitante abstracto prepara una operación de acceso para cada nodo específico. Como hay dos nodos, existen dos operaciones de acceso correspondientes.
Copie el código de código de la siguiente manera:
visitante de interfaz pública {
/**
* Corresponde a la operación de acceso del NodoA
*/
visita pública vacía (nodo NodoA);
/**
* Corresponde a la operación de acceso del NodoB
*/
visita pública vacía (nodo NodoB);
}
El código de copia de la clase visitante específica A es el siguiente:
la clase pública VisitorA implementa Visitante {
/**
* Corresponde a la operación de acceso del NodoA
*/
@Anular
visita pública vacía (nodo NodoA) {
System.out.println(nodo.operaciónA());
}
/**
* Corresponde a la operación de acceso del NodoB
*/
@Anular
visita pública vacía (nodo NodoB) {
System.out.println(nodo.operaciónB());
}
}
El código de copia de la clase VisitorB del visitante específico es el siguiente:
la clase pública VisitorB implementa Visitante {
/**
* Corresponde a la operación de acceso del NodoA
*/
@Anular
visita pública vacía (nodo NodoA) {
System.out.println(nodo.operaciónA());
}
/**
* Corresponde a la operación de acceso del NodoB
*/
@Anular
visita pública vacía (nodo NodoB) {
System.out.println(nodo.operaciónB());
}
}
El código de copia de la clase de nodo abstracto es el siguiente:
Nodo de clase abstracta pública {
/**
*Aceptar operación
*/
aceptación nula abstracta pública (visitante visitante);
}
Clase de nodo específica NodoA
Copie el código de código de la siguiente manera:
La clase pública NodoA extiende el Nodo{
/**
*Aceptar operación
*/
@Anular
aceptar public void (visitante visitante) {
visitante.visita(esto);
}
/**
*Método específico del NodoA
*/
operación de cadena públicaA(){
devolver "NodoA";
}
}
Clase de nodo específica NodoB
Copie el código de código de la siguiente manera:
La clase pública NodoB extiende el Nodo{
/**
*Aceptar método
*/
@Anular
aceptar public void (visitante visitante) {
visitante.visita(esto);
}
/**
*Métodos específicos del NodoB
*/
operación de cadena públicaB(){
devolver "NodoB";
}
}
Clase de rol de objeto estructural. Este rol de objeto estructural contiene una colección y proporciona el método add() al mundo exterior como una operación de gestión para la colección. Al llamar a este método, se puede agregar un nuevo nodo dinámicamente.
Copie el código de código de la siguiente manera:
clase pública Estructura de objetos {
Lista privada<Nodo> nodos = nueva ArrayList<Nodo>();
/**
* Ejecutar operación del método
*/
acción de anulación pública (visitante visitante) {
para (nodo nodo: nodos)
{
nodo.accept(visitante);
}
}
/**
* Agregar un nuevo elemento
*/
agregar vacío público (nodo nodo) {
nodos.add(nodo);
}
}
El código de copia de la clase de cliente es el siguiente:
Cliente de clase pública {
público estático vacío principal (String [] argumentos) {
//Crear un objeto de estructura
ObjectStructure os = nueva ObjectStructure();
//Añadir un nodo a la estructura
os.add(nuevo NodoA());
//Añadir un nodo a la estructura
os.add(nuevo NodoB());
//Crear un visitante
Visitante visitante = nuevo VisitanteA();
os.acción(visitante);
}
}
Aunque una estructura de árbol de objetos compleja con múltiples nodos de rama no aparece en esta implementación esquemática, en los sistemas reales el patrón de visitante generalmente se usa para manejar estructuras de árbol de objetos complejos, y el patrón de visitante puede usarse para lidiar con problemas de estructura de árbol que abarcan múltiples jerarquías. . Aquí es donde el patrón de visitantes es tan poderoso.
Diagrama de secuencia del proceso de preparación.
Primero, este cliente ilustrativo crea un objeto de estructura y luego pasa un nuevo objeto NodoA y un nuevo objeto NodoB.
En segundo lugar, el cliente crea un objeto VisitanteA y pasa este objeto al objeto de estructura.
Luego, el cliente llama al método de gestión de agregación de objetos de estructura para agregar los nodos NodoA y NodoB al objeto de estructura.
Finalmente, el cliente llama al método de acción action() del objeto de estructura para iniciar el proceso de acceso.
Diagrama de secuencia del proceso de acceso.
El objeto de estructura atravesará todos los nodos de la colección que guarda, que en este sistema son los nodos NodoA y NodoB. Primero, se accederá al NodoA. Este acceso consta de las siguientes operaciones:
(1) Se llama al método aceptar () del objeto NodoA y se pasa el objeto VisitorA;
(2) El objeto NodoA a su vez llama al método de acceso del objeto VisitanteA y pasa el objeto NodoA;
(3) El objeto VisitanteA llama al método único operaciónA() del objeto NodoA.
Por lo tanto, se completa el proceso de doble envío. Luego, se accederá al NodoB. El proceso de acceso es el mismo que el proceso de acceso al NodoA, que no se describirá aquí.
Ventajas del patrón de visitante
● Una buena extensibilidad puede agregar nuevas funciones a los elementos en la estructura del objeto sin modificar los elementos en la estructura del objeto.
● Una buena reutilización permite a los visitantes definir funciones comunes a toda la estructura del objeto, mejorando así el grado de reutilización.
● Separar comportamientos irrelevantes Puede utilizar visitantes para separar comportamientos irrelevantes y encapsular comportamientos relacionados para formar un visitante, de modo que la función de cada visitante sea relativamente única.
Desventajas del patrón de visitantes
● Es difícil cambiar la estructura del objeto. No es adecuado para situaciones en las que las clases en la estructura del objeto cambian con frecuencia. Debido a que la estructura del objeto cambia, la interfaz del visitante y la implementación del visitante deben cambiar en consecuencia, lo cual es demasiado costoso.
● Romper el patrón de visitante de encapsulación generalmente requiere que la estructura del objeto abra datos internos a los visitantes y a ObjectStructrue, lo que rompe la encapsulación del objeto.