Empresas
Empleos
  • Sobre nosotros
  • Soluciones
    • Publicación de vacantes
      Publica tu vacante y recibe candidatos calificados en 48h.
    • Evaluación de candidatos
      500+ pruebas técnicas y psicológicas, más anti-fraude.
    • Headhunting
      Búsqueda ejecutiva a la medida de principio a fin.
    • Nómina + EOR
      Dispersión de nómina y EOR en más de 15 países de LATAM.
  • Precios
  • Empleos

0

185
Vistas
Confundido sobre el patrón de diseño de visitantes

Entonces, estaba leyendo sobre el patrón de Visitante y ¡encontré el ir y venir entre el Visitante y los Elementos muy extraño!

Básicamente llamamos al elemento, le pasamos un visitante y luego el elemento se pasa al visitante. Y LUEGO el visitante opera el elemento. ¿Qué? ¿Por qué? Se siente tan innecesario. Yo lo llamo la "locura de ida y vuelta".

Entonces, la intención del Visitante es desvincular los Elementos de sus acciones cuando las mismas acciones deben implementarse en todos los elementos. Esto se hace en caso de que necesitemos extender nuestros Elementos con nuevas acciones, no queremos entrar en todas esas clases y modificar el código que ya es estable. Entonces estamos siguiendo el principio Abierto/Cerrado aquí.

¿Por qué hay todo este ida y vuelta y qué perdemos si no tenemos esto?

Por ejemplo, hice este código que tiene ese propósito en mente pero omite la locura de interacción del patrón de visitante. Básicamente tengo animales que saltan y comen. Quería desacoplar esas acciones de los objetos, así que muevo las acciones a Visitantes. Comer y saltar aumenta la salud del animal (lo sé, este es un ejemplo muy tonto...)

 public interface AnimalAction { // Abstract Visitor public void visit(Dog dog); public void visit(Cat cat); } public class EatVisitor implements AnimalAction { // ConcreteVisitor @Override public void visit(Dog dog) { // Eating increases the dog health by 100 dog.increaseHealth(100); } @Override public void visit(Cat cat) { // Eating increases the cat health by 50 cat.increaseHealth(50); } } public class JumpVisitor implements AnimalAction { // ConcreteVisitor public void visit(Dog dog) { // Jumping increases the dog health by 10 dog.increaseHealth(10); } public void visit(Cat cat) { // Jumping increases the cat health by 20 cat.increaseHealth(20); } } public class Cat { // ConcreteElement private int health; public Cat() { this.health = 50; } public void increaseHealth(int healthIncrement) { this.health += healthIncrement; } public int getHealth() { return health; } } public class Dog { // ConcreteElement private int health; public Dog() { this.health = 10; } public void increaseHealth(int healthIncrement) { this.health += healthIncrement; } public int getHealth() { return health; } } public class Main { public static void main(String[] args) { AnimalAction jumpAction = new JumpVisitor(); AnimalAction eatAction = new EatVisitor(); Dog dog = new Dog(); Cat cat = new Cat(); jumpAction.visit(dog); // NOTE HERE. NOT DOING THE BACK AND FORTH MADNESS. eatAction.visit(dog); System.out.println(dog.getHealth()); jumpAction.visit(cat); eatAction.visit(cat); System.out.println(cat.getHealth()); } }
over 4 years ago · Santiago Trujillo
4 Respuestas
Responde la pregunta

0

El código en el OP se parece a una variación bien conocida del patrón de diseño Visitante conocido como Visitante interno (ver, por ejemplo, Extensibilidad para las masas. Extensibilidad práctica con álgebras de objetos por Bruno C. d. S. Oliveira y William R. Cook). Esa variación, sin embargo, usa genéricos y valores devueltos (en lugar de void ) para resolver algunos de los problemas que aborda el patrón Visitor.

¿Qué problema es ese y por qué la variación OP probablemente sea insuficiente?

El principal problema que aborda el patrón Visitor es cuando tiene objetos heterogéneos que necesita tratar de la misma manera. Como afirma Gang of Four (los autores de Design Patterns ), usas el patrón cuando

"una estructura de objeto contiene muchas clases de objetos con diferentes interfaces, y desea realizar operaciones en estos objetos que dependen de sus clases concretas".

Lo que falta en esta oración es que, si bien le gustaría "realizar operaciones en estos objetos que dependen de sus clases concretas", desea tratar esas clases concretas como si tuvieran un único tipo polimórfico.

Un ejemplo de época

Usar el dominio animal rara vez es ilustrativo (volveré a eso más adelante), así que aquí hay otro ejemplo más realista. Los ejemplos están en C#. Espero que te sigan siendo útiles.

Imagine que está desarrollando un sistema de reservas de restaurantes en línea. Como parte de ese sistema, debe poder mostrar un calendario a los usuarios. Este calendario podría mostrar cuántos asientos restantes están disponibles en un día determinado o enumerar todas las reservas en el día.

A veces, desea mostrar un solo día, pero otras veces desea mostrar un mes completo como un solo objeto de calendario. Agregue un año entero por si acaso. Esto significa que tiene tres períodos: año , mes y día . Cada uno tiene diferentes interfaces:

 public Year(int year) public Month(int year, int month) public Day(int year, int month, int day)

Para abreviar, estos son solo los constructores de tres clases separadas. Mucha gente podría simplemente modelar esto como una sola clase con campos anulables, pero esto lo obliga a lidiar con campos nulos, enumeraciones u otros tipos de maldad.

Las tres clases anteriores tienen una estructura diferente porque contienen datos diferentes, pero le gustaría tratarlas como un solo concepto: un período .

Para hacerlo, defina una interfaz IPeriod :

 internal interface IPeriod { T Accept<T>(IPeriodVisitor<T> visitor); }

y hacer que cada clase implemente la interfaz. Aquí está el Month :

 internal sealed class Month : IPeriod { private readonly int year; private readonly int month; public Month(int year, int month) { this.year = year; this.month = month; } public T Accept<T>(IPeriodVisitor<T> visitor) { return visitor.VisitMonth(year, month); } }

Esto le permite tratar las tres clases heterogéneas como un único tipo y definir operaciones en ese único tipo sin tener que cambiar la interfaz.

Aquí, por ejemplo, hay una implementación que calcula el período anterior :

 private class PreviousPeriodVisitor : IPeriodVisitor<IPeriod> { public IPeriod VisitYear(int year) { var date = new DateTime(year, 1, 1); var previous = date.AddYears(-1); return Period.Year(previous.Year); } public IPeriod VisitMonth(int year, int month) { var date = new DateTime(year, month, 1); var previous = date.AddMonths(-1); return Period.Month(previous.Year, previous.Month); } public IPeriod VisitDay(int year, int month, int day) { var date = new DateTime(year, month, day); var previous = date.AddDays(-1); return Period.Day(previous.Year, previous.Month, previous.Day); } }

Si tiene un Day , obtendrá el Day anterior, pero si tiene un Month , obtendrá el Month anterior, y así sucesivamente.

Puede ver la clase PreviousPeriodVisitor y otros visitantes en uso en este artículo , pero aquí están algunas líneas de código donde se usan:

 var previous = period.Accept(new PreviousPeriodVisitor()); var next = period.Accept(new NextPeriodVisitor()); dto.Links = new[] { url.LinkToPeriod(previous, "previous"), url.LinkToPeriod(next, "next") };

Aquí, el period es un objeto IPeriod , pero el código no sabe si es un Day , un Month o un Year .

Para que quede claro, el ejemplo anterior utiliza la variación de Visitante interno, que es isomorfa a una codificación de iglesia .

animales

Usar animales para comprender la programación orientada a objetos rara vez es esclarecedor. Creo que las escuelas deberían dejar de usar ese ejemplo, ya que es más probable que confunda que ayude.

El ejemplo de código OP no sufre el problema que resuelve el patrón Visitor, por lo que en ese contexto, no es sorprendente si no ve el beneficio.

Las clases Cat y Dog no son heterogéneas. Tienen el mismo campo de clase y el mismo comportamiento. La única diferencia está en el constructor. Podrías refactorizar trivialmente esas dos clases en una sola clase Animal :

 public class Animal { private int health; public Animal(int health) { this.health = health; } public void increaseHealth(int healthIncrement) { this.health += healthIncrement; } public int getHealth() { return health; } }

Luego defina dos métodos de creación para gatos y perros, utilizando los dos valores health distintos.

Dado que ahora tiene una sola clase, no se garantiza ningún Visitante.

over 4 years ago · Santiago Trujillo Denunciar

0

Con ida y vuelta, ¿te refieres a esto?

 public class Dog implements Animal { //... @Override public void accept(AnimalAction action) { action.visit(this); } }

El propósito de este código es que puede despachar el tipo sin conocer el tipo concreto, como aquí:

 public class Main { public static void main(String[] args) { AnimalAction jumpAction = new JumpVisitor(); AnimalAction eatAction = new EatVisitor(); Animal animal = aFunctionThatCouldReturnAnyAnimal(); animal.accept(jumpAction); animal.accept(eatAction); } private static Animal aFunctionThatCouldReturnAnyAnimal() { return new Dog(); } }

Entonces, lo que obtienes es: puedes llamar a la acción individual correcta sobre un animal con solo saber que es un animal.

Esto es especialmente útil si atraviesa un patrón compuesto, donde los nodos de hoja son Animal y los nodos internos son agregaciones (por ejemplo, una List ) de Animals . No se puede procesar un List<Animal> con su diseño.

over 4 years ago · Santiago Trujillo Denunciar

0

El ir y venir en Visitor es emular una especie de mecanismo de envío doble , donde selecciona una implementación de método basada en el tipo de tiempo de ejecución de dos objetos.

Esto es útil si el tipo de animal y visitante son abstractos (o polimórficos). En cuyo caso, tiene un potencial de 2 x 2 = 4 implementaciones de métodos para elegir, en función de a) qué tipo de acción (visita) desea realizar yb) a qué tipo de animal desea que se aplique esta acción.

ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí

Si está utilizando tipos concretos y no polimórficos, entonces parte de este ir y venir es realmente superfluo.

over 4 years ago · Santiago Trujillo Denunciar

0

El patrón de visitante resuelve el problema de aplicar una función a los elementos de una estructura gráfica.

Más específicamente, resuelve el problema de visitar cada nodo N en alguna estructura gráfica, en el contexto de algún objeto V, y para cada N, invocando alguna función genérica F(V, N). La implementación del método de F se elige en función del tipo de V y de N.

En los lenguajes de programación que tienen despacho múltiple, el patrón de visitante casi desaparece. Se reduce a un recorrido del objeto gráfico (por ejemplo, descenso recursivo del árbol), que realiza una simple llamada F(V, N) para cada N nodo. ¡Hecho!

Por ejemplo, en Common Lisp. Para abreviar, ni siquiera definamos clases: los integers y las strings son clases, así que usémoslos.

Primero, escribamos los cuatro métodos de la función genérica, para cada combinación de un entero o cadena que visita un entero o cadena. Los métodos solo producen resultados. No definimos la función genérica con defgeneric ; Lisp infiere esto y lo hace implícitamente por nosotros:

 (defmethod visit ((visitor integer) (node string)) (format t "integer ~s visits string ~s!~%" visitor node)) (defmethod visit ((visitor integer) (node integer)) (format t "integer ~s visits integer ~s!~%" visitor node)) (defmethod visit ((visitor string) (node string)) (format t "string ~s visits string ~s!~%" visitor node)) (defmethod visit ((visitor string) (node integer)) (format t "string ~s visits integer ~s!~%" visitor node))

Ahora usemos una lista como nuestra estructura para ser iterada por el visitante, y escribamos una función contenedora para eso:

 (defun visitor-pattern (visitor list) ;; map over the list, doing the visitation (mapc (lambda (item) (visit visitor item)) list) ;; return nothing (values))

Prueba de forma interactiva:

 (visitor-pattern 42 '(1 "abc")) integer 42 visits integer 1! integer 42 visits string "abc"! (visitor-pattern "foo" '(1 "abc")) string "foo" visits integer 1! string "foo" visits string "abc"!

Bien, ese es el patrón de visitante: un recorrido de cada elemento en una estructura, con un envío doble de un método con un objeto de contexto de visita.

La "locura de ida y vuelta" tiene que ver con el código repetitivo de simular el envío doble en un sistema OOP que tiene un solo envío y en el que los métodos pertenecen a clases en lugar de ser especializaciones de funciones genéricas.

Debido a que en el sistema OOP de envío único convencional, los métodos se encapsulan en clases, el primer problema que tenemos es ¿dónde vive el método de visit ? ¿Está en el visitante o en el nodo?

La respuesta resulta que tiene que ser ambos. Tendremos que despachar algo de ambos tipos.

Luego viene el problema de que, en la práctica de la programación orientada a objetos, necesitamos una buena denominación. No podemos tener un método de visit tanto en el visitor como en el objeto visited . Cuando se visita un objeto visitado, el verbo "visitar" no se usa para describir lo que está haciendo ese objeto. Se "acepta" un visitante. Entonces tenemos que llamar a esa mitad de la acción accept .

Creamos una estructura en la que cada nodo a visitar tiene un método de accept . Este método se distribuye según el tipo de nodo y toma un argumento Visitor . De hecho, el nodo tiene múltiples métodos de accept , que están estáticamente especializados en diferentes tipos de visitantes: IntegerVisitor , StringVisitor , FooVisitor . Tenga en cuenta que no podemos simplemente usar String , incluso si tenemos esa clase en el idioma, porque no implementa la interfaz Visitor con el método visit .

Entonces, lo que sucede es que recorremos la estructura, obtenemos todos los nodos N y luego llamamos a V.visit(N) para que el visitante lo visite. No sabemos el tipo exacto de V ; es una referencia base. Cada implementación de Visitor debe implementar visit como parte de la placa estándar (usando un pseudo-lenguaje que no sea Java o C++):

 StringVisitor::visit(Visited obj) { obj.Accept(self) } IntegerVisitor::visit(Visited obj) { obj.Accept(self) }

La razón es que self tiene que escribirse estáticamente para la llamada de Accept , porque el objeto Visited tiene múltiples implementaciones de Accept para diferentes tipos elegidos en tiempo de compilación:

 IntegerNode::visit(StringVisitor v) { print(`integer @{self.value} visits string @{v.value}`) } IntegerNode::visit(IntegerVisitor v) { print(`integer @{self.value} visits string @{v.value}`) }

Todas esas clases y métodos deben declararse en algún lugar:

 class VisitorBase { virtual void Visit(VisitedBase); } class IntegerVisitor; class StringVisitor; class VisitedBase { virtual void Accept(IntegerVisitor); virtual void Accept(StringVisitor); } class IntegerVisitor : inherit VisitorBase { Integer value; void Visit(VisitedBase); } class StringVisitor: inherit VisitorBase { String value; void Visit(VisitedBase); } class IntegerNode : inherit VisitedBase { Integer value; void Accept(IntegerVisitor); void Accept(StringVisitor); } class StringNode : inherit VisitedBase { String value; void Accept(IntegerVisitor); void Accept(StringVisitor); }

Así que ese es el patrón de visitante de envío único con sobrecarga estática: hay un montón de placa de caldera, además de la limitación de que una de las clases, ya sea visitante o visitada, tiene que conocer los tipos estáticos de todos los demás que son compatible, por lo que puede enviarse estáticamente en él, y para cada tipo estático, también habrá un método ficticio.

over 4 years ago · Santiago Trujillo Denunciar
Responde la pregunta
Encuentra empleos remotos

¡Descubre la nueva forma de encontrar empleo!

Top de empleos
Top categorías de empleo
Empresas
Publicar vacante Precios Comercial
Legal
Términos y condiciones Política de privacidad
© 2026 PeakU Inc. All Rights Reserved.
Andres GPT
Recomiéndame algunas ofertas
Necesito ayuda