Padrão de visitante - Visitor pattern

Na programação orientada a objetos e na engenharia de software , o padrão de design do visitante é uma maneira de separar um algoritmo de uma estrutura de objeto na qual ele opera. Um resultado prático dessa separação é a capacidade de adicionar novas operações às estruturas de objetos existentes sem modificar as estruturas. É uma maneira de seguir o princípio aberto / fechado .

Em essência, o visitante permite adicionar novas funções virtuais a uma família de classes , sem modificar as classes. Em vez disso, é criada uma classe de visitante que implementa todas as especializações apropriadas da função virtual. O visitante usa a referência de instância como entrada e implementa a meta por meio de despacho duplo .

Visão geral

O padrão de design do Visitante é um dos vinte e três padrões de design GoF bem conhecidos que descrevem como resolver problemas de design recorrentes para projetar software orientado a objetos flexível e reutilizável, ou seja, objetos que são mais fáceis de implementar, alterar, testar e reuso.

Que problemas o padrão de design do Visitante pode resolver?

  • Deve ser possível definir uma nova operação para (algumas) classes de uma estrutura de objeto sem alterar as classes.

Quando novas operações são necessárias com frequência e a estrutura do objeto consiste em muitas classes não relacionadas, é inflexível adicionar novas subclasses cada vez que uma nova operação é necessária porque "[..] distribuir todas essas operações pelas várias classes de nós leva a um sistema difícil para entender, manter e mudar. "

Qual solução o padrão de design do Visitante descreve?

  • Defina um objeto separado (visitante) que implementa uma operação a ser executada em elementos de uma estrutura de objeto.
  • Os clientes percorrem a estrutura do objeto e chamam uma operação de despacho aceitar (visitante) em um elemento - que "despacha" (delega) a solicitação para o "objeto visitante aceito". O objeto visitante executa a operação no elemento ("visita o elemento").

Isso torna possível criar novas operações independentemente das classes de uma estrutura de objeto, adicionando novos objetos de visitante.

Consulte também a classe UML e o diagrama de sequência abaixo.

Definição

A Gangue dos Quatro define o Visitante como:

Representando uma operação a ser executada em elementos de uma estrutura de objeto. Visitor permite definir uma nova operação sem alterar as classes dos elementos nos quais opera.

A natureza do Visitante o torna um padrão ideal para se conectar a APIs públicas, permitindo que seus clientes executem operações em uma classe usando uma classe "visitante" sem ter que modificar a fonte.

Usos

Mover operações para classes de visitantes é benéfico quando

  • muitas operações não relacionadas em uma estrutura de objeto são necessárias,
  • as classes que compõem a estrutura do objeto são conhecidas e não se espera que mudem,
  • novas operações precisam ser adicionadas com frequência,
  • um algoritmo envolve várias classes da estrutura do objeto, mas deseja-se gerenciá-lo em um único local,
  • um algoritmo precisa funcionar em várias hierarquias de classes independentes.

Uma desvantagem desse padrão, no entanto, é que ele torna as extensões da hierarquia de classes mais difíceis, já que novas classes geralmente exigem que um novo visitmétodo seja adicionado a cada visitante.

Exemplo de caso de uso

Considere o projeto de um sistema 2D de projeto auxiliado por computador (CAD). Em sua essência, existem vários tipos para representar formas geométricas básicas, como círculos, linhas e arcos. As entidades são ordenadas em camadas e, no topo da hierarquia de tipos, está o desenho, que é simplesmente uma lista de camadas, mais algumas propriedades adicionadas.

Uma operação fundamental nesta hierarquia de tipo é salvar um desenho no formato de arquivo nativo do sistema. À primeira vista, pode parecer aceitável adicionar métodos de salvamento locais a todos os tipos na hierarquia. Mas também é útil poder salvar desenhos em outros formatos de arquivo. Adicionar cada vez mais métodos para salvar em muitos formatos de arquivo diferentes logo desorganiza a estrutura de dados geométricos original relativamente pura.

Uma maneira ingênua de resolver isso seria manter funções separadas para cada formato de arquivo. Essa função de salvar pegaria um desenho como entrada, percorreria-o e codificaria nesse formato de arquivo específico. Como isso é feito para cada formato diferente adicionado, a duplicação entre as funções se acumula. Por exemplo, salvar uma forma de círculo em um formato raster requer um código muito semelhante, não importa qual forma raster específica é usada, e é diferente de outras formas primitivas. O caso de outras formas primitivas como linhas e polígonos é semelhante. Assim, o código se torna um grande loop externo percorrendo os objetos, com uma grande árvore de decisão dentro do loop questionando o tipo do objeto. Outro problema com esta abordagem é que é muito fácil perder uma forma em um ou mais protetores, ou uma nova forma primitiva é introduzida, mas a rotina de salvamento é implementada apenas para um tipo de arquivo e não para outros, levando à extensão e manutenção do código problemas.

Em vez disso, o padrão de visitante pode ser aplicado. Ele codifica uma operação lógica em toda a hierarquia em uma classe contendo um método por tipo. No exemplo CAD, cada função de salvamento seria implementada como uma subclasse de Visitante separada. Isso removeria toda a duplicação de verificações de tipo e etapas de travessia. Também faria o compilador reclamar se uma forma fosse omitida.

Outro motivo é reutilizar o código de iteração. Por exemplo, a iteração em uma estrutura de diretório pode ser implementada com um padrão de visitante. Isso permitiria a criação de pesquisas de arquivos, backups de arquivos, remoção de diretórios, etc., implementando um visitante para cada função enquanto reutiliza o código de iteração.

Estrutura

Classe UML e diagrama de sequência

Um diagrama de classe UML de amostra e um diagrama de sequência para o padrão de design do Visitor.

No diagrama de classes UML acima, a ElementAclasse não implementa uma nova operação diretamente. Em vez disso, ElementAimplementa uma operação de despacho accept(visitor) que "despacha" (delega) uma solicitação para o "objeto de visitante aceito" ( visitor.visitElementA(this)). A Visitor1classe implementa a operação ( visitElementA(e:ElementA)).
ElementBem seguida, implementa accept(visitor)enviando para visitor.visitElementB(this). A Visitor1classe implementa a operação ( visitElementB(e:ElementB)).

O diagrama de sequência UML mostra as interações em tempo de execução: O Clientobjeto atravessa os elementos de uma estrutura de objeto ( ElementA,ElementB) e chama accept(visitor)cada elemento.
Em primeiro lugar, as Clientchamadas accept(visitor)sobre ElementA, o que exige visitElementA(this)a aceita visitorobjeto. O próprio elemento ( this) é passado para o visitorpara que ele possa "visitar" ElementA(chamar operationA()).
Depois disso, as Clientchamadas accept(visitor)sobre ElementB, o que exige visitElementB(this)nos visitorque "visitas" ElementB(chamadas operationB()).

Diagrama de classes

Visitante em Unified Modeling Language (UML)
Visitante em LePUS3 ( legenda )

Detalhes

O padrão de visitante requer uma linguagem de programação que suporte envio único , como as linguagens orientadas a objetos comuns (como C ++ , Java , Smalltalk , Objective-C , Swift , JavaScript , Python e C # ) fazem. Sob essa condição, considere dois objetos, cada um de algum tipo de classe; um é denominado elemento e o outro é visitante .

O visitante declara um visitmétodo, que leva o elemento como um argumento, para cada classe de elemento. Visitantes concretos são derivados da classe visitante e implementam esses visitmétodos, cada um dos quais implementa parte do algoritmo que opera na estrutura do objeto. O estado do algoritmo é mantido localmente pela classe de visitante concreta.

O elemento declara um acceptmétodo para aceitar um visitante, tomando o visitante como argumento. Elementos concretos , derivados da classe de elemento, implementam o acceptmétodo. Em sua forma mais simples, isso nada mais é do que uma chamada ao visitmétodo do visitante . Os elementos compostos , que mantêm uma lista de objetos filho, costumam iterar sobre eles, chamando o acceptmétodo de cada filho .

O cliente cria a estrutura do objeto, direta ou indiretamente, e instancia os visitantes concretos. Quando uma operação implementada usando o padrão Visitor deve ser executada, ela chama o acceptmétodo do (s) elemento (s) de nível superior.

Quando o acceptmétodo é chamado no programa, sua implementação é escolhida com base no tipo dinâmico do elemento e no tipo estático do visitante. Quando o visitmétodo associado é chamado, sua implementação é escolhida com base no tipo dinâmico do visitante e no tipo estático do elemento, conforme conhecido na implementação do acceptmétodo, que é igual ao tipo dinâmico do elemento. (Como um bônus, se o visitante não puder lidar com um argumento do tipo de elemento fornecido, o compilador detectará o erro.)

Assim, a implementação do visitmétodo é escolhida com base no tipo dinâmico do elemento e no tipo dinâmico do visitante. Isso implementa efetivamente o despacho duplo . Para linguagens cujos sistemas de objetos suportam despacho múltiplo, não apenas despacho único, como Common Lisp ou C # por meio do Dynamic Language Runtime (DLR), a implementação do padrão de visitante é bastante simplificada (também conhecido como Visitante Dinâmico), permitindo o uso de sobrecarga de função simples para cobrir todos os casos visitados. Um visitante dinâmico, desde que opere apenas com dados públicos, está em conformidade com o princípio aberto / fechado (uma vez que não modifica as estruturas existentes) e com o princípio de responsabilidade única (uma vez que implementa o padrão Visitor em um componente separado).

Desta forma, um algoritmo pode ser escrito para percorrer um gráfico de elementos e muitos tipos diferentes de operações podem ser realizadas durante essa travessia, fornecendo diferentes tipos de visitantes para interagir com os elementos com base nos tipos dinâmicos de ambos os elementos e os visitantes.

Exemplo C #

Este exemplo declara uma ExpressionPrintingVisitorclasse separada que cuida da impressão.

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);
		}
	}
}

Exemplo Smalltalk

Nesse caso, é responsabilidade do objeto saber como se imprimir em um fluxo. O visitante aqui é então o objeto, não o fluxo.

"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.

Exemplo C ++

Fontes

#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);
  }
}

Saída

dispatching ArchivedFile
dispatching SplitFile
dispatching ExtractedFile

Ir exemplo

Go não suporta sobrecarga, portanto, os métodos de visita precisam de nomes diferentes.

Fontes

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)
}

Saída

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

Exemplo de Java

O exemplo a seguir está na linguagem Java e mostra como o conteúdo de uma árvore de nós (neste caso, descrevendo os componentes de um carro) pode ser impresso. Em vez de criar printmétodos para cada subclasse nó ( Wheel, Engine, Body, e Car), uma classe de visitante ( CarElementPrintVisitor) executa a acção de impressão necessário. Como as subclasses de nós diferentes requerem ações ligeiramente diferentes para serem impressas corretamente, CarElementPrintVisitordespacha ações com base na classe do argumento passado para seu visitmétodo. CarElementDoVisitor, que é análogo a uma operação de salvar para um formato de arquivo diferente, faz o mesmo.

Diagrama

Diagrama UML do exemplo de padrão Visitor com Car Elements

Fontes

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());
    }
}


Saída

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

Exemplo de Lisp Comum

Fontes

(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))

Saída

"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

O other-objectparâmetro é supérfluo em traverse. O motivo é que é possível usar uma função anônima que chama o método de destino desejado com um objeto capturado lexicamente:

(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)

Agora, o despacho múltiplo ocorre na chamada emitida do corpo da função anônima e, portanto, traverseé apenas uma função de mapeamento que distribui um aplicativo de função sobre os elementos de um objeto. Assim, todos os traços do Padrão de Visitante desaparecem, exceto para a função de mapeamento, na qual não há evidência de dois objetos sendo envolvidos. Todo o conhecimento da existência de dois objetos e um despacho em seus tipos está na função lambda.

Exemplo Python

Python não oferece suporte à sobrecarga de método no sentido clássico (comportamento polimórfico de acordo com o tipo de parâmetros passados), portanto, os métodos de "visita" para os diferentes tipos de modelo precisam ter nomes diferentes.

Fontes

"""
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())

Saída

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.

Abstração

Se alguém estiver usando Python 3 ou superior, eles podem fazer uma implementação geral do método de aceitação:

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

Pode-se estender isso para iterar sobre a ordem de resolução do método da classe se quiserem recorrer a classes já implementadas. Eles também podem usar o recurso de gancho da subclasse para definir a pesquisa antecipadamente.

Padrões de design relacionados

  • Padrão de iterador - define um princípio de passagem como o padrão de visitante, sem fazer uma diferenciação de tipo dentro dos objetos de passagem
  • Codificação de igreja - um conceito relacionado da programação funcional, em que os tipos de união / soma marcados podem ser modelados usando os comportamentos de "visitantes" em tais tipos, e que permite que o padrão de visitante emule variantes e padrões .

Veja também

Referências

links externos