El envío doble es solo una razón entre otras para usar este patrón .
Pero tenga en cuenta que es la única forma de implementar el envío doble o más en idiomas que utiliza un paradigma de envío único.
Aquí hay razones para usar el patrón:
1) Queremos definir nuevas operaciones sin cambiar el modelo en cada momento porque el modelo no cambia a menudo, mientras que las operaciones cambian con frecuencia.
2) No queremos unir modelo y comportamiento porque queremos tener un modelo reutilizable en múltiples aplicaciones o queremos tener un modelo extensible que permita a las clases de clientes definir sus comportamientos con sus propias clases.
3) Tenemos operaciones comunes que dependen del tipo concreto del modelo, pero no queremos implementar la lógica en cada subclase, ya que explotaría la lógica común en múltiples clases y, por lo tanto, en múltiples lugares .
4) Estamos utilizando un diseño de modelo de dominio y las clases de modelo de la misma jerarquía realizan demasiadas cosas distintas que podrían reunirse en otro lugar .
5) Necesitamos un doble despacho .
Tenemos variables declaradas con tipos de interfaz y queremos poder procesarlas de acuerdo con su tipo de tiempo de ejecución ... por supuesto sin usar if (myObj instanceof Foo) {}
ni ningún truco.
La idea es, por ejemplo, pasar estas variables a métodos que declaran un tipo concreto de la interfaz como parámetro para aplicar un procesamiento específico. Esta forma de hacerlo no es posible desde el primer momento, ya que los idiomas se basan en un único envío porque la invocación elegida en tiempo de ejecución depende solo del tipo de tiempo de ejecución del receptor.
Tenga en cuenta que en Java, el método (firma) para llamar se elige en tiempo de compilación y depende del tipo declarado de los parámetros, no de su tipo de tiempo de ejecución.
El último punto que es una razón para usar el visitante también es una consecuencia porque a medida que implementa el visitante (por supuesto, para idiomas que no admiten el envío múltiple), necesariamente debe introducir una implementación de envío doble.
Tenga en cuenta que el recorrido de elementos (iteración) para aplicar el visitante en cada uno no es una razón para usar el patrón.
Utiliza el patrón porque divide el modelo y el procesamiento.
Y al usar el patrón, se beneficia además de una capacidad de iterador.
Esta capacidad es muy poderosa y va más allá de la iteración en el tipo común con un método específico como accept()
es un método genérico.
Es un caso de uso especial. Así que lo pondré a un lado.
Ejemplo en Java
Ilustraré el valor agregado del patrón con un ejemplo de ajedrez en el que nos gustaría definir el procesamiento cuando el jugador solicita que se mueva una pieza.
Sin el uso del patrón visitante, podríamos definir comportamientos de movimiento de piezas directamente en las subclases de piezas.
Podríamos tener, por ejemplo, una Piece
interfaz como:
public interface Piece{
boolean checkMoveValidity(Coordinates coord);
void performMove(Coordinates coord);
Piece computeIfKingCheck();
}
Cada subclase de pieza lo implementaría como:
public class Pawn implements Piece{
@Override
public boolean checkMoveValidity(Coordinates coord) {
...
}
@Override
public void performMove(Coordinates coord) {
...
}
@Override
public Piece computeIfKingCheck() {
...
}
}
Y lo mismo para todas las subclases de piezas.
Aquí hay una clase de diagrama que ilustra este diseño:
Este enfoque presenta tres inconvenientes importantes:
- comportamientos como performMove()
o computeIfKingCheck()
muy probablemente usarán una lógica común.
Por ejemplo, cualquiera que sea el concreto Piece
, performMove()
finalmente establecerá la pieza actual en una ubicación específica y potencialmente tomará la pieza oponente.
Dividir comportamientos relacionados en múltiples clases en lugar de reunirlos derrota de alguna manera el patrón de responsabilidad única. Haciendo más difícil su mantenibilidad.
- procesar como checkMoveValidity()
no debería ser algo que las Piece
subclases puedan ver o cambiar.
Es un control que va más allá de las acciones humanas o informáticas. Esta verificación se realiza en cada acción solicitada por un jugador para garantizar que el movimiento de la pieza solicitada sea válido.
Por lo tanto, ni siquiera queremos proporcionar eso en la Piece
interfaz.
- En los juegos de ajedrez desafiantes para los desarrolladores de bot, generalmente la aplicación proporciona una API estándar ( Piece
interfaces, subclases, tablero, comportamientos comunes, etc.) y permite a los desarrolladores enriquecer su estrategia de bot.
Para poder hacer eso, tenemos que proponer un modelo donde los datos y los comportamientos no estén estrechamente acoplados en las Piece
implementaciones.
¡Así que vamos a usar el patrón de visitante!
Tenemos dos tipos de estructura:
- las clases modelo que aceptan ser visitadas (las piezas)
- los visitantes que los visitan (operaciones de mudanza)
Aquí hay un diagrama de clase que ilustra el patrón:
En la parte superior tenemos los visitantes y en la parte inferior tenemos las clases modelo.
Aquí está la PieceMovingVisitor
interfaz (comportamiento especificado para cada tipo de Piece
):
public interface PieceMovingVisitor {
void visitPawn(Pawn pawn);
void visitKing(King king);
void visitQueen(Queen queen);
void visitKnight(Knight knight);
void visitRook(Rook rook);
void visitBishop(Bishop bishop);
}
La pieza se define ahora:
public interface Piece {
void accept(PieceMovingVisitor pieceVisitor);
Coordinates getCoordinates();
void setCoordinates(Coordinates coordinates);
}
Su método clave es:
void accept(PieceMovingVisitor pieceVisitor);
Proporciona el primer despacho: una invocación basada en el Piece
receptor.
En tiempo de compilación, el método está vinculado al accept()
método de la interfaz Piece y en tiempo de ejecución, el método acotado se invocará en la Piece
clase de tiempo de ejecución .
Y es la accept()
implementación del método la que realizará un segundo envío.
De hecho, cada Piece
subclase que quiere ser visitada por un PieceMovingVisitor
objeto invoca el PieceMovingVisitor.visit()
método pasando como argumento en sí.
De esta manera, el compilador limita tan pronto como el tiempo de compilación, el tipo del parámetro declarado con el tipo concreto.
Existe el segundo despacho.
Aquí está la Bishop
subclase que ilustra eso:
public class Bishop implements Piece {
private Coordinates coord;
public Bishop(Coordinates coord) {
super(coord);
}
@Override
public void accept(PieceMovingVisitor pieceVisitor) {
pieceVisitor.visitBishop(this);
}
@Override
public Coordinates getCoordinates() {
return coordinates;
}
@Override
public void setCoordinates(Coordinates coordinates) {
this.coordinates = coordinates;
}
}
Y aquí un ejemplo de uso:
// 1. Player requests a move for a specific piece
Piece piece = selectPiece();
Coordinates coord = selectCoordinates();
// 2. We check with MoveCheckingVisitor that the request is valid
final MoveCheckingVisitor moveCheckingVisitor = new MoveCheckingVisitor(coord);
piece.accept(moveCheckingVisitor);
// 3. If the move is valid, MovePerformingVisitor performs the move
if (moveCheckingVisitor.isValid()) {
piece.accept(new MovePerformingVisitor(coord));
}
Inconvenientes de los visitantes
El patrón Visitor es un patrón muy poderoso, pero también tiene algunas limitaciones importantes que debe considerar antes de usarlo.
1) Riesgo de reducir / romper la encapsulación
En algunos tipos de operaciones, el patrón de visitante puede reducir o romper la encapsulación de objetos de dominio.
Por ejemplo, como la MovePerformingVisitor
clase necesita establecer las coordenadas de la pieza real, la Piece
interfaz debe proporcionar una forma de hacerlo:
void setCoordinates(Coordinates coordinates);
La responsabilidad de los Piece
cambios de coordenadas ahora está abierta a otras clases además de las Piece
subclases.
Mover el procesamiento realizado por el visitante en las Piece
subclases tampoco es una opción.
De hecho, creará otro problema ya que Piece.accept()
acepta la implementación de cualquier visitante. No sabe qué realiza el visitante y, por lo tanto, no tiene idea de si y cómo cambiar el estado de la Pieza.
Una forma de identificar al visitante sería realizar un procesamiento posterior de Piece.accept()
acuerdo con la implementación del visitante. Sería una muy mala idea, ya que crearía un alto acoplamiento entre las implementaciones de Visitor y las subclases de Piece y, además, probablemente requeriría usar truco como getClass()
, instanceof
o cualquier marcador que identifique la implementación de Visitor.
2) Requisito para cambiar el modelo
Contrariamente a algunos otros patrones de diseño de comportamiento como, Decorator
por ejemplo, el patrón de visitante es intrusivo.
De hecho, necesitamos modificar la clase de receptor inicial para proporcionar un accept()
método para aceptar ser visitado.
No tuvimos ningún problema Piece
y sus subclases, ya que estas son nuestras clases .
En las clases integradas o de terceros, las cosas no son tan fáciles.
Necesitamos envolverlos o heredarlos (si podemos) para agregar el accept()
método.
3) Indirecciones
El patrón crea múltiples indirecciones.
El envío doble significa dos invocaciones en lugar de una sola:
call the visited (piece) -> that calls the visitor (pieceMovingVisitor)
Y podríamos tener indirecciones adicionales a medida que el visitante cambia el estado del objeto visitado.
Puede parecer un ciclo:
call the visited (piece) -> that calls the visitor (pieceMovingVisitor) -> that calls the visited (piece)