訪問者模式
定義
訪問者模式(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實現訪問者模式/