C++ 設計模式 - 訪問者模式 Visitor Pattern

想像你是一位探險家,來到了一座神秘的古城。這座城市有著各式各樣的建築:宏偉的宮殿、莊嚴的神廟、繁忙的市集。作為一個訪問者,你想要深入了解每個地方,但每個地方都有其獨特的探索方式。這就像程式世界中的訪問者模式,一種讓你能夠優雅地處理複雜物件結構的設計模式。準備好開始這段奇妙的探索之旅了嗎?

什麼是訪問者模式?

訪問者模式是一種行為設計模式,它允許你在不改變原有物件結構的情況下,定義對這些物件的新操作。這個模式的核心思想是將資料結構和資料操作分離。在這個模式中,訪問者會遍歷不同類型的物件,並對它們進行不同的操作。這樣的好處是可以靈活地新增行為,而不用修改原來的類別。

就像我們的探險家例子,訪問者就像是探險家,而被訪問的元素就像是城市中的各個地點。無論你去到哪裡,都能以適合該地點的方式進行探索,而不需要改變這些地點本身。

訪問者模式在繪圖軟體中的應用

讓我們透過一個繪圖軟體的例子來深入理解訪問者模式。想像我們正在開發一款繪圖軟體,裡面有各式各樣的圖形,如圓形、矩形和三角形等。這些圖形都有各自的繪圖和顯示方式,但現在我們需要新增一個功能:計算每個圖形的面積。

首先我們定義訪問者介面和圖形的基本介面,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 前向宣告
class Circle;
class Rectangle;
class Triangle;

// 訪問者基類
class Visitor {
public
virtual void visit(Circle* circle) = 0;
virtual void visit(Rectangle* rectangle) = 0;
virtual void visit(Triangle* triangle) = 0;
};

class Shape {
public:
virtual void accept(Visitor* visitor) = 0;
virtual ~Shape() {}
};

接著我們實現具體的圖形類,這裡的 CircleRectangleTriangle 類就是我們所說的”不需要修改的類”。這些類代表了我們的基本圖形,它們的主要職責是維護圖形的基本屬性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Circle : public Shape {
public:
Circle(double radius) : radius(radius) {}
void accept(Visitor* visitor) override {
visitor->visit(this);
}
double getRadius() const { return radius; }
private:
double radius;
};

class Rectangle : public Shape {
public:
Rectangle(double width, double height) : width(width), height(height) {}
void accept(Visitor* visitor) override {
visitor->visit(this);
}
double getWidth() const { return width; }
double getHeight() const { return height; }
private:
double width, height;
};

class Triangle : public Shape {
public:
Triangle(double base, double height) : base(base), height(height) {}
void accept(Visitor* visitor) override {
visitor->visit(this);
}
double getBase() const { return base; }
double getHeight() const { return height; }
private:
double base, height;
};

然後我們可以實現具體的訪問者,例如計算面積的訪問者,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 計算面積大小的訪問者
class AreaCalculator : public Visitor {
public:
void visit(Circle* circle) override {
totalArea += 3.14159 * circle->getRadius() * circle->getRadius();
}
void visit(Rectangle* rectangle) override {
totalArea += rectangle->getWidth() * rectangle->getHeight();
}
void visit(Triangle* triangle) override {
totalArea += 0.5 * triangle->getBase() * triangle->getHeight();
}
double getTotalArea() const { return totalArea; }
private:
double totalArea = 0;
};

最後客戶端可以這樣使用來取得所有圖形的面積總合,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main() {
std::vector<Shape*> shapes = {
new Circle(5),
new Rectangle(46),
new Triangle(34)
};
AreaCalculator areaCalculator;
for (auto shape : shapes) {
shape->accept(&areaCalculator);
}
std::cout << "Total area: " << areaCalculator.getTotalArea() << std::endl;

// 釋放記憶體
for (auto shape : shapes) {
delete shape;
}

return 0;
}

執行上述程式碼,我們會得到以下輸出:

1
Total area: 108.54

透過這種方式,我們可以輕鬆地新增新的操作(如計算周長、繪製圖形等),而不需要修改現有的 CircleRectangleTriangle 類。這就是訪問者模式的核心優勢:當我們需要新增新的操作時,我們只需要建立一個新的訪問者類(比如 PerimeterCalculatorShapeDrawer),而不需要改變這些基本圖形類的結構。

這種設計使得我們的圖形結構保持穩定,同時允許我們靈活地新增新的操作。例如,如果我們之後想要新增一個計算所有圖形重心的功能,我們只需要建立一個新的 CenterOfGravityCalculator 訪問者,而不需要修改任何現有的圖形類。

訪問者模式在檔案結構分析中的應用

另一個常見的應用例子就是檔案結構分析。假設你有一個複雜的檔案系統,裡面包含了檔案(File)和資料夾(Folder)。當你想要統計資料夾內所有檔案的大小,或是對特定類型的檔案進行處理時,訪問者模式就能幫上大忙。

我們有一個檔案系統,其中包含兩種類型的元素:FileFolder。我們需要一個訪問者來遍歷這些元素並計算檔案的大小。

定義一個訪問者介面 Visitor,其中包含兩個方法,分別對 FileFolder 類別進行操作,

1
2
3
4
5
6
7
8
9
10
// 前向宣告
class File;
class Folder;

// 訪問者基類
class Visitor {
public:
virtual void visit(File* file) = 0;
virtual void visit(Folder* folder) = 0;
};

定義 FileFolder 類別,它們都會有一個 accept 方法來接受訪問者,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 檔案類
class File {
public:
void accept(Visitor* visitor) {
visitor->visit(this);
}
int getSize() const { return size; }
private:
int size = 100; // 假設每個檔案都有大小
};

// 資料夾類
class Folder {
public:
void accept(Visitor* visitor) {
visitor->visit(this);
}

void addFile(File* file) {
files.push_back(file);
}

const std::vector<File*>& getFiles() const { return files; }
private:
std::vector<File*> files;
};

定義具體的訪問者,這個訪問者用來計算檔案總大小,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 計算大小的訪問者
class SizeVisitor : public Visitor {
public:
void visit(File* file) override {
totalSize += file->getSize();
}

void visit(Folder* folder) override {
for (auto file : folder->getFiles()) {
file->accept(this);
}
}
int getTotalSize() const { return totalSize; }
private:
int totalSize = 0;
};

最後在客戶端中使用訪問者來計算總大小,

1
2
3
4
5
6
7
8
9
10
11
int main() {
File file1, file2;
Folder folder;
folder.addFile(&file1);
folder.addFile(&file2);

SizeVisitor visitor;
folder.accept(&visitor);

std::cout << "Total size: " << visitor.getTotalSize() << std::endl;
}

透過這種方式,我們可以輕鬆地新增新的操作(如搜尋、統計、結構顯示等),而不需要修改現有的 File 和 Folder 類。這就是訪問者模式的核心優勢:當我們需要新增新的操作時,我們只需要建立一個新的訪問者類(比如 FileSearcher、FileCounter 或 StructurePrinter),而不需要改變 File 和 Folder 這些基本類的結構。
這種設計使得我們的檔案系統結構保持穩定,同時允許我們靈活地新增新的操作。例如,如果我們之後想要新增一個統計特定類型檔案數量的功能,我們只需要建立一個新的 FileTypeCounter 訪問者,而不需要修改 File 和 Folder 類。

訪問者模式的優缺點

訪問者模式就像是一把瑞士軍刀,提供了極大的靈活性。它允許我們輕鬆地新增新的操作,而無需修改現有的類結構。這不僅提高了程式碼的可維護性,還有助於遵守開閉原則。此外它還能將相關的操作集中在訪問者中,讓程式碼更加整潔和易於管理。

訪問者模式也有其缺點。它不太適合經常改變結構的系統,因為每當物件結構發生變化,你就必須更新訪問者中的方法。另外如果物件的類型數量很多,訪問者模式會變得難以維護,因為每新增一種類型都需要在訪問者中新增對應的方法。

總結

訪問者模式特別適合在物件結構穩定,但需要經常增加行為的情況下使用。透過將操作與物件分離,我們可以更輕鬆地擴展系統功能,而不會影響原有的結構。不過這個模式也有其適用範圍,過於複雜的結構變化可能會使訪問者模式的維護成本變得很高。因此選擇是否使用訪問者模式時,需要根據具體需求來權衡。