1. 程式人生 > 實用技巧 >【設計模式】訪問者模式

【設計模式】訪問者模式

訪問者模式

簡介

訪問者模式:封裝一些作用於某種資料結構中的各元素的操作,它可以在不改變資料結構的前提下定義於這些元素的新操作。

重要:為什麼不能通過方法過載來簡單地替換訪問者?

class Exporter is
    method export(s: Shape) is
        print("匯出形狀")
    method export(d: Dot)
        print("匯出點")
    method export(c: Circle)
        print("匯出圓形")
    method export(r: Rectangle)
        print("匯出矩形")
    method export(cs: CompoundGraphic)
        print("匯出組合圖形")

// 使用
class App() is
    method export(shape: Shape) is
        Exporter exporter = new Exporter()
        exporter.export(shape);

app.export(new Circle());
// 不幸的是,這裡將輸出“匯出形狀”。

後期/動態繫結

動態程式語言:PythonRubyPHPJavaScriptErlang

# 編譯這段程式碼:

method drawShape(shape: Shape) is
    shape.draw();

最終的機器程式碼將檢查shape引數的類並且從合適的類中選擇draw方法的實現。

這種動態型別檢查被稱為後期(或動態)繫結:

  • 後期,是因為在編譯後和執行時才將物件及其實現連結起來。
  • 動態,是因為每個新物件都可能需要連結到不同的實現。

前期/靜態繫結

靜態程式語言:CC++C#JavaObject-C

// 編譯這段程式碼:

method exportShape(shape: Shape) is
    Exporter exporter = new Exporter()
    exporter.export(shape);

編譯器不能確保存在適當的與重寫後的方法相對應的過載方法。編譯器無法應對這種模凌兩可的情況。因此, 編譯器開發者會選擇安全的方式: 使用前期 (或靜態) 繫結來處理過載方法。

  • 前期,是因為它發生在執行程式前編譯的時候。
  • 靜態,是因為它無法在執行時更改。

雙分派

雙分派是一個允許在過載是使用動態繫結的技巧。

class Visitor is
    method visit(s: Shape) is
        print("訪問形狀")
    method visit(d: Dot)
        print("訪問點")

interface Graphic is
    method accept(v: Visitor)

class Shape implements Graphic is
    method accept(v: Visitor)
        // 編譯器明確知道 `this` 的型別是 `Shape`。
        // 因此可以安全地呼叫 `visit(s: Shape)`。
        v.visit(this)

class Dot extends Shape is
    method accept(v: Visitor)
        // 編譯器明確知道 `this` 的型別是 `Dot`。
        // 因此可以安全地呼叫 `visit(s: Dot)`。
        v.visit(this)


Visitor v = new Visitor();
Graphic g = new Dot();

// `accept` 方法是重寫而不是過載的。編譯器可以進行動態繫結。
// 因此在物件呼叫某個方法時,將執行其所屬類中的 `accept`
// 方法(在本例中是 `Dot` 類)。
g.accept(v);

// 輸出:"訪問點"

總結

儘管訪問者模式基於雙分派的原則建立,但是這並不是其主要目的。訪問者的目的是讓你能為整個類層次結構新增"外部"操作,而無需修改這些類的已有程式碼。

結構

實現

實現方式:

  • 在訪問者介面中宣告一組"訪問"方法,分別對應程式中的每個具體元素類。
  • 宣告元素介面。如果程式中已有元素類層次介面,可在層次結構基類中新增抽象的"接收"方法,該方法必須接受訪問者物件作為引數。
  • 在所有具體元素類中實現接收方法。這些方法必須將呼叫重定向到當前元素對應的訪問者物件中的訪問者方法上。
  • 元素類只能通過訪問者介面和訪問者進行互動。不過訪問者必須知曉所有的具體元素類,因為這些類在訪問者方法中都被作為引數型別引用。
  • 為每個無法在元素層次結構中實現的行為建立一個具體訪問者類並實現所有的訪問者方法。
  • 客戶端必須建立訪問者物件並通過"接收"方法將其傳遞給元素。
#include <iostream>
#include <array>


class ConcreteComponentA;
class ConcreteComponentB;

// 訪問者
class Visitor {
 public:
  virtual void VisitConcreteComponentA(const ConcreteComponentA *element) const = 0;
  virtual void VisitConcreteComponentB(const ConcreteComponentB *element) const = 0;
};

// 抽象元件
class Component {
 public:
  virtual ~Component() {}
  virtual void Accept(Visitor *visitor) const = 0;
};

// 具體元件A
class ConcreteComponentA : public Component {
 public:
  void Accept(Visitor *visitor) const override {
    visitor->VisitConcreteComponentA(this);
  }
  std::string ExclusiveMethodOfConcreteComponentA() const {
    return "A";
  }
};

// 具體元件B
class ConcreteComponentB : public Component {
 public:
  void Accept(Visitor *visitor) const override {
    visitor->VisitConcreteComponentB(this);
  }
  std::string SpecialMethodOfConcreteComponentB() const {
    return "B";
  }
};

// 具體訪問者
class ConcreteVisitor1 : public Visitor {
 public:
  void VisitConcreteComponentA(const ConcreteComponentA *element) const override {
    std::cout << element->ExclusiveMethodOfConcreteComponentA() << " + ConcreteVisitor1\n";
  }

  void VisitConcreteComponentB(const ConcreteComponentB *element) const override {
    std::cout << element->SpecialMethodOfConcreteComponentB() << " + ConcreteVisitor1\n";
  }
};

// 具體訪問者
class ConcreteVisitor2 : public Visitor {
 public:
  void VisitConcreteComponentA(const ConcreteComponentA *element) const override {
    std::cout << element->ExclusiveMethodOfConcreteComponentA() << " + ConcreteVisitor2\n";
  }
  void VisitConcreteComponentB(const ConcreteComponentB *element) const override {
    std::cout << element->SpecialMethodOfConcreteComponentB() << " + ConcreteVisitor2\n";
  }
};


void ClientCode(std::array<const Component *, 2> components, Visitor *visitor) {
  // ...
  for (const Component *comp : components) {
    comp->Accept(visitor);
  }
  // ...
}

int main() {
  std::array<const Component *, 2> components = {new ConcreteComponentA, new ConcreteComponentB};
  std::cout << "The client code works with all visitors via the base Visitor interface:\n";
  ConcreteVisitor1 *visitor1 = new ConcreteVisitor1;
  ClientCode(components, visitor1);
  std::cout << "\n";
  std::cout << "It allows the same client code to work with different types of visitors:\n";
  ConcreteVisitor2 *visitor2 = new ConcreteVisitor2;
  ClientCode(components, visitor2);

  for (const Component *comp : components) {
    delete comp;
  }
  delete visitor1;
  delete visitor2;

  return 0;
}
# -*- coding: utf-8 -*-

from __future__ import annotations
from abc import ABC, abstractmethod
from typing import List


class Component(ABC):
    """
    """

    @abstractmethod
    def accept(self, visitor: Visitor) -> None:
        pass


class ConcreteComponentA(Component):
    """
    """

    def accept(self, visitor: Visitor) -> None:
        visitor.visit_concrete_component_a(self)

    def exclusive_method_of_concrete_component_a(self) -> str:
        return "A"


class ConcreteComponentB(Component):
    """
    """

    def accept(self, visitor: Visitor):
        visitor.visit_concrete_component_b(self)

    def special_method_of_concrete_component_b(self) -> str:
        return "B"


class Visitor(ABC):
    """
    """

    @abstractmethod
    def visit_concrete_component_a(self, element: ConcreteComponentA) -> None:
        pass

    @abstractmethod
    def visit_concrete_component_b(self, element: ConcreteComponentB) -> None:
        pass


class ConcreteVisitor1(Visitor):
    def visit_concrete_component_a(self, element) -> None:
        print(f"{element.exclusive_method_of_concrete_component_a()} + ConcreteVisitor1")

    def visit_concrete_component_b(self, element) -> None:
        print(f"{element.special_method_of_concrete_component_b()} + ConcreteVisitor1")


class ConcreteVisitor2(Visitor):
    def visit_concrete_component_a(self, element) -> None:
        print(f"{element.exclusive_method_of_concrete_component_a()} + ConcreteVisitor2")

    def visit_concrete_component_b(self, element) -> None:
        print(f"{element.special_method_of_concrete_component_b()} + ConcreteVisitor2")


def client_code(components: List[Component], visitor: Visitor) -> None:
    # ...
    for component in components:
        component.accept(visitor)
    # ...


if __name__ == "__main__":
    components = [ConcreteComponentA(), ConcreteComponentB()]

    print("The client code works with all visitors via the base Visitor interface:")
    visitor1 = ConcreteVisitor1()
    client_code(components, visitor1)

    print("It allows the same client code to work with different types of visitors:")
    visitor2 = ConcreteVisitor2()
    client_code(components, visitor2)

例項

問題描述

同上。

問題解答

同上。

總結

優點

  • 開閉原則。可以引入在不同類物件上執行的新行為,且無需對這些類做出修改。
  • 單一職責原則。可將同一行為的不同版本移到同一個類中。
  • 訪問者物件可以在與各種物件互動時收集一些有用的資訊。當想要遍歷一些複雜的物件結構 (例如物件樹),並在結構中的每個物件上應用訪問者時,這些資訊可能會有所幫助。

缺點

  • 每次在元素層次結構中新增或移除一個類時,都要更新所有的訪問者。
  • 在訪問者同某個元素進行互動時,它們可能沒有訪問元素私有成員變數和方法的必要許可權。

場景

  • 如果你需要對一個複雜物件結構(例如物件樹)中的所有元素執行某些操作,可使用該模式。
  • 當某個行為僅在類層次結構中的一些類中有意義,而在其他類中沒有意義時,可使用該模式。
  • 可使用訪問者模式來清理輔助行為的業務邏輯。

與其他模式的關係

  • 可以將訪問者模式視為命令模式的加強版本,其物件可對不同類的多種物件執行操作。
  • 可以使用訪問者模式對整個組合模式樹執行操作。
  • 可以同時使用訪問者模式迭代器模式來遍歷複雜資料結構,並對其中的元素執行所需操作,即使這些元素所屬的類完全不同。