Patrón de visitante - Visitor pattern

En la programación orientada a objetos y la ingeniería de software , el patrón de diseño del visitante es una forma de separar un algoritmo de una estructura de objeto en la que opera. Un resultado práctico de esta separación es la capacidad de agregar nuevas operaciones a las estructuras de objetos existentes sin modificar las estructuras. Es una forma de seguir el principio abierto / cerrado .

En esencia, el visitante permite agregar nuevas funciones virtuales a una familia de clases , sin modificar las clases. En cambio, se 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 mediante un envío doble .

Visión general

El patrón de diseño Visitor es uno de los veintitrés patrones de diseño GoF bien conocidos que describen cómo resolver problemas de diseño recurrentes para diseñar software orientado a objetos flexible y reutilizable, es decir, objetos que son más fáciles de implementar, cambiar, probar y reutilizar.

¿Qué problemas puede resolver el patrón de diseño de visitante?

  • Debería ser posible definir una nueva operación para (algunas) clases de una estructura de objeto sin cambiar las clases.

Cuando se necesitan nuevas operaciones con frecuencia y la estructura del objeto consta de muchas clases no relacionadas, es inflexible agregar nuevas subclases cada vez que se requiere una nueva operación porque "[..] distribuir todas estas operaciones en las diversas clases de nodos conduce a un sistema que es difícil para comprender, mantener y cambiar ".

¿Qué solución describe el patrón de diseño de visitante?

  • Defina un objeto separado (visitante) que implemente una operación que se realizará en elementos de una estructura de objeto.
  • Los clientes atraviesan la estructura del objeto y llaman a una operación de envío aceptar (visitante) en un elemento - que "envía" (delega) la solicitud al "objeto visitante aceptado". A continuación, el objeto visitante realiza la operación sobre el elemento ("visita el elemento").

Esto hace posible crear nuevas operaciones independientemente de las clases de una estructura de objeto agregando nuevos objetos de visitante.

Consulte también el diagrama de secuencia y clase UML a continuación.

Definición

La Banda de los Cuatro define al Visitante como:

Representa [ing] una operación a realizar en elementos de una estructura de objeto. Visitor le permite definir una nueva operación sin cambiar las clases de los elementos sobre los que opera.

La naturaleza del visitante lo convierte en un patrón ideal para conectarse a las API públicas, lo que permite a sus clientes realizar operaciones en una clase utilizando una clase "visitante" sin tener que modificar la fuente.

Usos

Mover las operaciones a las clases de visitantes es beneficioso cuando

  • se requieren muchas operaciones no relacionadas en una estructura de objeto,
  • las clases que componen la estructura del objeto son conocidas y no se espera que cambien,
  • es necesario agregar nuevas operaciones con frecuencia,
  • un algoritmo involucra varias clases de la estructura del objeto, pero se desea administrarlo en una sola ubicación,
  • un algoritmo debe funcionar en varias jerarquías de clases independientes.

Sin embargo, un inconveniente de este patrón es que dificulta las extensiones de la jerarquía de clases, ya que las nuevas clases normalmente requieren visitque se agregue un nuevo método a cada visitante.

Ejemplo de caso de uso

Considere el diseño de un sistema de diseño asistido por computadora (CAD) en 2D . En esencia, existen varios tipos para representar formas geométricas básicas como círculos, líneas y arcos. Las entidades están ordenadas en capas, y en la parte superior de la jerarquía de tipos está el dibujo, que es simplemente una lista de capas, más algunas propiedades agregadas.

Una operación fundamental en esta jerarquía de tipos es guardar un dibujo en el formato de archivo nativo del sistema. A primera vista, puede parecer aceptable agregar métodos de guardado locales a todos los tipos de la jerarquía. Pero también es útil poder guardar dibujos en otros formatos de archivo. Agregar cada vez más métodos para guardar en muchos formatos de archivo diferentes pronto satura la estructura de datos geométrica original relativamente pura.

Una forma ingenua de resolver esto sería mantener funciones separadas para cada formato de archivo. Esta función de guardado tomaría un dibujo como entrada, lo atravesaría y codificaría en ese formato de archivo específico. Como esto se hace para cada formato diferente agregado, se acumula la duplicación entre las funciones. Por ejemplo, guardar una forma de círculo en un formato de ráster requiere un código muy similar sin importar qué forma de ráster específica se use, y es diferente de otras formas primitivas. El caso de otras formas primitivas como líneas y polígonos es similar. Por lo tanto, el código se convierte en un gran bucle exterior que atraviesa los objetos, con un gran árbol de decisión dentro del bucle que consulta el tipo de objeto. Otro problema con este enfoque es que es muy fácil pasar por alto una forma en uno o más ahorradores, o se introduce una nueva forma primitiva, pero la rutina de guardado se implementa solo para un tipo de archivo y no para otros, lo que lleva a la extensión y mantenimiento del código. problemas.

En cambio, se puede aplicar el patrón de visitantes. Codifica una operación lógica en toda la jerarquía en una clase que contiene un método por tipo. En el ejemplo de CAD, cada función de guardado se implementaría como una subclase de visitante separada. Esto eliminaría toda duplicación de comprobaciones de tipo y pasos transversales. También haría que el compilador se quejara si se omite una forma.

Otro motivo es reutilizar el código de iteración. Por ejemplo, la iteración sobre una estructura de directorio podría implementarse con un patrón de visitante. Esto permitiría crear búsquedas de archivos, copias de seguridad de archivos, eliminación de directorios, etc., implementando un visitante para cada función mientras se reutiliza el código de iteración.

Estructura

Diagrama de secuencia y clase UML

Un diagrama de secuencia y un diagrama de clases UML de muestra para el patrón de diseño del visitante.

En el diagrama de clases de UML anterior, la ElementAclase no implementa una nueva operación directamente. En su lugar, ElementAimplementa una operación de envío accept(visitor) que "envía" (delega) una solicitud al "objeto visitante aceptado" ( visitor.visitElementA(this)). La Visitor1clase implementa la operación ( visitElementA(e:ElementA)).
ElementBluego implementa accept(visitor)enviando a visitor.visitElementB(this). La Visitor1clase implementa la operación ( visitElementB(e:ElementB)).

El diagrama de secuencia UML muestra las interacciones en tiempo de ejecución: el Clientobjeto atraviesa los elementos de una estructura de objeto ( ElementA,ElementB) y llama accept(visitor)a cada elemento.
Primero, las Clientllamadas accept(visitor)on ElementA, que invoca visitElementA(this)el visitorobjeto aceptado . El elemento en sí ( this) se pasa al visitorpara que pueda "visitar" ElementA(llamar operationA()).
A partir de entonces, las Clientllamadas accept(visitor)sobre ElementB, lo que exige visitElementB(this)en los visitorque las "visitas" ElementB(llamadas operationB()).

Diagrama de clase

Visitante en LePUS3 ( leyenda )

Detalles

El patrón de visitante requiere un lenguaje de programación que admita el envío único , como lo hacen los lenguajes orientados a objetos comunes (como C ++ , Java , Smalltalk , Objective-C , Swift , JavaScript , Python y C # ). Bajo esta condición, considere dos objetos, cada uno de algún tipo de clase; uno se denomina elemento y el otro visitante .

El visitante declara un visitmétodo, que toma el elemento como argumento, para cada clase de elemento. Los visitantes concretos se derivan de la clase de visitante e implementan estos visitmétodos, cada uno de los cuales implementa parte del algoritmo que opera en la estructura del objeto. El estado del algoritmo lo mantiene localmente la clase de visitante concreta.

El elemento declara un acceptmétodo para aceptar un visitante, tomando al visitante como argumento. Los elementos concretos , derivados de la clase de elemento, implementan el acceptmétodo. En su forma más simple, esto no es más que una llamada al visitmétodo del visitante . Los elementos compuestos , que mantienen una lista de objetos secundarios, suelen iterar sobre ellos, llamando al acceptmétodo de cada niño .

El cliente crea la estructura del objeto, directa o indirectamente, e instancia a los visitantes concretos. Cuando se va a realizar una operación que se implementa utilizando el patrón Visitor, llama al acceptmétodo de los elementos de nivel superior.

Cuando acceptse llama al método en el programa, su implementación se elige según el tipo dinámico del elemento y el tipo estático del visitante. Cuando visitse llama al método asociado , su implementación se elige en función tanto del tipo dinámico del visitante como del tipo estático del elemento, como se conoce desde la implementación del acceptmétodo, que es el mismo que el tipo dinámico del elemento. (Como beneficio adicional, si el visitante no puede manejar un argumento del tipo de elemento dado, el compilador detectará el error).

Por lo tanto, la implementación del visitmétodo se elige en función tanto del tipo dinámico del elemento como del tipo dinámico del visitante. Esto implementa efectivamente el doble despacho . Para los lenguajes cuyos sistemas de objetos admiten el envío múltiple, no solo el envío único, como Common Lisp o C # a través de Dynamic Language Runtime (DLR), la implementación del patrón de visitante se simplifica enormemente (también conocido como Dynamic Visitor) al permitir el uso de la sobrecarga de funciones simples para cubrir todos los casos que se visitan. Un visitante dinámico, siempre que opere solo con datos públicos, se ajusta al principio abierto / cerrado (ya que no modifica las estructuras existentes) y al principio de responsabilidad única (ya que implementa el patrón de visitante en un componente separado).

De esta manera, se puede escribir un algoritmo para atravesar un gráfico de elementos, y se pueden realizar muchos tipos diferentes de operaciones durante ese recorrido proporcionando diferentes tipos de visitantes para interactuar con los elementos en función de los tipos dinámicos tanto de los elementos como de la visitantes.

Ejemplo de C #

Este ejemplo declara una ExpressionPrintingVisitorclase separada que se encarga de la impresión.

namespace Wikipedia
{
	public class ExpressionPrintingVisitor
	{
		public void PrintLiteral(Literal literal)
		{
			Console.WriteLine(literal.Value);
		}
		
		public void PrintAddition(Addition addition)
		{
			double leftValue = addition.Left.GetValue();
			double rightValue = addition.Right.GetValue();
			var sum = addition.GetValue();
			Console.WriteLine("{0} + {1} = {2}", leftValue, rightValue, sum);
		}
	}
	
	public abstract class Expression
	{	
		public abstract void Accept(ExpressionPrintingVisitor v);
		
		public abstract double GetValue();
	}

	public class Literal : Expression
	{
		public double Value { get; set; }

		public Literal(double value)
		{
			this.Value = value;
		}
		
		public override void Accept(ExpressionPrintingVisitor v)
		{
			v.PrintLiteral(this);
		}
		
		public override double GetValue()
		{
			return Value;
		}
	}

	public class Addition : Expression
	{
		public Expression Left { get; set; }
		public Expression Right { get; set; }

		public Addition(Expression left, Expression right)
		{
			Left = left;
			Right = right;
		}
		
		public override void Accept(ExpressionPrintingVisitor v)
		{
			Left.Accept(v);
			Right.Accept(v);
			v.PrintAddition(this);
		}
		
		public override double GetValue()
		{
			return Left.GetValue() + Right.GetValue();	
		}
	}

	public static class Program
	{
		public static void Main(string[] args)
		{
			// Emulate 1 + 2 + 3
			var e = new Addition(
				new Addition(
					new Literal(1),
					new Literal(2)
				),
				new Literal(3)
			);
			
			var printingVisitor = new ExpressionPrintingVisitor();
			e.Accept(printingVisitor);
		}
	}
}

Ejemplo de Smalltalk

En este caso, es responsabilidad del objeto saber cómo imprimirse en una secuencia. El visitante aquí es entonces el objeto, no la corriente.

"There's no syntax for creating a class. Classes are created by sending messages to other classes."
WriteStream subclass: #ExpressionPrinter
    instanceVariableNames: ''
    classVariableNames: ''
    package: 'Wikipedia'.

ExpressionPrinter>>write: anObject
    "Delegates the action to the object. The object doesn't need to be of any special
    class; it only needs to be able to understand the message #putOn:"
    anObject putOn: self.
    ^ anObject.

Object subclass: #Expression
    instanceVariableNames: ''
    classVariableNames: ''
    package: 'Wikipedia'.

Expression subclass: #Literal
    instanceVariableNames: 'value'
    classVariableNames: ''
    package: 'Wikipedia'.

Literal class>>with: aValue
    "Class method for building an instance of the Literal class"
    ^ self new
        value: aValue;
        yourself.

Literal>>value: aValue
  "Setter for value"
  value := aValue.

Literal>>putOn: aStream
    "A Literal object knows how to print itself"
    aStream nextPutAll: value asString.

Expression subclass: #Addition
    instanceVariableNames: 'left right'
    classVariableNames: ''
    package: 'Wikipedia'.

Addition class>>left: a right: b
    "Class method for building an instance of the Addition class"
    ^ self new
        left: a;
        right: b;
        yourself.

Addition>>left: anExpression
    "Setter for left"
    left := anExpression.

Addition>>right: anExpression
    "Setter for right"
    right := anExpression.

Addition>>putOn: aStream
    "An Addition object knows how to print itself"
    aStream nextPut: $(.
    left putOn: aStream.
    aStream nextPut: $+.
    right putOn: aStream.
    aStream nextPut: $).

Object subclass: #Program
    instanceVariableNames: ''
    classVariableNames: ''
    package: 'Wikipedia'.

Program>>main
    | expression stream |
    expression := Addition
                    left: (Addition
                            left: (Literal with: 1)
                            right: (Literal with: 2))
                    right: (Literal with: 3).
    stream := ExpressionPrinter on: (String new: 100).
    stream write: expression.
    Transcript show: stream contents.
    Transcript flush.

Ejemplo de C ++

Fuentes

#include <iostream>
#include <vector>

class AbstractDispatcher;  // Forward declare AbstractDispatcher

class File {  // Parent class for the elements (ArchivedFile, SplitFile and
              // ExtractedFile)
 public:
  // This function accepts an object of any class derived from
  // AbstractDispatcher and must be implemented in all derived classes
  virtual void Accept(AbstractDispatcher& dispatcher) = 0;
};

// Forward declare specific elements (files) to be dispatched
class ArchivedFile;
class SplitFile;
class ExtractedFile;

class AbstractDispatcher {  // Declares the interface for the dispatcher
 public:
  // Declare overloads for each kind of a file to dispatch
  virtual void Dispatch(ArchivedFile& file) = 0;
  virtual void Dispatch(SplitFile& file) = 0;
  virtual void Dispatch(ExtractedFile& file) = 0;
};

class ArchivedFile : public File {  // Specific element class #1
 public:
  // Resolved at runtime, it calls the dispatcher's overloaded function,
  // corresponding to ArchivedFile.
  void Accept(AbstractDispatcher& dispatcher) override {
    dispatcher.Dispatch(*this);
  }
};

class SplitFile : public File {  // Specific element class #2
 public:
  // Resolved at runtime, it calls the dispatcher's overloaded function,
  // corresponding to SplitFile.
  void Accept(AbstractDispatcher& dispatcher) override {
    dispatcher.Dispatch(*this);
  }
};

class ExtractedFile : public File {  // Specific element class #3
 public:
  // Resolved at runtime, it calls the dispatcher's overloaded function,
  // corresponding to ExtractedFile.
  void Accept(AbstractDispatcher& dispatcher) override {
    dispatcher.Dispatch(*this);
  }
};

class Dispatcher : public AbstractDispatcher {  // Implements dispatching of all
                                                // kind of elements (files)
 public:
  void Dispatch(ArchivedFile&) override {
    std::cout << "dispatching ArchivedFile" << std::endl;
  }

  void Dispatch(SplitFile&) override {
    std::cout << "dispatching SplitFile" << std::endl;
  }

  void Dispatch(ExtractedFile&) override {
    std::cout << "dispatching ExtractedFile" << std::endl;
  }
};

int main() {
  ArchivedFile archived_file;
  SplitFile split_file;
  ExtractedFile extracted_file;

  std::vector<File*> files = {
      &archived_file,
      &split_file,
      &extracted_file,
  };

  Dispatcher dispatcher;
  for (File* file : files) {
    file->Accept(dispatcher);
  }
}

Producción

dispatching ArchivedFile
dispatching SplitFile
dispatching ExtractedFile

Ir ejemplo

Go no admite la sobrecarga, por lo que los métodos de visita necesitan nombres diferentes.

Fuentes

package main

import "fmt"

type Visitor interface {
	visitWheel(wheel Wheel) string
	visitEngine(engine Engine) string
	visitBody(body Body) string
	visitCar(car Car) string
}

type element interface {
	Accept(visitor Visitor) string
}

type Wheel struct {
	name string
}

func (w *Wheel) Accept(visitor Visitor) string {
	return visitor.visitWheel(*w)
}

func (w *Wheel) getName() string {
	return w.name
}

type Engine struct{}

func (e *Engine) Accept(visitor Visitor) string {
	return visitor.visitEngine(*e)
}

type Body struct{}

func (b *Body) Accept(visitor Visitor) string {
	return visitor.visitBody(*b)
}

type Car struct {
	engine Engine
	body   Body
	wheels [4]Wheel
}

func (c *Car) Accept(visitor Visitor) string {
	elements := []element{
		&c.engine,
		&c.body,
		&c.wheels[0],
		&c.wheels[1],
		&c.wheels[2],
		&c.wheels[3],
	}
	res := visitor.visitCar(*c)
	for _, elem := range elements {
		res += elem.Accept(visitor)
	}
	return res
}

type PrintVisitor struct{}

func (pv *PrintVisitor) visitWheel(wheel Wheel) string {
	return fmt.Sprintln("visiting", wheel.getName(), "wheel")
}
func (pv *PrintVisitor) visitEngine(engine Engine) string {
	return fmt.Sprintln("visiting engine")
}
func (pv *PrintVisitor) visitBody(body Body) string {
	return fmt.Sprintln("visiting body")
}
func (pv *PrintVisitor) visitCar(car Car) string {
	return fmt.Sprintln("visiting car")
}

/* output:
visiting car
visiting engine
visiting body
visiting front left wheel
visiting front right wheel
visiting back left wheel
visiting back right wheel
*/
func main() {
	car := Car{
		engine: Engine{},
		body:   Body{},
		wheels: [4]Wheel{
			{"front left"},
			{"front right"},
			{"back left"},
			{"back right"},
		},
	}

	visitor := PrintVisitor{}
	res := car.Accept(&visitor)
	fmt.Println(res)
}

Producción

visiting car
visiting engine
visiting body
visiting front left wheel
visiting front right wheel
visiting back left wheel
visiting back right wheel

Ejemplo de Java

El siguiente ejemplo está en el lenguaje Java y muestra cómo se puede imprimir el contenido de un árbol de nodos (en este caso describiendo los componentes de un automóvil). En lugar de crear printmétodos para cada subclase nodo ( Wheel, Engine, Body, y Car), una clase visitante ( CarElementPrintVisitor) realiza la acción de impresión deseado. Debido a que las diferentes subclases de nodos requieren acciones ligeramente diferentes para imprimirse correctamente, CarElementPrintVisitordistribuye acciones según la clase del argumento pasado a su visitmétodo. CarElementDoVisitor, que es análoga a una operación de guardado para un formato de archivo diferente, hace lo mismo.

Diagrama

Diagrama UML del ejemplo de patrón de visitante con Car Elements

Fuentes

import java.util.List;

interface CarElement {
    void accept(CarElementVisitor visitor);
}

interface CarElementVisitor {
    void visit(Body body);
    void visit(Car car);
    void visit(Engine engine);
    void visit(Wheel wheel);
}

class Wheel implements CarElement {
  private final String name;

  public Wheel(final String name) {
      this.name = name;
  }

  public String getName() {
      return name;
  }

  @Override
  public void accept(CarElementVisitor visitor) {
      /*
       * accept(CarElementVisitor) in Wheel implements
       * accept(CarElementVisitor) in CarElement, so the call
       * to accept is bound at run time. This can be considered
       * the *first* dispatch. However, the decision to call
       * visit(Wheel) (as opposed to visit(Engine) etc.) can be
       * made during compile time since 'this' is known at compile
       * time to be a Wheel. Moreover, each implementation of
       * CarElementVisitor implements the visit(Wheel), which is
       * another decision that is made at run time. This can be
       * considered the *second* dispatch.
       */
      visitor.visit(this);
  }
}

class Body implements CarElement {
  @Override
  public void accept(CarElementVisitor visitor) {
      visitor.visit(this);
  }
}

class Engine implements CarElement {
  @Override
  public void accept(CarElementVisitor visitor) {
      visitor.visit(this);
  }
}

class Car implements CarElement {
    private final List<CarElement> elements;

    public Car() {
        this.elements = List.of(
            new Wheel("front left"), new Wheel("front right"),
            new Wheel("back left"), new Wheel("back right"),
            new Body(), new Engine()
        );
    }

    @Override
    public void accept(CarElementVisitor visitor) {
        for (CarElement element : elements) {
            element.accept(visitor);
        }
        visitor.visit(this);
    }
}

class CarElementDoVisitor implements CarElementVisitor {
    @Override
    public void visit(Body body) {
        System.out.println("Moving my body");
    }

    @Override
    public void visit(Car car) {
        System.out.println("Starting my car");
    }

    @Override
    public void visit(Wheel wheel) {
        System.out.println("Kicking my " + wheel.getName() + " wheel");
    }

    @Override
    public void visit(Engine engine) {
        System.out.println("Starting my engine");
    }
}

class CarElementPrintVisitor implements CarElementVisitor {
    @Override
    public void visit(Body body) {
        System.out.println("Visiting body");
    }

    @Override
    public void visit(Car car) {
        System.out.println("Visiting car");
    }

    @Override
    public void visit(Engine engine) {
        System.out.println("Visiting engine");
    }

    @Override
    public void visit(Wheel wheel) {
        System.out.println("Visiting " + wheel.getName() + " wheel");
    }
}

public class VisitorDemo {
    public static void main(final String[] args) {
        Car car = new Car();

        car.accept(new CarElementPrintVisitor());
        car.accept(new CarElementDoVisitor());
    }
}


Producción

Visiting front left wheel
Visiting front right wheel
Visiting back left wheel
Visiting back right wheel
Visiting body
Visiting engine
Visiting car
Kicking my front left wheel
Kicking my front right wheel
Kicking my back left wheel
Kicking my back right wheel
Moving my body
Starting my engine
Starting my car

Ejemplo de Common Lisp

Fuentes

(defclass auto ()
  ((elements :initarg :elements)))

(defclass auto-part ()
  ((name :initarg :name :initform "<unnamed-car-part>")))

(defmethod print-object ((p auto-part) stream)
  (print-object (slot-value p 'name) stream))

(defclass wheel (auto-part) ())

(defclass body (auto-part) ())

(defclass engine (auto-part) ())

(defgeneric traverse (function object other-object))

(defmethod traverse (function (a auto) other-object)
  (with-slots (elements) a
    (dolist (e elements)
      (funcall function e other-object))))

;; do-something visitations

;; catch all
(defmethod do-something (object other-object)
  (format t "don't know how ~s and ~s should interact~%" object other-object))

;; visitation involving wheel and integer
(defmethod do-something ((object wheel) (other-object integer))
  (format t "kicking wheel ~s ~s times~%" object other-object))

;; visitation involving wheel and symbol
(defmethod do-something ((object wheel) (other-object symbol))
  (format t "kicking wheel ~s symbolically using symbol ~s~%" object other-object))

(defmethod do-something ((object engine) (other-object integer))
  (format t "starting engine ~s ~s times~%" object other-object))

(defmethod do-something ((object engine) (other-object symbol))
  (format t "starting engine ~s symbolically using symbol ~s~%" object other-object))

(let ((a (make-instance 'auto
                        :elements `(,(make-instance 'wheel :name "front-left-wheel")
                                    ,(make-instance 'wheel :name "front-right-wheel")
                                    ,(make-instance 'wheel :name "rear-left-wheel")
                                    ,(make-instance 'wheel :name "rear-right-wheel")
                                    ,(make-instance 'body :name "body")
                                    ,(make-instance 'engine :name "engine")))))
  ;; traverse to print elements
  ;; stream *standard-output* plays the role of other-object here
  (traverse #'print a *standard-output*)

  (terpri) ;; print newline

  ;; traverse with arbitrary context from other object
  (traverse #'do-something a 42)

  ;; traverse with arbitrary context from other object
  (traverse #'do-something a 'abc))

Producción

"front-left-wheel"
"front-right-wheel"
"rear-left-wheel"
"rear-right-wheel"
"body"
"engine"
kicking wheel "front-left-wheel" 42 times
kicking wheel "front-right-wheel" 42 times
kicking wheel "rear-left-wheel" 42 times
kicking wheel "rear-right-wheel" 42 times
don't know how "body" and 42 should interact
starting engine "engine" 42 times
kicking wheel "front-left-wheel" symbolically using symbol ABC
kicking wheel "front-right-wheel" symbolically using symbol ABC
kicking wheel "rear-left-wheel" symbolically using symbol ABC
kicking wheel "rear-right-wheel" symbolically using symbol ABC
don't know how "body" and ABC should interact
starting engine "engine" symbolically using symbol ABC

Notas

El other-objectparámetro es superfluo en traverse. La razón es que es posible utilizar una función anónima que llame al método de destino deseado con un objeto capturado léxicamente:

(defmethod traverse (function (a auto)) ;; other-object removed
  (with-slots (elements) a
    (dolist (e elements)
      (funcall function e)))) ;; from here too

  ;; ...

  ;; alternative way to print-traverse
  (traverse (lambda (o) (print o *standard-output*)) a)

  ;; alternative way to do-something with
  ;; elements of a and integer 42
  (traverse (lambda (o) (do-something o 42)) a)

Ahora, el envío múltiple ocurre en la llamada emitida desde el cuerpo de la función anónima, por lo que traversees solo una función de mapeo que distribuye una aplicación de función sobre los elementos de un objeto. Por lo tanto, desaparecen todos los rastros del patrón de visitante, excepto la función de mapeo, en la que no hay evidencia de que estén involucrados dos objetos. Todo el conocimiento de que hay dos objetos y un envío de sus tipos está en la función lambda.

Ejemplo de Python

Python no admite la sobrecarga de métodos en el sentido clásico (comportamiento polimórfico según el tipo de parámetros pasados), por lo que los métodos de "visita" para los diferentes tipos de modelos deben tener nombres diferentes.

Fuentes

"""
Visitor pattern example.
"""

from abc import ABCMeta, abstractmethod

NOT_IMPLEMENTED = "You should implement this."

class CarElement:
    __metaclass__ = ABCMeta
    @abstractmethod
    def accept(self, visitor):
        raise NotImplementedError(NOT_IMPLEMENTED)

class Body(CarElement):
    def accept(self, visitor):
        visitor.visitBody(self)

class Engine(CarElement):
    def accept(self, visitor):
        visitor.visitEngine(self)

class Wheel(CarElement):
    def __init__(self, name):
        self.name = name
    def accept(self, visitor):
        visitor.visitWheel(self)

class Car(CarElement):
    def __init__(self):
        self.elements = [
            Wheel("front left"), Wheel("front right"),
            Wheel("back left"), Wheel("back right"),
            Body(), Engine()
        ]

    def accept(self, visitor):
        for element in self.elements:
            element.accept(visitor)
        visitor.visitCar(self)

class CarElementVisitor:
    __metaclass__ = ABCMeta
    @abstractmethod
    def visitBody(self, element):
        raise NotImplementedError(NOT_IMPLEMENTED)
    @abstractmethod
    def visitEngine(self, element):
        raise NotImplementedError(NOT_IMPLEMENTED)
    @abstractmethod
    def visitWheel(self, element):
        raise NotImplementedError(NOT_IMPLEMENTED)
    @abstractmethod
    def visitCar(self, element):
        raise NotImplementedError(NOT_IMPLEMENTED)

class CarElementDoVisitor(CarElementVisitor):
    def visitBody(self, body):
        print("Moving my body.")
    def visitCar(self, car):
        print("Starting my car.")
    def visitWheel(self, wheel):
        print("Kicking my {} wheel.".format(wheel.name))
    def visitEngine(self, engine):
        print("Starting my engine.")

class CarElementPrintVisitor(CarElementVisitor):
    def visitBody(self, body):
        print("Visiting body.")
    def visitCar(self, car):
        print("Visiting car.")
    def visitWheel(self, wheel):
        print("Visiting {} wheel.".format(wheel.name))
    def visitEngine(self, engine):
        print("Visiting engine.")

car = Car()
car.accept(CarElementPrintVisitor())
car.accept(CarElementDoVisitor())

Producción

Visiting front left wheel.
Visiting front right wheel.
Visiting back left wheel.
Visiting back right wheel.
Visiting body.
Visiting engine.
Visiting car.
Kicking my front left wheel.
Kicking my front right wheel.
Kicking my back left wheel.
Kicking my back right wheel.
Moving my body.
Starting my engine.
Starting my car.

Abstracción

Si uno está usando Python 3 o superior, puede hacer una implementación general del método accept:

class Visitable:
    def accept(self, visitor):
        lookup = "visit_" + type(self).__qualname__.replace(".", "_")
        return getattr(visitor, lookup)(self)

Se podría extender esto para iterar sobre el orden de resolución del método de la clase si quisieran recurrir a clases ya implementadas. También podrían usar la función de enlace de subclase para definir la búsqueda por adelantado.

Patrones de diseño relacionados

  • Patrón de iterador : define un principio transversal como el patrón de visitante, sin hacer una diferenciación de tipo dentro de los objetos atravesados.
  • Codificación de la iglesia : un concepto relacionado de la programación funcional, en el que los tipos de unión / suma etiquetados se pueden modelar utilizando los comportamientos de los "visitantes" en dichos tipos, y que permite que el patrón de visitante emule variantes y patrones .

Ver también

Referencias

enlaces externos