訪問者模式
定義
訪問者模式(Visitor):表示一個作用於某對象結構中的各元素的操作。它使你可以在不改變元素類的前提下定義作用於這些元素的新操作。
使用訪問者模式,元素的執行算法可以隨着訪問者改變而改變。主要意圖是將數據結構與數據操作分離。
不過作為比較難理解的設計模式之一,因為它難理解、難實現,應用它會導致代碼的可讀性、可維護性變差,所以,訪問者模式在實際的軟件開發中很少被用到,在沒有特別必要的情況下,訪問者模式是不建議使用的。
優點
1、開閉原則。 你可以引入在不同類對象上執行的新行為, 且無需對這些類做出修改。
2、單一職責原則。 可將同一行為的不同版本移到同一個類中。
3、靈活性更好。
缺點
1、具體元素變更比較困難。每次在元素層次結構中添加或移除一個類時,都要更新所有的訪問者。
2、比較難理解,應用它會導致代碼的可讀性、可維護性變差。
適用范圍
1、對象結構中對象對應的類很少改變,但經常需要在此對象結構上定義新的操作。
2、需要對一個對象結構中的對象進行很多不同的並且不相關的操作,而需要避免讓這些操作"污染"這些對象的類,也不希望在增加新操作時修改這些類。
代碼實現
代碼實現:
type Visitor interface {
VisitConcreteElementA(cea *ConcreteElementA)
VisitConcreteElementB(ceb *ConcreteElementB)
}
type ConcreteVisitor1 struct {
}
func (cea *ConcreteVisitor1) VisitConcreteElementA(concreteElementA *ConcreteElementA) {
fmt.Println("concreteVisitor1 visitConcreteElementA")
}
func (*ConcreteVisitor1) VisitConcreteElementB(concreteElementB *ConcreteElementB) {
fmt.Println("concreteVisitor1 visitConcreteElementB")
}
type ConcreteVisitor2 struct {
}
func (*ConcreteVisitor2) VisitConcreteElementA(concreteElementA *ConcreteElementA) {
fmt.Println("concreteVisitor2 visitConcreteElementA")
}
func (*ConcreteVisitor2) VisitConcreteElementB(concreteElementB *ConcreteElementB) {
fmt.Println("concreteVisitor2 visitConcreteElementB")
}
type Element interface {
Accept(visitor Visitor)
}
type ConcreteElementA struct {
}
func (cea *ConcreteElementA) Accept(visitor Visitor) {
visitor.VisitConcreteElementA(cea)
}
type ConcreteElementB struct {
}
func (ceb *ConcreteElementB) Accept(visitor Visitor) {
visitor.VisitConcreteElementB(ceb)
}
測試代碼:
func TestVisitor(t *testing.T) {
var elements []Element
elements = append(elements, &ConcreteElementA{})
elements = append(elements, &ConcreteElementB{})
for _, item := range elements {
cv1 := &ConcreteVisitor1{}
cv2 := &ConcreteVisitor2{}
item.Accept(cv1)
item.Accept(cv2)
}
}
結構圖:

什么是 Double Dispatch
什么是分派?
分派即 Dispatch,在面向對象編程語言中,我們可以把方法調用理解為一種消息傳遞(Dispatch)。一個對象調用另一個對象的方法,相當於給被調用對象發送一個消息,這個消息包括對象名、方法名、方法參數等信息。
什么是單分派?
單分派,即執行哪個對象的方法,根據對象的運行時類型決定;執行對象的哪個方法,根據方法參數的編譯時類型決定。
什么是雙分派?
雙分派,即執行哪個對象的方法,根據對象的運行時類型來決定;執行對象的哪個方法,根據方法參數的運行時的類型來決定。
具體到編程語言的語法機制,Single Dispatch 和 Double Dispatch 跟多態和函數重載直接相關。所以 go 是不支持雙分派的。
當前主流的面向對象編程語言(比如,Java、C++、C#)都只支持Single Dispatch,不支持Double Dispatch。
使用 java 舉栗子更容易理解:
import java.util.ArrayList;
import java.util.List;
abstract class ResourceFile {
protected String filePath;
public ResourceFile(String filePath) {
this.filePath = filePath;
}
}
class PdfFile extends ResourceFile {
public PdfFile(String filePath) {
super(filePath);
}
}
class PPTFile extends ResourceFile {
public PPTFile(String filePath) {
super(filePath);
}
}
//...PPTFile、WordFile代碼省略...
class Extractor {
public void extract2txt(PdfFile pdfFile) {
System.out.println("Extract PDF.");
}
public void extract2txt(PPTFile ppTFile) {
System.out.println("Extract PPT.");
}
}
public class Test {
public static void main(String[] args) {
Extractor extractor = new Extractor();
List<ResourceFile> resourceFiles = listAllResourceFiles();
for (ResourceFile resourceFile : resourceFiles) {
extractor.extract2txt(resourceFile);
}
}
private static List<ResourceFile> listAllResourceFiles() {
List<ResourceFile> resourceFiles = new ArrayList<>();
//...根據后綴(pdf/ppt/word)由工廠方法創建不同的類對象(PdfFile/PPTFile/WordFile)
resourceFiles.add(new PPTFile("a.ppt"));
resourceFiles.add(new PdfFile("a.pdf"));
return resourceFiles;
}
}
比如這段代碼,就會在extractor.extract2txt(resourceFile);
,代碼會在運行時,根據參數(resourceFile)的實際類型(PdfFile、PPTFile、WordFile),來決定使用extract2txt的三個重載函數中的哪一個。那下面的代碼實現就能正常運行了。
報錯信息
java: 對於extract2txt(ResourceFile), 找不到合適的方法
方法 Extractor.extract2txt(PdfFile)不適用
(參數不匹配; ResourceFile無法轉換為PdfFile)
方法 Extractor.extract2txt(PPTFile)不適用
(參數不匹配; ResourceFile無法轉換為PPTFile)
參考
【文中代碼】https://github.com/boilingfrog/design-pattern-learning/tree/master/訪問者模式
【大話設計模式】https://book.douban.com/subject/2334288/
【極客時間】https://time.geekbang.org/column/intro/100039001
【雙分派-訪問者模式的前世今生】https://www.codenong.com/cs110749395/
【訪問者模式】https://boilingfrog.github.io/2021/11/25/使用go實現訪問者模式/