¿Cuándo debo utilizar el patrón de diseño de visitantes? [cerrado]
Sigo viendo referencias al patrón de visitantes en los blogs, pero debo admitir que no lo entiendo. Leí el artículo de Wikipedia sobre el patrón y entiendo su mecánica, pero todavía no sé cuándo usarlo.
Como alguien que recientemente entendió el patrón decorador y ahora ve usos para él en absolutamente todas partes, me gustaría poder entender realmente intuitivamente este patrón aparentemente útil también.
No estoy muy familiarizado con el patrón Visitante. A ver si lo hice 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 emita su sonido. En la medida en que la jerarquía sea así de 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 quieras agregar una operación debes modificar la interfaz para cada clase de la jerarquía. Ahora supongamos que está satisfecho con la interfaz original y que desea realizarle la menor cantidad de modificaciones posibles.
El patrón Visitante le permite mover cada nueva operación a 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 "Visitor" 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 tienes una manera de agregar operaciones sin modificar más la jerarquía. Así es como funciona:
int main()
{
Cat c;
Sound theSound;
c.letsDo(&theSound);
}
La razón de su confusión es probablemente que el nombre de Visitante es fatalmente inapropiado. Muchos (¡destacados 1 !) programadores se han topado 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 lo hacen).
1) Mi ejemplo favorito es Scott Meyers, aclamado autor de “Effective C++”, quien llamó a este uno de sus C++ más importantes, ¡ajá! momentos de siempre .
Todos aquí tienen razón, pero creo que no aborda el "cuándo". Primero, de Patrones de diseño:
Visitor le permite definir una nueva operación sin cambiar las clases de los elementos sobre los que opera.
Ahora, pensemos en una jerarquía de clases simple. Tengo las clases 1, 2, 3 y 4 y los métodos A, B, C y D. Dispóngalos 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 supone 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, agregando columnas. La forma estándar en un diseño OO sería agregar dichos métodos a todas las clases, lo que puede resultar costoso. El patrón Visitante lo hace fácil.
Por cierto, este es el problema que las coincidencias de patrones de Scala pretenden resolver.
El patrón de diseño Visitor funciona muy 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 cambio, los métodos de visitante se aplican a cada nodo de la estructura.
A continuación se muestra una estructura de nodo recursivo típica. Podría ser un directorio o una etiqueta XML. [Si eres una persona de Java, imagina muchos métodos adicionales para crear y mantener la lista secundaria.]
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 visit
método aplica un objeto Visitante a cada nodo de la estructura. En este caso, se trata de un visitante de arriba hacia abajo. Puede cambiar la estructura del visit
método para realizar un orden ascendente o de otro tipo.
Aquí hay una superclase para los visitantes. Es utilizado por el visit
método. "Llega a" cada nodo de la estructura. Dado que el visit
método 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 buena ruta con números de sección jerárquicos.
Aquí tienes una aplicación. Construye una estructura de árbol someTree
. Crea un Visitor
, dumpNodes
.
Luego aplica el dumpNodes
al árbol. El dumpNode
objeto "visitará" cada nodo del árbol.
someTree= TreeNode( "Top", TreeNode("c1"), TreeNode("c2"), TreeNode("c3") )
dumpNodes= Visitor()
someTree.visit( dumpNodes )
El visit
algoritmo TreeNode asegurará que cada TreeNode se utilice como argumento para el arrivedAt
método del Visitante.