跳轉到內容

訪客

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

待辦事項
新增更多說明。


模板方法 計算機科學設計模式
訪客


您對本頁有任何疑問嗎?
在這裡提問


在本圖書上建立一個新頁面


華夏公益教科書