design-patterns - observer - patrones de diseño java
¿Cuándo debo usar el patrón de diseño de visitante? (21)
Basado en la excelente respuesta de @Federico A. Ramponi.
Solo imagina que tienes esta jerarquía:
public interface IAnimal
{
void DoSound();
}
public class Dog : IAnimal
{
public void DoSound()
{
Console.WriteLine("Woof");
}
}
public class Cat : IAnimal
{
public void DoSound(IOperation o)
{
Console.WriteLine("Meaw");
}
}
¿Qué sucede si necesita agregar un método "Caminar" aquí? Eso será doloroso para todo el diseño.
Al mismo tiempo, agregar el método "Caminar" genera nuevas preguntas. ¿Qué pasa con "comer" o "dormir"? ¿Realmente debemos agregar un nuevo método a la jerarquía Animal para cada nueva acción u operación que deseamos agregar? Eso es feo y lo más importante, nunca podremos cerrar la interfaz de Animal. Entonces, con el patrón de visitantes, podemos agregar un nuevo método a la jerarquía sin modificar la jerarquía.
Entonces, solo revisa y ejecuta este ejemplo de C #:
using System;
using System.Collections.Generic;
namespace VisitorPattern
{
class Program
{
static void Main(string[] args)
{
var animals = new List<IAnimal>
{
new Cat(), new Cat(), new Dog(), new Cat(),
new Dog(), new Dog(), new Cat(), new Dog()
};
foreach (var animal in animals)
{
animal.DoOperation(new Walk());
animal.DoOperation(new Sound());
}
Console.ReadLine();
}
}
public interface IOperation
{
void PerformOperation(Dog dog);
void PerformOperation(Cat cat);
}
public class Walk : IOperation
{
public void PerformOperation(Dog dog)
{
Console.WriteLine("Dog walking");
}
public void PerformOperation(Cat cat)
{
Console.WriteLine("Cat Walking");
}
}
public class Sound : IOperation
{
public void PerformOperation(Dog dog)
{
Console.WriteLine("Woof");
}
public void PerformOperation(Cat cat)
{
Console.WriteLine("Meaw");
}
}
public interface IAnimal
{
void DoOperation(IOperation o);
}
public class Dog : IAnimal
{
public void DoOperation(IOperation o)
{
o.PerformOperation(this);
}
}
public class Cat : IAnimal
{
public void DoOperation(IOperation o)
{
o.PerformOperation(this);
}
}
}
Sigo viendo referencias al patrón de visitantes en los blogs, pero debo admitir que simplemente no lo entiendo. Leí el artículo de wikipedia para el patrón y comprendo su mecánica, pero todavía estoy confundido sobre cuándo lo usaría.
Como alguien que recientemente adquirió el patrón de decorador y ahora está viendo usos absolutamente en todas partes, me gustaría poder entender realmente intuitivamente este patrón aparentemente útil también.
Cay Horstmann tiene un gran ejemplo de dónde aplicar Visitor en su libro OO Design and patterns book . Resume el problema:
Los objetos compuestos a menudo tienen una estructura compleja, compuesta de elementos individuales. Algunos elementos pueden tener nuevamente elementos hijos. ... Una operación en un elemento visita sus elementos secundarios, les aplica la operación y combina los resultados. ... Sin embargo, no es fácil agregar nuevas operaciones a dicho diseño.
La razón por la que no es fácil es porque las operaciones se agregan dentro de las propias clases de estructura. Por ejemplo, imagina que tienes un sistema de archivos:
Aquí hay algunas operaciones (funcionalidades) que podríamos querer implementar con esta estructura:
- Mostrar los nombres de los elementos del nodo (una lista de archivos)
- Mostrar el tamaño calculado de los elementos del nodo (donde el tamaño de un directorio incluye el tamaño de todos los elementos secundarios)
- etc.
Puede agregar funciones a cada clase en el sistema de archivos para implementar las operaciones (y las personas lo han hecho en el pasado, ya que es muy obvio cómo hacerlo). El problema es que cada vez que agregue una nueva funcionalidad (la línea "etc." más arriba), es posible que necesite agregar más y más métodos a las clases de estructura. En algún momento, después de una serie de operaciones que ha agregado a su software, los métodos en esas clases ya no tienen sentido en términos de la cohesión funcional de las clases. Por ejemplo, tiene un FileNode
que tiene un método FileNode
calculateFileColorForFunctionABC()
para implementar la última funcionalidad de visualización en el sistema de archivos.
El Patrón de visitante (como muchos patrones de diseño) nació del dolor y el sufrimiento de los desarrolladores que sabían que había una mejor manera de permitir que su código cambia sin requerir muchos cambios en todas partes y también respetar los principios de buen diseño (alta cohesión, bajo acoplamiento). ). Es mi opinión que es difícil entender la utilidad de muchos patrones hasta que hayas sentido ese dolor. Explicar el dolor (como intentamos hacer arriba con las funcionalidades de "etc." que se agregan) ocupa espacio en la explicación y es una distracción. Comprender los patrones es difícil por esta razón.
El visitante nos permite desacoplar las funcionalidades en la estructura de datos (por ejemplo, FileSystemNodes
) de las estructuras de datos en sí. El patrón permite que el diseño respete la cohesión: las clases de estructura de datos son más simples (tienen menos métodos) y también las funcionalidades se encapsulan en las implementaciones de los Visitor
. Esto se hace a través del doble despacho (que es la parte complicada del patrón): utilizando los métodos accept()
en las clases de estructura y los métodos visitX()
en las clases de Visitante (la funcionalidad):
Esta estructura nos permite agregar nuevas funcionalidades que funcionan en la estructura como visitantes concretos (sin cambiar las clases de estructura).
Por ejemplo, un PrintNameVisitor
que implementa la funcionalidad de listado de directorios y un PrintSizeVisitor
que implementa la versión con el tamaño. Podríamos imaginarnos un día teniendo un ''ExportXMLVisitor'' que genere los datos en XML u otro visitante que los genere en JSON, etc. Incluso podríamos tener un visitante que muestre mi árbol de directorios usando un lenguaje gráfico como DOT , para visualizarlo. con otro programa.
Como nota final: la complejidad de Visitor con su doble despacho significa que es más difícil de entender, codificar y depurar. En resumen, tiene un alto factor geek y repite el principio KISS. En una encuesta realizada por investigadores, se demostró que el visitante era un patrón controvertido (no hubo consenso sobre su utilidad). Algunos experimentos incluso mostraron que no hacía que el código fuera más fácil de mantener.
Como ya señaló Konrad Rudolph, es adecuado para casos en los que necesitamos doble despacho.
Aquí hay un ejemplo para mostrar una situación en la que necesitamos doble despacho y cómo el visitante nos ayuda a hacerlo.
Ejemplo:
Digamos que tengo 3 tipos de dispositivos móviles: iPhone, Android, Windows Mobile.
Todos estos tres dispositivos tienen una radio Bluetooth instalada en ellos.
Supongamos que el radio de diente azul puede ser de 2 OEM independientes: Intel y Broadcom.
Solo para hacer que el ejemplo sea relevante para nuestra discusión, asumamos también que las API expuestas por la radio Intel son diferentes de las expuestas por la radio Broadcom.
Así es como se ven mis clases.
Ahora, me gustaría introducir una operación: encender el Bluetooth en un dispositivo móvil.
Su función de firma debería gustar algo como esto:
void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio)
Por lo tanto, dependiendo del tipo de dispositivo correcto y del tipo correcto de radio Bluetooth , puede activarse llamando a los pasos adecuados o al algoritmo .
En principio, se convierte en una matriz de 3 x 2, donde estoy tratando de vectorizar la operación correcta dependiendo del tipo correcto de objetos involucrados.
Un comportamiento polimórfico que depende del tipo de ambos argumentos.
Ahora, el patrón de visitante se puede aplicar a este problema. La inspiración proviene de la página de Wikipedia que dice: “En esencia, el visitante permite agregar nuevas funciones virtuales a una familia de clases sin modificar las clases en sí; en su lugar, uno crea una clase de visitante que implementa todas las especializaciones apropiadas de la función virtual. El visitante toma la referencia de la instancia como entrada e implementa el objetivo a través del doble despacho ".
El doble despacho es una necesidad aquí debido a la matriz 3x2.
Así es como se verá la configuración:
Escribí el ejemplo para responder otra pregunta, el código y su explicación se mencionan here .
El patrón de diseño del visitante funciona realmente bien para estructuras "recursivas" como árboles de directorios, estructuras XML o esquemas de documentos.
Un objeto Visitante visita cada nodo en la estructura recursiva: cada directorio, cada etiqueta XML, lo que sea. El objeto Visitante no recorre la estructura. En su lugar, los métodos de visitante se aplican a cada nodo de la estructura.
Aquí hay una estructura de nodo recursiva típica. Podría ser un directorio o una etiqueta XML. [Si eres una persona de Java, imagina un montón de métodos adicionales para construir y mantener la lista de hijos.]
class TreeNode( object ):
def __init__( self, name, *children ):
self.name= name
self.children= children
def visit( self, someVisitor ):
someVisitor.arrivedAt( self )
someVisitor.down()
for c in self.children:
c.visit( someVisitor )
someVisitor.up()
El método de visit
aplica un objeto de visitante a cada nodo en la estructura. En este caso, es un visitante de arriba hacia abajo. Puede cambiar la estructura del método de visit
para hacer de abajo hacia arriba o algún otro pedido.
Aquí hay una superclase para los visitantes. Es utilizado por el método de visit
. Se "llega a" cada nodo en la estructura. Dado que el método de visit
llama up
y down
, el visitante puede realizar un seguimiento de la profundidad.
class Visitor( object ):
def __init__( self ):
self.depth= 0
def down( self ):
self.depth += 1
def up( self ):
self.depth -= 1
def arrivedAt( self, aTreeNode ):
print self.depth, aTreeNode.name
Una subclase podría hacer cosas como contar nodos en cada nivel y acumular una lista de nodos, generando una ruta agradable con números de sección jerárquica.
Aquí hay una aplicación. Se construye una estructura de árbol, un poco de árbol. Se crea un Visitor
, dumpNodes
.
Luego aplica los dumpNodes
al árbol. El objeto dumpNode
"visitará" cada nodo en el árbol.
someTree= TreeNode( "Top", TreeNode("c1"), TreeNode("c2"), TreeNode("c3") )
dumpNodes= Visitor()
someTree.visit( dumpNodes )
El algoritmo de visit
TreeNode asegurará que cada TreeNode se use como un argumento para el método arrivedAt
del Visitante.
En mi opinión, la cantidad de trabajo para agregar una nueva operación es más o menos la misma utilizando el Visitor Pattern
o la modificación directa de la estructura de cada elemento. Además, si tuviera que agregar una nueva clase de elemento, digamos Cow
, la interfaz de Operación se verá afectada y esto se propaga a todas las clases de elementos existentes, por lo que se requiere la recompilación de todas las clases de elementos. Entonces, ¿cuál es el punto?
Hay al menos tres buenas razones para usar el patrón de visitante:
Reducir la proliferación de código, que es solo ligeramente diferente cuando cambian las estructuras de datos.
Aplique el mismo cálculo a varias estructuras de datos, sin cambiar el código que implementa el cálculo.
Agregue información a las bibliotecas heredadas sin cambiar el código heredado.
Por favor, eche un vistazo a un artículo que he escrito sobre esto .
La razón de su confusión es probablemente que el Visitante es un nombre inapropiado fatal. Muchos programadores (¡ 1 prominente!) Han tropezado con este problema. Lo que realmente hace es implementar el envío doble en idiomas que no lo admiten de forma nativa (la mayoría de ellos no).
1) Mi ejemplo favorito es Scott Meyers, el aclamado autor de "Effective C ++", quien llamó a este uno de sus más importantes C ++ aha. momentos nunca
Lo encontré más fácil en los siguientes enlaces:
En http://www.remondo.net/visitor-pattern-example-csharp/ encontré un ejemplo que muestra un ejemplo simulado que muestra lo que es el beneficio del patrón de visitantes. Aquí tienes diferentes clases de contenedores para la Pill
:
namespace DesignPatterns
{
public class BlisterPack
{
// Pairs so x2
public int TabletPairs { get; set; }
}
public class Bottle
{
// Unsigned
public uint Items { get; set; }
}
public class Jar
{
// Signed
public int Pieces { get; set; }
}
}
Como se ve en la sección de arriba, Usted BilsterPack
contiene pares de píldoras, por lo que necesita multiplicar el número de pares por 2. También puede notar que la unit
uso de la Bottle
es un tipo de datos diferente y debe ser lanzada.
Entonces, en el método principal, puede calcular el conteo de píldoras usando el siguiente código:
foreach (var item in packageList)
{
if (item.GetType() == typeof (BlisterPack))
{
pillCount += ((BlisterPack) item).TabletPairs * 2;
}
else if (item.GetType() == typeof (Bottle))
{
pillCount += (int) ((Bottle) item).Items;
}
else if (item.GetType() == typeof (Jar))
{
pillCount += ((Jar) item).Pieces;
}
}
Tenga en cuenta que el código anterior viola el Single Responsibility Principle
. Eso significa que debe cambiar el código del método principal si agrega un nuevo tipo de contenedor. También hacer el cambio más largo es una mala práctica.
Entonces introduciendo el siguiente código:
public class PillCountVisitor : IVisitor
{
public int Count { get; private set; }
#region IVisitor Members
public void Visit(BlisterPack blisterPack)
{
Count += blisterPack.TabletPairs * 2;
}
public void Visit(Bottle bottle)
{
Count += (int)bottle.Items;
}
public void Visit(Jar jar)
{
Count += jar.Pieces;
}
#endregion
}
Usted movió la responsabilidad de contar el número de PillCountVisitor
a la clase llamada PillCountVisitor
(y eliminamos la declaración del caso del interruptor). Eso significa que cada vez que necesite agregar un nuevo tipo de contenedor de píldoras, debe cambiar solo la clase PillCountVisitor
. También IVisitor
cuenta que la interfaz de IVisitor
es general para usar en otros escenarios.
Agregando el método Accept a la clase contenedor de píldoras:
public class BlisterPack : IAcceptor
{
public int TabletPairs { get; set; }
#region IAcceptor Members
public void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
#endregion
}
Permitimos que el visitante visite las clases de contenedores de píldoras.
Al final calculamos el conteo de pastillas usando el siguiente código:
var visitor = new PillCountVisitor();
foreach (IAcceptor item in packageList)
{
item.Accept(visitor);
}
Eso significa: cada envase de píldoras permite que el visitante de PillCountVisitor
vea cómo cuentan sus píldoras. Él sabe cómo contar su píldora.
En el visitor.Count
tiene el valor de las pastillas.
En http://butunclebob.com/ArticleS.UncleBob.IuseVisitor ve un escenario real en el que no puede usar el polymorphism (la respuesta) para seguir el Principio de responsabilidad única. De hecho en:
public class HourlyEmployee extends Employee {
public String reportQtdHoursAndPay() {
//generate the line for this hourly employee
}
}
reportQtdHoursAndPay
método reportQtdHoursAndPay
es para la presentación de informes y la representación y esto viola el principio de responsabilidad única. Así que es mejor usar el patrón de visitantes para superar el problema.
No estoy muy familiarizado con el patrón de visitante. A ver si lo entendí bien. Supongamos que tienes una jerarquía de animales.
class Animal { };
class Dog: public Animal { };
class Cat: public Animal { };
(Supongamos que es una jerarquía compleja con una interfaz bien establecida).
Ahora queremos agregar una nueva operación a la jerarquía, es decir, queremos que cada animal haga su sonido. En la medida en que la jerarquía es tan simple, puedes hacerlo con polimorfismo directo:
class Animal
{ public: virtual void makeSound() = 0; };
class Dog : public Animal
{ public: void makeSound(); };
void Dog::makeSound()
{ std::cout << "woof!/n"; }
class Cat : public Animal
{ public: void makeSound(); };
void Cat::makeSound()
{ std::cout << "meow!/n"; }
Pero procediendo de esta manera, cada vez que desee agregar una operación debe modificar la interfaz a cada clase de la jerarquía. Ahora, supongamos que, en cambio, está satisfecho con la interfaz original y que desea realizar la menor cantidad posible de modificaciones.
El patrón de visitante le permite mover cada nueva operación en una clase adecuada, y necesita extender la interfaz de la jerarquía solo una vez. Vamos a hacerlo. Primero, definimos una operación abstracta (la clase "Visitante" en GoF) que tiene un método para cada clase en la jerarquía:
class Operation
{
public:
virtual void hereIsADog(Dog *d) = 0;
virtual void hereIsACat(Cat *c) = 0;
};
Luego, modificamos la jerarquía para aceptar nuevas operaciones:
class Animal
{ public: virtual void letsDo(Operation *v) = 0; };
class Dog : public Animal
{ public: void letsDo(Operation *v); };
void Dog::letsDo(Operation *v)
{ v->hereIsADog(this); }
class Cat : public Animal
{ public: void letsDo(Operation *v); };
void Cat::letsDo(Operation *v)
{ v->hereIsACat(this); }
Finalmente, implementamos la operación real, sin modificar ni Cat ni Dog :
class Sound : public Operation
{
public:
void hereIsADog(Dog *d);
void hereIsACat(Cat *c);
};
void Sound::hereIsADog(Dog *d)
{ std::cout << "woof!/n"; }
void Sound::hereIsACat(Cat *c)
{ std::cout << "meow!/n"; }
Ahora tiene una forma de agregar operaciones sin modificar más la jerarquía. Así es como funciona:
int main()
{
Cat c;
Sound theSound;
c.letsDo(&theSound);
}
Patrón de visitante como la misma implementación subterránea a la programación de Objetos de Aspecto.
Por ejemplo, si define una nueva operación sin cambiar las clases de los elementos sobre los que opera
Todos aquí están en lo cierto, pero creo que no aborda el "cuándo". Primero, desde los patrones de diseño:
El visitante le permite definir una nueva operación sin cambiar las clases de los elementos sobre los que opera.
Ahora, pensemos en una simple jerarquía de clases. Tengo las clases 1, 2, 3 y 4 y los métodos A, B, C y D. Colóquelas como en una hoja de cálculo: las clases son líneas y los métodos son columnas.
Ahora, el diseño orientado a objetos presume que es más probable que crezcan nuevas clases que nuevos métodos, por lo que agregar más líneas, por así decirlo, es más fácil. Simplemente agrega una nueva clase, especifica qué es diferente en esa clase y hereda el resto.
A veces, sin embargo, las clases son relativamente estáticas, pero es necesario agregar más métodos con frecuencia: agregar columnas. La forma estándar en un diseño OO sería agregar dichos métodos a todas las clases, lo que puede ser costoso. El patrón de visitante hace que esto sea fácil.
Por cierto, este es el problema que el patrón de Scala intenta resolver.
Una forma de verlo es que el patrón de visitante es una forma de permitir que sus clientes agreguen métodos adicionales a todas sus clases en una jerarquía de clases en particular.
Es útil cuando tiene una jerarquía de clases bastante estable, pero tiene requisitos cambiantes de lo que debe hacerse con esa jerarquía.
El ejemplo clásico es para compiladores y similares. Un árbol de sintaxis abstracta (AST) puede definir con precisión la estructura del lenguaje de programación, pero las operaciones que desee realizar en el AST cambiarán a medida que avance su proyecto: generadores de código, impresoras bonitas, depuradores, análisis de métricas de complejidad.
Sin el patrón de visitante, cada vez que un desarrollador quisiera agregar una nueva característica, necesitaría agregar ese método a cada característica de la clase base. Esto es particularmente difícil cuando las clases base aparecen en una biblioteca separada o son producidas por un equipo separado.
(He oído que argumentó que el patrón de Visitante está en conflicto con las buenas prácticas de OO, porque aleja las operaciones de los datos de los datos. El patrón de Visitante es útil precisamente en la situación en la que fallan las prácticas normales de OO).
El visitante permite agregar nuevas funciones virtuales a una familia de clases sin modificar las clases; en su lugar, uno crea una clase de visitante que implementa todas las especializaciones apropiadas de la función virtual
Estructura del visitante:
Use el patrón de visitante si:
- Se deben realizar operaciones similares en objetos de diferentes tipos agrupados en una estructura
- Necesita ejecutar muchas operaciones distintas y no relacionadas. Separa la operación de los objetos. Estructura.
- Nuevas operaciones deben ser agregadas sin cambios en la estructura del objeto.
- Reúna las operaciones relacionadas en una sola clase en lugar de obligarlo a cambiar o derivar clases
- Agregue funciones a las bibliotecas de clases para las cuales no tiene la fuente o no puede cambiar la fuente
A pesar de que el patrón de visitante proporciona flexibilidad para agregar nuevas operaciones sin cambiar el código existente en Object, esta flexibilidad tiene un inconveniente.
Si se ha agregado un nuevo objeto Visitable, se requieren cambios de código en las clases de Visitor y ConcreteVisitor . Hay una solución para solucionar este problema: usar la reflexión, que tendrá un impacto en el rendimiento.
Fragmento de código:
import java.util.HashMap;
interface Visitable{
void accept(Visitor visitor);
}
interface Visitor{
void logGameStatistics(Chess chess);
void logGameStatistics(Checkers checkers);
void logGameStatistics(Ludo ludo);
}
class GameVisitor implements Visitor{
public void logGameStatistics(Chess chess){
System.out.println("Logging Chess statistics: Game Completion duration, number of moves etc..");
}
public void logGameStatistics(Checkers checkers){
System.out.println("Logging Checkers statistics: Game Completion duration, remaining coins of loser");
}
public void logGameStatistics(Ludo ludo){
System.out.println("Logging Ludo statistics: Game Completion duration, remaining coins of loser");
}
}
abstract class Game{
// Add game related attributes and methods here
public Game(){
}
public void getNextMove(){};
public void makeNextMove(){}
public abstract String getName();
}
class Chess extends Game implements Visitable{
public String getName(){
return Chess.class.getName();
}
public void accept(Visitor visitor){
visitor.logGameStatistics(this);
}
}
class Checkers extends Game implements Visitable{
public String getName(){
return Checkers.class.getName();
}
public void accept(Visitor visitor){
visitor.logGameStatistics(this);
}
}
class Ludo extends Game implements Visitable{
public String getName(){
return Ludo.class.getName();
}
public void accept(Visitor visitor){
visitor.logGameStatistics(this);
}
}
public class VisitorPattern{
public static void main(String args[]){
Visitor visitor = new GameVisitor();
Visitable games[] = { new Chess(),new Checkers(), new Ludo()};
for (Visitable v : games){
v.accept(visitor);
}
}
}
Explicación:
-
Visitable
(Element
) es una interfaz y este método de interfaz debe agregarse a un conjunto de clases. -
Visitor
es una interfaz, que contiene métodos para realizar una operación en elementosVisitable
. -
GameVisitor
es una clase que implementa una interfaz deVisitor
(ConcreteVisitor
). - Cada elemento
Visitable
aceptaVisitor
e invoca un método relevante de interfaz deVisitor
. - Puedes tratar el
Game
comoElement
y los juegos concretos comoChess,Checkers and Ludo
comoElement
ConcreteElements
.
En el ejemplo anterior, Chess, Checkers and Ludo
son tres juegos diferentes (y clases Visitable
).En un buen día, me encontré con un escenario para registrar estadísticas de cada juego. Por lo tanto, sin modificar la clase individual para implementar la funcionalidad de estadísticas, puede centralizar esa responsabilidad en GameVisitor
clase, lo que hace el truco por usted sin modificar la estructura de cada juego.
salida:
Logging Chess statistics: Game Completion duration, number of moves etc..
Logging Checkers statistics: Game Completion duration, remaining coins of loser
Logging Ludo statistics: Game Completion duration, remaining coins of loser
Referirse a
sourcemakingartículo de sourcemaking
para más detalles
el patrón permite que el comportamiento se agregue a un objeto individual, ya sea de forma estática o dinámica, sin afectar el comportamiento de otros objetos de la misma clase
Artículos Relacionados:
Breve descripción del patrón de visitante. 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 única para realizar una amplia gama de nuevas acciones al pasar a 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. Estos métodos de visita pasan una instancia en la que trabajar.
Cuando podrías considerar usarlo
- Cuando tenga una familia de clases, sabrá que tendrá que agregar muchas acciones nuevas, pero por alguna razón no podrá alterar o volver a compilar la familia de clases en el futuro.
- Cuando desee agregar una nueva acción y tener esa nueva acción totalmente definida dentro de una clase de visitante, en lugar de distribuirla en varias clases.
- Cuando su jefe dice que debe producir una serie de clases que deben hacer algo ahora mismo ... ... pero en realidad nadie sabe exactamente qué es ese algo.
Cuando desee tener objetos de función en tipos de datos de unión, necesitará un patrón de visitante.
Podría preguntarse qué son los objetos de función y los tipos de datos de unión, entonces vale la pena leer http://www.ccs.neu.edu/home/matthias/htdc.html
tu pregunta es cuando saber:
No primero código con patrón de visitante. Yo codifico el estándar y espero que ocurra la necesidad y luego lo refactorizo. así que digamos que tiene varios sistemas de pago que instaló uno a la vez. En el momento de la facturación, puede tener muchas condiciones si (o instanceOf), por ejemplo:
//psuedo code
if(payPal)
do paypal checkout
if(stripe)
do strip stuff checkout
if(payoneer)
do payoneer checkout
Ahora imagina que tenía 10 métodos de pago, se pone algo feo. Entonces, cuando ves que el patrón que se presenta, el visitante llega a mano para separar todo eso y terminas llamando algo como esto después:
new PaymentCheckoutVistor(paymentType).visit()
Puede ver cómo implementarlo a partir del número de ejemplos aquí, solo le muestro un caso de uso.
El doble despacho 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 único paradigma de envío.
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 el modelo y el comportamiento porque queremos tener un modelo reutilizable en múltiples aplicaciones o queremos tener un modelo extensible que permita a las clases del cliente 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 varias 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 según su tipo de tiempo de ejecución ... por supuesto, sin usar if (myObj instanceof Foo) {}
o 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 hacer no es posible fuera de la caja ya que los idiomas se basan en un envío único porque el invocado elegido en el 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) a llamar se elige en el momento de la 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 al 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 los elementos (iteración) para aplicar el visitante en cada uno no es una razón para usar el patrón.
Usas el patrón porque divides 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 un 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 voy a poner eso 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 una pieza en movimiento.
Sin el uso del patrón de 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 Piece lo implementaría tal 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 Piece.
Aquí hay una clase de diagrama que ilustra este diseño:
Este enfoque presenta tres inconvenientes importantes:
- Comportamientos como performMove()
o computeIfKingCheck()
muy probablemente usen 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 toma la pieza del oponente.
Dividir los comportamientos relacionados en varias clases en lugar de acumularlas de alguna manera en el único patrón de responsabilidad. Haciendo más difícil su mantenibilidad.
- El procesamiento como checkMoveValidity()
no debería ser algo que las Piece
subclases puedan ver o cambiar.
Es el cheque 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 la jugada de pieza solicitada sea válida.
Así que incluso no queremos proporcionar eso en la Piece
interfaz.
- En los juegos de ajedrez desafiantes para los desarrolladores de bots, generalmente la aplicación proporciona una API estándar ( Piece
interfaces, subclases, Board, comportamientos comunes, etc.) y permite a los desarrolladores enriquecer su estrategia 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 (mudanzas).
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 envío: una invocación basada en el Piece
receptor.
En el momento de la compilación, el método está vinculado al accept()
método de la interfaz Piece y en el tiempo de ejecución, el método delimitado se invocará en la Piece
clase de tiempo de ejecución .
Y es la accept()
implementación del método 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.
Ahí está 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));
}
Desventajas de los visitantes
El patrón de visitante 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 operación, 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 que no sean 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 los Visitantes y las subclases de piezas y, además, probablemente requiera usar el truco como getClass()
, instanceof
o cualquier marcador que identifique la implementación del Visitante.
2) Requisito para cambiar el modelo.
Contrariamente a 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 que acepte para ser visitado.
No tuvimos ningún problema Piece
y sus subclases ya que estas son nuestras clases .
En 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 indirectas.
El doble despacho significa dos invocaciones en lugar de una sola:
call the visited (piece) -> that calls the visitor (pieceMovingVisitor)
Y podríamos tener conexiones 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)
Gracias por la asombrosa explicación de @Federico A. Ramponi , acabo de hacer esto en la versión Java . Espero que pueda ser útil.
También, como lo señaló @Konrad Rudolph , en realidad es un doble despacho que usa dos instancias concretas para determinar los métodos de tiempo de ejecución.
Por lo tanto, en realidad no es necesario crear una interfaz común para el ejecutor de operaciones , siempre que tengamos la interfaz de operaciones definida correctamente.
import static java.lang.System.out;
public class Visitor_2 {
public static void main(String...args) {
Hearen hearen = new Hearen();
FoodImpl food = new FoodImpl();
hearen.showTheHobby(food);
Katherine katherine = new Katherine();
katherine.presentHobby(food);
}
}
interface Hobby {
void insert(Hearen hearen);
void embed(Katherine katherine);
}
class Hearen {
String name = "Hearen";
void showTheHobby(Hobby hobby) {
hobby.insert(this);
}
}
class Katherine {
String name = "Katherine";
void presentHobby(Hobby hobby) {
hobby.embed(this);
}
}
class FoodImpl implements Hobby {
public void insert(Hearen hearen) {
out.println(hearen.name + " start to eat bread");
}
public void embed(Katherine katherine) {
out.println(katherine.name + " start to eat mango");
}
}
Como espera, una interfaz común nos brindará más claridad, aunque en realidad no es la parte esencial de este patrón.
import static java.lang.System.out;
public class Visitor_2 {
public static void main(String...args) {
Hearen hearen = new Hearen();
FoodImpl food = new FoodImpl();
hearen.showHobby(food);
Katherine katherine = new Katherine();
katherine.showHobby(food);
}
}
interface Hobby {
void insert(Hearen hearen);
void insert(Katherine katherine);
}
abstract class Person {
String name;
protected Person(String n) {
this.name = n;
}
abstract void showHobby(Hobby hobby);
}
class Hearen extends Person {
public Hearen() {
super("Hearen");
}
@Override
void showHobby(Hobby hobby) {
hobby.insert(this);
}
}
class Katherine extends Person {
public Katherine() {
super("Katherine");
}
@Override
void showHobby(Hobby hobby) {
hobby.insert(this);
}
}
class FoodImpl implements Hobby {
public void insert(Hearen hearen) {
out.println(hearen.name + " start to eat bread");
}
public void insert(Katherine katherine) {
out.println(katherine.name + " start to eat mango");
}
}
Me gusta mucho la descripción y el ejemplo de http://python-3-patterns-idioms-test.readthedocs.io/en/latest/Visitor.html .
El supuesto es que usted tiene una jerarquía de clases primaria que está fija; quizás sea de otro proveedor y no puede realizar cambios en esa jerarquía. Sin embargo, su intención es que le gustaría agregar nuevos métodos polimórficos a esa jerarquía, lo que significa que normalmente tendría que agregar algo a la interfaz de clase base. Entonces, el dilema es que necesita agregar métodos a la clase base, pero no puede tocar la clase base. ¿Cómo resuelves esto?
El patrón de diseño que resuelve este tipo de problema se denomina "visitante" (el último en el libro de Patrones de diseño) y se basa en el esquema de envío doble que se muestra en la sección anterior.
El patrón de visitante le permite extender la interfaz del tipo primario creando una jerarquía de clases separada del tipo Visitante para virtualizar las operaciones realizadas en el tipo primario. Los objetos del tipo primario simplemente "aceptan" al visitante, luego llaman a la función miembro de enlace dinámico del visitante.
No entendí este patrón hasta que encontré el http://butunclebob.com/ArticleS.UncleBob.IuseVisitor y leí los comentarios. Considere el siguiente código:
public class Employee { } public class SalariedEmployee : Employee { } public class HourlyEmployee : Employee { } public class QtdHoursAndPayReport { public void PrintReport() { var employees = new List<Employee> { new SalariedEmployee(), new HourlyEmployee() }; foreach (Employee e in employees) { if (e is HourlyEmployee he) PrintReportLine(he); if (e is SalariedEmployee se) PrintReportLine(se); } } public void PrintReportLine(HourlyEmployee he) { System.Diagnostics.Debug.WriteLine("hours"); } public void PrintReportLine(SalariedEmployee se) { System.Diagnostics.Debug.WriteLine("fix"); } } class Program { static void Main(string[] args) { new QtdHoursAndPayReport().PrintReport(); } }
Si bien puede parecer bueno, ya que confirma a la responsabilidad única, viola el principio de apertura / cierre. Cada vez que tenga un nuevo tipo de empleado tendrá que agregarlo con la verificación de tipo. Y si no lo haces, nunca lo sabrás en el momento de la compilación.
Con el patrón de visitante puede hacer que su código sea más limpio, ya que no viola el principio de apertura / cierre y no viola la responsabilidad única. Y si olvidas implementar la visita no se compilará:
public abstract class Employee { public abstract void Accept(EmployeeVisitor v); } public class SalariedEmployee : Employee { public override void Accept(EmployeeVisitor v) { v.Visit(this); } } public class HourlyEmployee:Employee { public override void Accept(EmployeeVisitor v) { v.Visit(this); } } public interface EmployeeVisitor { void Visit(HourlyEmployee he); void Visit(SalariedEmployee se); } public class QtdHoursAndPayReport : EmployeeVisitor { public void Visit(HourlyEmployee he) { System.Diagnostics.Debug.WriteLine("hourly"); // generate the line of the report. } public void Visit(SalariedEmployee se) { System.Diagnostics.Debug.WriteLine("fix"); } // do nothing public void PrintReport() { var employees = new List<Employee> { new SalariedEmployee(), new HourlyEmployee() }; QtdHoursAndPayReport v = new QtdHoursAndPayReport(); foreach (var emp in employees) { emp.Accept(v); } } } class Program { public static void Main(string[] args) { new QtdHoursAndPayReport().PrintReport(); } } }
La magia es que, aunque v.Visit(this)
parece lo mismo, en realidad es diferente ya que llama a diferentes sobrecargas de visitantes.
Si bien he entendido cómo y cuándo, nunca he entendido el por qué. En caso de que ayude a cualquier persona con antecedentes en un lenguaje como C ++, debe leer esto con mucho cuidado.
Para los perezosos, usamos el patrón de visitante porque "mientras las funciones virtuales se distribuyen dinámicamente en C ++, la sobrecarga de funciones se realiza de forma estática" .
O, dicho de otra manera, para asegurarse de que se llame a CollideWith (ApolloSpacecraft &) cuando pase una referencia de SpaceShip que en realidad esté vinculada a un objeto ApolloSpacecraft.
class SpaceShip {};
class ApolloSpacecraft : public SpaceShip {};
class ExplodingAsteroid : public Asteroid {
public:
virtual void CollideWith(SpaceShip&) {
cout << "ExplodingAsteroid hit a SpaceShip" << endl;
}
virtual void CollideWith(ApolloSpacecraft&) {
cout << "ExplodingAsteroid hit an ApolloSpacecraft" << endl;
}
}