跳转到内容

访客

25% developed
来自维基教科书,开放世界中的开放书籍

模板方法 计算机科学设计模式
访客

访问者模式允许您轻松地对复杂结构的每个元素执行给定的操作(函数、算法等)。优点是您可以轻松添加其他要执行的操作,并且可以轻松更改元素的结构。原则是执行操作的类和滚动结构的类是不同的。这也意味着,如果您知道您将始终对结构执行一个操作,那么此模式就毫无用处。

让我们以简单的类数据为例

元素
 
 

我们想对其执行一个操作。与其在类内部实现它,我们将使用访问者类。访问者类是一个对对象执行操作的类。它将通过对每个元素进行调用来对整个结构执行操作。但是,目前我们只有一个对象。访问者将由以下方法调用:访问数据对象的方法,而对象将由以下方法调用:接受客户端的方法

客户端
 
 
<<接口>>
访客
 
+visit(Element)
 
<<接口>>
元素
 
+accept(Visitor)
 
具体访问者
 
+visit(Element)
   
具体元素
 
+accept(Visitor)
   

然后,您可以定义数据对象的结构,每个对象将在其子元素上调用accept()方法。您可能会认为此模式的主要缺点是,即使访问者可以处理所有元素,它也不能将其作为一个整体进行处理,即它不能将不同的元素连接在一起,因此模式受到限制。并非如此。访问者可以保留有关其已访问元素的信息(例如,记录带有适当缩进的行)。如果访问者需要来自所有元素的信息(例如,根据总和计算百分比),它可以访问所有元素,存储信息,并在最后一次访问后使用另一种方法实际处理所有信息。换句话说,您可以在访问者中添加状态。

此过程基本上等同于获取集合的迭代器,并使用while(iterator.hasNext())循环调用visitor.visit(iterator.next())。关键区别在于,集合可以决定如何精确地遍历元素。如果您熟悉迭代器,您可能认为迭代器也可以决定如何遍历,并且迭代器通常由集合定义,那么有什么区别呢?区别在于迭代器仍然绑定到while循环,逐个访问每个元素。使用访问者模式,集合可以想象为每个元素创建一个单独的线程,并让访问者并发地访问它们。这只是集合可能决定以迭代器无法模拟的方式改变访问方法的方式的一个例子。关键是,它是封装的,集合可以以它认为最好的任何方式实现该模式。更重要的是,它可以随时更改实现,而不会影响客户端代码,因为实现是在foreach方法中封装的。

访问者模式的示例实现:字符串

为了说明访问者模式,让我们假设我们正在重新发明 Java 的 String 类(这将是一个非常荒谬的重新发明,但对本练习来说是好的)。我们不会实现该类的太多内容,但让我们假设我们正在存储一组char组成字符串的,我们有一个名为getCharAt的方法,它将int作为其唯一参数,并返回字符串中该位置的字符,作为char。我们还有一个名为length的方法,它不接受任何参数,并返回一个int,它给出字符串中的字符数。让我们还假设我们希望为该类提供访问者模式的实现,该实现将采用实现ICharacterVisitor接口(我们将在下面定义)的实例,并为字符串中的每个字符调用其访问方法。首先,我们需要定义这个ICharacterVisitor接口的样子

public interface ICharacterVisitor {
  public void visit(final char aChar);
}

足够简单。现在,让我们开始我们的类,我们将其命名为MyString,它看起来像这样

public class MyString {

// … other methods, fields

  // Our main implementation of the visitor pattern
  public void foreach(final ICharacterVisitor aVisitor) {
    int length = this.length();
    // Loop over all the characters in the string
    for (int i = 0; i < length; i++) {
      // Get the current character, and let the visitor visit it.
      aVisitor.visit(this.getCharAt(i));
    }
  }

// … other methods, fields

}// end class MyString

所以,这非常轻松。我们可以用它做什么?好吧,让我们创建一个名为MyStringPrinter的类,它将MyString的实例打印到标准输出。

public class MyStringPrinter implements ICharacterVisitor {

  // We have to implement this method because we're implementing the ICharacterVisitor
  // interface
  public void visit(final char aChar) {
    // All we're going to do is print the current character to the standard output
    System.out.print(aChar);
  }

  // This is the method you call when you want to print a string
  public void print(final MyString aStr) {
    // we'll let the string determine how to get each character, and
    // we already defined what to do with each character in our
    // visit method.
    aStr.foreach(this);
  }

} // end class MyStringPrinter

那也很简单。当然,它本来可以简单得多,对吧?我们不需要foreach方法在MyString中,我们不需要MyStringPrinter实现访问者,我们可以直接使用MyStringgetCharAtlength方法来设置我们自己的for循环,并在循环内打印每个 char。好吧,当然可以,但是如果MyString不是MyString而是MyBoxOfRocks.

访问者模式的另一个示例实现:岩石

在一盒岩石中,岩石是否按特定顺序排列?不太可能。当然MyBoxOfRocks必须以某种方式存储岩石。也许,它将它们存储在数组中,实际上岩石确实按特定顺序排列,即使它是为了存储而人为引入的。另一方面,也许它没有。关键是,再一次,这是一个实现细节,作为MyBoxOfRocks的客户端,你不必担心,而且永远不应该依赖它。

想必,MyBoxOfRocks希望为客户端提供某种方式来获取其内部的岩石。它也可以再一次为岩石引入一个人为顺序;为每块岩石分配一个索引,并提供类似以下的方法:public Rock getRock(int aRockNumber)。或者,也许它想为所有岩石命名,并让你像这样访问它:public Rock getRock(String aRockName)。但也许它真的只是一盒岩石,没有名字,没有数字,没有办法识别你想要哪块岩石;你只知道你想要岩石。好吧,让我们用访问者模式试试。首先,我们的访问者接口(假设Rock已经定义在某个地方,我们不关心它是什么或它做什么)

public interface IRockVisitor {
  public void visit(final Rock aRock);
}

简单。现在出来MyBoxOfRocks

public class MyBoxOfRocks {

  private Rock[] fRocks;

  //… some kind of instantiation code

  public void foreach(final IRockVisitor aVisitor) {
    int length = fRocks.length;
    for (int i = 0; i < length; i++) {
      aVisitor.visit(fRocks[i]);
    }
  }

} // End class MyBoxOfRocks

嗯,你知道吗,它确实将它们存储在数组中。但现在我们为什么要关心呢?我们已经编写了访问者接口,现在我们所要做的就是在某个类中实现它,该类定义了对每个岩石采取的操作,这将是我们必须在for循环内执行的操作。此外,该数组是私有的,我们的访问者无法访问它。

如果MyBoxOfRocks的实现者做了一些功课,发现将数字与零比较比将其与非零值比较快到无穷小?无穷小,当然,但是当你遍历 1000 万块岩石时,也许会有所不同(也许!)。因此,他决定更改实现

public void foreach(final IRockVisitor aVisitor) {
  int length = fRocks.length;
  for (int i = length - 1; i >= 0; i--) {
    aVisitor.visit(fRocks[i]);
  }
}

现在,他正在反向遍历数组并节省了(非常)少许时间。他改变了实现,因为他找到了一个更好的方法。你不必担心找到最好的方法,也不必更改代码,因为实现是封装的。这还不是全部。也许,一个新的程序员接管了这个项目,也许这个程序员讨厌数组,并决定完全改变它

public class MyBoxOfRocks {

// This coder really likes Linked Lists
  private class RockNode {
    Rock iRock;
    RockNode iNext;

    RockNode(final Rock aRock, final RockNode aNext) {
       this.iRock = aRock;
       this.iNext = aNext;
    }
  } // end inner class RockNode

  private RockNode fFirstRock;

  // … some instantiation code goes in here

  // Our new implementation
  public void foreach (final IRockVisitor aVisitor) {

    RockNode current = this.fFirstRock;
    // a null value indicates the list is ended
    while (current != null) {
      // have the visitor visit the current rock
      aVisitor.visit(current.iRock);
      // step to the next rock
      current = current.iNext;
    }
  }
}

现在,也许在这个例子中,链表是一个糟糕的想法,不如一个简洁的数组和一个for循环快。另一方面,你不知道这个类应该做些什么。也许,提供对岩石的访问只是它所做的事情的一小部分,而链表更符合其他要求。如果我没有说够的话,关键是作为MyBoxOfRocks的客户端,你不必担心实现的更改,访问者模式会保护你免受它的影响。

我还有最后一招。也许,MyBoxOfRocks的实现者注意到许多访问者花费了很长时间来访问每块岩石,并且foreach方法返回所需的时间太长,因为它必须等待所有访问者完成。他决定不能等那么久,并且他还决定,其中一些操作可能可以同时进行。因此,他决定做些什么,具体来说,就是这个

// Back to our backward-array model
public void foreach(final IRockVisitor aVisitor) {

  Thread t; // This should speed up the return

  int length = fRocks.length;
  for (int i = length - 1; i >= 0; i--) {
    final Rock current = fRocks[i];
    t = new Thread() {
      public void run() {
        aVisitor.visit(current);
      }
    }; // End anonymous Thread class

    t.start(); // Run the thread we just created.
  }
}

如果您熟悉线程,您将了解这里发生了什么。如果您不熟悉,我将快速总结一下:线程基本上是可以在同一台机器上与其他线程“同时”运行的东西。当然,它们并不真正同时运行,除非你可能有一台多处理器机器,但在 Java 看来,它们确实如此。因此,例如,当我们创建了这个名为t的新线程,并定义了线程运行时会发生什么(使用run方法,自然地),我们就可以启动线程,它将立即开始运行,与其他线程在处理器上分配周期,无需等待当前方法返回。同样,我们可以启动它运行,然后立即继续我们自己的方式,无需等待它返回。因此,使用上述实现,我们只需要在这个方法中花费的时间就是实例化所有线程所需的时间,start它们运行,并循环遍历数组;我们不必等待访问者实际访问每个Rock在循环之前,我们直接进入循环,访问者可以在任何可用的 CPU 周期上执行操作。整个访问过程可能需要很长时间,如果由于多线程而丢失了一些周期,甚至可能需要更长时间,但调用foreach的线程无需等待其完成:它可以从方法中返回,并更快地继续执行。

如果您对最终的用法感到困惑Rock在上面的代码中称为“current”,这仅仅是使用匿名类的一种技术细节:它们不能访问非 final 的局部变量。即使fRocks不属于此类别(它不是局部变量,而是一个实例变量),i是。如果您尝试删除此行并简单地放入fRocks[i]run方法中,它将无法编译。

那么,如果您是访问者,并且您决定需要一次访问每一块岩石,会发生什么?这可能有很多原因,例如,如果您的访问方法更改您的实例变量,或者它依赖于先前调用访问的结果。好吧,内部的实现foreach方法是封装的,因此您不知道它是否使用单独的线程。当然,您可以通过一些精密的调试或一些巧妙的打印到标准输出的方式来弄清楚,但如果您不必这样做,不是很好吗?而且如果您能够确信,如果他们在下一个版本中更改它,您的代码仍然可以正常工作?嗯,幸运的是,Java 提供了 *同步* 机制,它本质上是一种用于锁定代码块的复杂机制,以便一次只有一个线程可以访问它们。这也不会与多线程实现的利益冲突,因为被锁定的线程仍然不会阻塞创建它们的线程,但它们只会静静地等待,仅阻塞自身代码。然而,这一切都超出了本节的范围,但请注意它可用,如果您打算使用对同步敏感的访问者,可能值得研究一下。

示例

Clipboard

待办事项
找到一个例子。


成本

此模式足够灵活,不会阻止您。在最坏的情况下,您需要花费时间考虑如何解决问题,但此模式永远不会阻止您。

创建

如果您的代码已经存在,此模式的创建成本很高。

维护

即使在项目访问之间存在新的链接,也很容易使用此模式调整代码。

移除

可以使用 IDE 中的重构操作移除此模式。

建议

  • 使用 *访问者* 和 *访问* 术语向其他开发人员表明此模式的使用。
  • 如果您只有一个并且永远只有一个访问者,您最好实现 *组合* 模式。

实现

C# 中的实现

以下示例是 C# 编程语言 中的示例

using System;

namespace VisitorPattern
{
    class Program
    {
        static void Main(string[] args)
        {
            var car = new Car();

            CarElementVisitor doVisitor = new CarElementDoVisitor();
            CarElementVisitor printVisitor = new CarElementPrintVisitor();

            printVisitor.visit(car);
            doVisitor.visit(car);
        }
    }

    public interface CarElementVisitor
    {
        void visit(Body body);
        void visit(Car car);
        void visit(Engine engine);
        void visit(Wheel wheel);
    }
    public interface CarElement
    {
        void accept(CarElementVisitor visitor); // CarElements have to provide accept().
    }
    public class Wheel : CarElement
    {

        public String name { get; set; }

        public void accept(CarElementVisitor visitor)
        {
            visitor.visit(this);
        }
    }

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

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

    public class Car
    {
        public CarElement[] elements { get; private set; }

        public Car()
        {
            elements = new CarElement[]
			  { new Wheel{name = "front left"}, new Wheel{name = "front right"},
				new Wheel{name = "back left"} , new Wheel{name="back right"},
				new Body(), new Engine() };
        }
    }

    public class CarElementPrintVisitor : CarElementVisitor
    {
        public void visit(Body body)
        {
            Console.WriteLine("Visiting body");
        }
        public void visit(Car car)
        {
            Console.WriteLine("\nVisiting car");
            foreach (var element in car.elements)
            {
                element.accept(this);
            }
            Console.WriteLine("Visited car");
        }
        public void visit(Engine engine)
        {
            Console.WriteLine("Visiting engine");
        }
        public void visit(Wheel wheel)
        {
            Console.WriteLine("Visiting " + wheel.name + " wheel");
        }
    }

    public class CarElementDoVisitor : CarElementVisitor
    {
        public void visit(Body body)
        {
            Console.WriteLine("Moving my body");
        }
        public void visit(Car car)
        {
            Console.WriteLine("\nStarting my car");
            foreach (var element in car.elements)
            {
                element.accept(this);
            }
        }
        public void visit(Engine engine)
        {
            Console.WriteLine("Starting my engine");
        }
        public void visit(Wheel wheel)
        {
            Console.WriteLine("Kicking my " + wheel.name);
        }
    }
}
D 中的实现

以下示例是在 D 编程语言 中的

import std.stdio;
import std.string;

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

interface CarElement{
    void accept(CarElementVisitor visitor);
}

class Wheel : CarElement {
    private string name;
    this(string name) {
        this.name = name;
    }
    string getName() {
        return name;
    }
    public void accept(CarElementVisitor visitor) {
        visitor.visit(this);
    }
}

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

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

class Car {
    CarElement[] elements;
    public CarElement[] getElements() {
        return elements;
    }
    public this() {
        elements =
        [
            cast(CarElement) new Wheel("front left"),
            cast(CarElement) new Wheel("front right"),
            cast(CarElement) new Wheel("back left"),
            cast(CarElement) new Wheel("back right"),
            cast(CarElement) new Body(),
            cast(CarElement) new Engine()
        ];
    }
}

class CarElementPrintVisitor : CarElementVisitor {
    public void visit(Wheel wheel) {
        writefln("Visiting "~ wheel.getName() ~ " wheel");
    }
    public void visit(Car car) {
        writefln("\nVisiting car");
        foreach(CarElement element ; car.elements) {
            element.accept(this);
        }
        writefln("Visited car");
    }
    public void visit(Engine engine) {
        writefln("Visiting engine");
    }
    public void visit(Body bod) {
        writefln("Visiting body");
    }
}

class CarElementDoVisitor : CarElementVisitor {
    public void visit(Body bod) {
        writefln("Moving my body");
    }
    public void visit(Car car) {
        writefln("\nStarting my car");
        foreach(CarElement carElement ; car.getElements()) {
            carElement.accept(this);
        }
        writefln("Started car");
    }
    public void visit(Engine engine) {
        writefln("Starting my engine");
    }
    public void visit(Wheel wheel) {
        writefln("Kicking my "~ wheel.name);
    }
}

void main() {
    Car car = new Car;

    CarElementVisitor printVisitor = new CarElementPrintVisitor;
    CarElementVisitor doVisitor = new CarElementDoVisitor;
    printVisitor.visit(car);
    doVisitor.visit(car);
}
Java 中的实现

以下示例是在 Java 编程语言 中的

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

interface CarElement {
    void accept(CarElementVisitor visitor); // CarElements have to provide accept().
}
class Wheel implements CarElement {
    private String name;

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

    public String getName() {
        return this.name;
    }

    public void accept(final 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 Engine implements CarElement {
    /**
     * Accept the visitor.
     * This method will call the method visit(Engine)
     * and not visit(Wheel) nor visit(Body)
     * because <tt>this</tt> is declared as Engine.
     * That's why we need to define this code in each car element class.
     */
    public void accept(final CarElementVisitor visitor) {
        visitor.visit(this);
    }
}

class Body implements CarElement {
    /**
     * Accept the visitor.
     * This method will call the method visit(Body)
     * and not visit(Wheel) nor visit(Engine)
     * because <tt>this</tt> is declared as Body.
     * That's why we need to define this code in each car element class.
     */
    public void accept(final CarElementVisitor visitor) {
        visitor.visit(this);
    }
}
class Car implements CarElement {
    CarElement[] elements;

    public Car() {
        // Create new Array of elements
        this.elements = new CarElement[] { new Wheel("front left"),
            new Wheel("front right"), new Wheel("back left") ,
            new Wheel("back right"), new Body(), new Engine() };
    }

    public void accept(final CarElementVisitor visitor) {
        visitor.visit(this);
    }
}
/**
 * One visitor.
 * You can define as many visitor as you want.
 */
class CarElementPrintVisitor implements CarElementVisitor {
    public void visit(final Body body) {
        System.out.println("Visiting body");
    }

    public void visit(final Car car) {
        System.out.println("Visiting car");
        for(CarElement element : elements) {
            element.accept(visitor);
        }
        System.out.println("Visited car");
    }

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

    public void visit(final Wheel wheel) {
        System.out.println("Visiting " + wheel.getName() + " wheel");
    }
}
/**
 * Another visitor.
 * Each visitor has one functional purpose.
 */
class CarElementDoVisitor implements CarElementVisitor {
    public void visit(final Body body) {
        System.out.println("Moving my body");
    }

    public void visit(final Car car) {
        System.out.println("Starting my car");
        for(final CarElement element : elements) {
            element.accept(visitor);
        }
        System.out.println("Started my car");
    }

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

    public void visit(final Wheel wheel) {
        System.out.println("Kicking my " + wheel.getName() + " wheel");
    }
}
public class VisitorDemo {
    public static void main(final String[] arguments) {
        final CarElement car = new Car();

        car.accept(new CarElementPrintVisitor());
        car.accept(new CarElementDoVisitor());
    }
}
Lisp 中的实现
(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-right-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))
Scala 中的实现

以下示例是 Scala 编程语言 中的示例

trait Visitable {
  def accept[T](visit: Visitor[T]): T = visit(this)
}

trait Visitor[T] {
  def apply(visitable: Visitable): T
}

trait Node extends Visitable

trait Operand extends Node
case class IntegerLiteral(value: Long) extends Operand
case class PropertyReference(name: String) extends Operand

trait Operator extends Node
case object Greater extends Operator
case object Less extends Operator

case class ComparisonOperation(left: Operand, op: Operator, right: Operand) extends Node

class NoSqlStringifier extends Visitor[String] {
  def apply(visitable: Visitable): String = visitable match {
    case IntegerLiteral(value) => value.toString
    case PropertyReference(name: String) => name
    case Greater => s"&gt"
    case Less => "&lt"
    case ComparisonOperation(left, operator, right) =>
      s"${left.accept(this)}: { ${operator.accept(this)}: ${right.accept(this)} }"
  }
}

class SqlStringifier extends Visitor[String] {
  def apply(visitable: Visitable): String = visitable match {
    case IntegerLiteral(value) => value.toString
    case PropertyReference(name: String) => name
    case Greater => ">"
    case Less => "<"
    case ComparisonOperation(left, operator, right) =>
      s"WHERE ${ left.accept(this)} ${operator.accept(this)} ${right.accept(this) }"
  }
}

object VisitorPatternTest {
  def main(args: Array[String]) {
    val condition: Node = ComparisonOperation(PropertyReference("price"), Greater, IntegerLiteral(12))
    println(s"No sql representation = ${condition.accept(new NoSqlStringifier)}")
    println(s"Sql representation = ${condition.accept(new SqlStringifier)}")
  }
}

输出

   No sql representation = price: { &gt: 12 }
   Sql representation = WHERE price > 12


Clipboard

待办事项
添加更多说明。


模板方法 计算机科学设计模式
访客


您对本页有任何疑问吗?
在这里提问


在本图书上创建一个新页面


华夏公益教科书