想像你是一位探險家,來到了一座神秘的古城。這座城市有著各式各樣的建築:宏偉的宮殿、莊嚴的神廟、繁忙的市集。作為一個訪問者,你想要深入了解每個地方,但每個地方都有其獨特的探索方式。這就像程式世界中的訪問者模式,一種讓你能夠優雅地處理複雜物件結構的設計模式。準備好開始這段奇妙的探索之旅了嗎?
什麼是訪問者模式?
訪問者模式是一種行為設計模式,它允許你在不改變原有物件結構的情況下,定義對這些物件的新操作。這個模式的核心思想是將資料結構和資料操作分離。在這個模式中,訪問者會遍歷不同類型的物件,並對它們進行不同的操作。這樣的好處是可以靈活地新增行為,而不用修改原來的類別。
就像我們的探險家例子,訪問者就像是探險家,而被訪問的元素就像是城市中的各個地點。無論你去到哪裡,都能以適合該地點的方式進行探索,而不需要改變這些地點本身。
訪問者模式在繪圖軟體中的應用
讓我們透過一個繪圖軟體的例子來深入理解訪問者模式。想像我們正在開發一款繪圖軟體,裡面有各式各樣的圖形,如圓形、矩形和三角形等。這些圖形都有各自的繪圖和顯示方式,但現在我們需要新增一個功能:計算每個圖形的面積。
首先我們定義訪問者介面和圖形的基本介面,
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() {} };
|
接著我們實現具體的圖形類,這裡的 Circle
、Rectangle
和 Triangle
類就是我們所說的”不需要修改的類”。這些類代表了我們的基本圖形,它們的主要職責是維護圖形的基本屬性。
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(4, 6), new Triangle(3, 4) }; 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; }
|
執行上述程式碼,我們會得到以下輸出:
透過這種方式,我們可以輕鬆地新增新的操作(如計算周長、繪製圖形等),而不需要修改現有的 Circle
、Rectangle
和 Triangle
類。這就是訪問者模式的核心優勢:當我們需要新增新的操作時,我們只需要建立一個新的訪問者類(比如 PerimeterCalculator
或 ShapeDrawer
),而不需要改變這些基本圖形類的結構。
這種設計使得我們的圖形結構保持穩定,同時允許我們靈活地新增新的操作。例如,如果我們之後想要新增一個計算所有圖形重心的功能,我們只需要建立一個新的 CenterOfGravityCalculator
訪問者,而不需要修改任何現有的圖形類。
訪問者模式在檔案結構分析中的應用
另一個常見的應用例子就是檔案結構分析。假設你有一個複雜的檔案系統,裡面包含了檔案(File)和資料夾(Folder)。當你想要統計資料夾內所有檔案的大小,或是對特定類型的檔案進行處理時,訪問者模式就能幫上大忙。
我們有一個檔案系統,其中包含兩種類型的元素:File
和 Folder
。我們需要一個訪問者來遍歷這些元素並計算檔案的大小。
定義一個訪問者介面 Visitor
,其中包含兩個方法,分別對 File
和 Folder
類別進行操作,
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; };
|
定義 File
和 Folder
類別,它們都會有一個 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 類。
訪問者模式的優缺點
訪問者模式就像是一把瑞士軍刀,提供了極大的靈活性。它允許我們輕鬆地新增新的操作,而無需修改現有的類結構。這不僅提高了程式碼的可維護性,還有助於遵守開閉原則。此外它還能將相關的操作集中在訪問者中,讓程式碼更加整潔和易於管理。
訪問者模式也有其缺點。它不太適合經常改變結構的系統,因為每當物件結構發生變化,你就必須更新訪問者中的方法。另外如果物件的類型數量很多,訪問者模式會變得難以維護,因為每新增一種類型都需要在訪問者中新增對應的方法。
總結
訪問者模式特別適合在物件結構穩定,但需要經常增加行為的情況下使用。透過將操作與物件分離,我們可以更輕鬆地擴展系統功能,而不會影響原有的結構。不過這個模式也有其適用範圍,過於複雜的結構變化可能會使訪問者模式的維護成本變得很高。因此選擇是否使用訪問者模式時,需要根據具體需求來權衡。