歐耶!這周Jungle的作業終於做完了!作業是什麼呢?就是完成一個習題冊。Jungle做完之後,得讓家長檢查習題冊並簽字;第二天交到學校,組長得初步檢查作業是否做完、家長是否簽字,然後老師會評閱作業是否正確,並給出評分。
就是這麼一個習題冊,這是經了多少人的手啊!
Jungle——完成習題冊上的題;
Jungle家長——檢查兒子的作業,並在習題冊上簽字;
組長——初步檢查Jungle的習題冊是否完成;
老師——評閱習題冊,給出評分。
同樣一個對象(習題冊),不同的人都去訪問它,並且訪問的方式不同,Jungle是爲了完成作業,Jungle爸爸是爲了簽字,組長是爲了檢查Jungle是否完成,而老師是爲了評分。 每一個人都扮演了訪問者的角色。
什麼?訪問者?
1.訪問者模式簡介
類似於上述的習題冊,軟件設計中也需要這樣的類似於習題冊的對象結構,不同的對象對應不同的處理。設計模式中,訪問者模式就是爲了以不同的方式來操作複雜的對象結構。
訪問者模式是一種較爲複雜的行爲型設計模式,具有訪問者和被訪問元素兩個主要的角色。被訪問的元素常常有不同的類型,不同的訪問者可以對它們提供不同的訪問方式。被訪問元素通常不是單獨存在,而是以集合的形式存在於一個對象結構中,訪問者可以遍歷該對象結構,以逐個訪問其中的每一個元素。
訪問者模式:
表示一個作用於某對象結構中的各個元素的操作。訪問者模式讓用戶可以在不改變各元素的前提下定義作用於這些元素的新操作。
2.訪問者模式結構
訪問者模式的結構相對較複雜,角色有如下幾個:
- Visitor(抽象訪問者):抽象類,聲明瞭訪問對象結構中不同具體元素的方法,由方法名稱可知該方法將訪問對象結構中的某個具體元素;
- ConcreteVisitor(具體訪問者):訪問某個具體元素的訪問者,實現具體的訪問方法;
- Element(抽象元素):抽象類,一般聲明一個accept()的方法,用於接受訪問者的訪問,accept()方法常常以一個抽象訪問者的指針作爲參數;
- ConcreteElement(具體元素):針對具體被訪問的元素,實現accept()方法;
- ObjectStructure(對象結構):元素的集合,提供了遍歷對象結構中所有元素的方法。對象結構存儲了不同類型的元素對象,以供不同的訪問者訪問。
訪問者模式的UML結構圖如下:
從上圖和前述可以看出,訪問者模式中有兩個層次結構:
- 訪問者的層次結構:抽象訪問者和具體訪問者,不同的具體訪問者有不同的訪問方式(visit()方式);
- 被訪問元素的層次結構:抽象元素和具體元素,不同的具體元素有不同的被訪問方式(accept()方式)
正式由於有這兩個層次結構,在增加新的訪問者時,不必修改已有的代碼,通過繼承抽象訪問者即可實現擴展,符合開閉原則,系統擴展性較好。但是在增加新的元素時,既要修改抽象訪問者類(增加訪問新增元素方法的聲明),又要修改具體訪問者(增加新的具體訪問者類),不符合開閉原則。
訪問者模式的示例代碼如下:
#ifndef __DEMO_H__
#define __DEMO_H__
// 抽象訪問者 Visitor
class Visitor
{
public:
virtual void visit(ConcreteElementA*) = 0;
virtual void visit(ConcreteElementB*) = 0;
};
// 具體訪問者 ConcreteVisitor
class ConcreteVisitor :public Visitor
{
public:
// 實現一種針對特定元素的訪問操作
void visit(ConcreteElementA*){
// 元素A的訪問操作代碼
}
void visit(ConcreteElementB*){
// 元素B的訪問操作代碼
}
};
// 抽象元素
class Element
{
public:
// 聲明抽象方法,以一個抽象訪問者的指針作爲函數參數
virtual void accept(Visitor*) = 0;
};
// 具體元素
class ConcreteElement :public Element
{
public:
void accept(Visitor* visitor){
visitor->visit(this);
}
};
// 對象結構
class ObjectStructure
{
public:
// 提供接口接受訪問者訪問
void accept(Visitor* visitor){
// 遍歷訪問對象結構中的元素
for (){
elementList[i]->accept(visitor);
}
}
void addElement(){}
void removeElement(){}
private:
lsit<Element*>elementList;
};
#endif
3.訪問者模式代碼實例
Jungle作爲一名顧客,去超市購物,加入購物車的商品包括兩種蘋果和兩本書,結賬時收銀員需要計算各個商品的的價格。本例Jungle採用訪問者模式來模擬該過程。
本例中,客戶Jungle和收銀員都會去訪問商品,但關心的地方不同:Jungle關心的是蘋果和書的單價、品牌等,收銀員關注的是商品的價格。因此,客戶Customer和收銀員Cashier是具體訪問者,而蘋果Apple和書Book是具體被訪問元素;而購物車則是對象結構。本例的UML圖如下:
3.1.元素類
3.1.1.抽象元素
// 抽象元素
class Element
{
public:
Element(){};
virtual void accept(Visitor*) = 0;
void setPrice(int iPrice){
this->price = iPrice;
}
int getPrice(){
return this->price;
}
void setNum(int iNum){
this->num = iNum;
}
int getNum(){
return num;
}
void setName(string iName){
this->name = iName;
}
string getName(){
return this->name;
}
private:
int price;
int num;
string name;
};
3.1.2.具體元素Apple
// 具體元素:Apple
class Apple :public Element
{
public:
Apple();
Apple(string name, int price);
void accept(Visitor*);
};
實現:
Apple::Apple(){
setPrice(0);
setNum(0);
setName("");
}
Apple::Apple(string name, int price){
setPrice(price);
setNum(0);
setName(name);
}
void Apple::accept(Visitor* visitor){
visitor->visit(this);
}
3.1.3.具體元素Book
// 具體元素:Book
class Book :public Element
{
public:
Book();
Book(string name, int price);
void accept(Visitor*);
};
實現:
Book::Book(){
setPrice(0);
setNum(0);
setName("");
}
Book::Book(string iName, int iPrice){
setPrice(iPrice);
setNum(0);
setName(iName);
}
void Book::accept(Visitor* visitor){
visitor->visit(this);
}
3.2.訪問者
3.2.1.抽象訪問者
// 抽象訪問者
class Visitor
{
public:
Visitor(){};
// 聲明一組訪問方法
virtual void visit(Apple*) = 0;
virtual void visit(Book*) = 0;
};
3.2.2.具體訪問者Customer
// 具體訪問者:顧客
class Customer :public Visitor
{
public:
Customer();
Customer(string iName);
void setNum(Apple*, int);
void setNum(Book*, int);
void visit(Apple* apple);
void visit(Book* book);
private:
string name;
};
實現:
Customer::Customer(){
this->name = "";
}
Customer::Customer(string iName){
this->name = iName;
}
void Customer::setNum(Apple* apple, int iNum){
apple->setNum(iNum);
}
void Customer::setNum(Book* book, int iNum){
book->setNum(iNum);
}
void Customer::visit(Apple* apple){
int price = apple->getPrice();
printf(" %s \t單價: \t%d 元/kg\n", apple->getName().c_str(), apple->getPrice());
}
void Customer::visit(Book* book){
int price = book->getPrice();
string name = book->getName();
printf(" 《%s》\t單價: \t%d 元/本\n", book->getName().c_str(), book->getPrice());
}
3.2.3.具體訪問者Cashier
class Cashier :public Visitor
{
public:
Cashier();
void visit(Apple* apple);
void visit(Book* book);
};
實現:
Cashier::Cashier(){
}
void Cashier::visit(Apple* apple){
string name = apple->getName();
int price = apple->getPrice();
int num = apple->getNum();
int total = price*num;
printf(" %s 總價: %d 元\n", name.c_str(), total);
}
void Cashier::visit(Book* book){
int price = book->getPrice();
string name = book->getName();
int num = book->getNum();
int total = price*num;
printf(" 《%s》 總價: %d 元\n", name.c_str(), total);
}
3.3.購物車ShoppingCart
class ShoppingCart
{
public:
ShoppingCart(){}
void addElement(Element* element){
printf(" 商品名:%s, \t數量:%d, \t加入購物車成功!\n", element->getName().c_str(), element->getNum());
elementList.push_back(element);
}
void accept(Visitor* visitor){
for (int i = 0; i < elementList.size(); i++){
elementList[i]->accept(visitor);
}
}
private:
vector<Element*>elementList;
};
3.4.客戶端代碼示例及結果
#include "Element.h"
#include "Visitor.h"
#include "ShoppingCart.h"
#include <Windows.h>
int main()
{
Apple *apple1 = new Apple("紅富士蘋果", 7);
Apple *apple2 = new Apple("花牛蘋果", 5);
Book *book1 = new Book("紅樓夢", 129);
Book *book2 = new Book("終結者", 49);
Cashier* cashier = new Cashier();
Customer* jungle = new Customer("Jungle");
jungle->setNum(apple1, 2);
jungle->setNum(apple2, 4);
jungle->setNum(book1, 1);
jungle->setNum(book2, 3);
ShoppingCart* shoppingCart = new ShoppingCart();
shoppingCart->addElement(apple1);
shoppingCart->addElement(apple2);
shoppingCart->addElement(book1);
shoppingCart->addElement(book2);
printf("\n\n");
shoppingCart->accept(jungle);
printf("\n\n");
shoppingCart->accept(cashier);
printf("\n\n");
system("pause");
return 0;
}
上述代碼運行結果如下:
上述代碼資源見https://github.com/FengJungle/DesignPattern
4.總結
訪問者模式的結構相對較複雜,在實際應用中使用頻率較低。如果系統中存在一個複雜的對象結構,且不同的訪問者對其具有不同的操作,那麼可以考慮使用訪問者模式。訪問者模式的特點總結如下:
優點:
- 增加新的訪問者很方便,即增加一個新的具體訪問者類,定義新的訪問方式,無需修改原有代碼,符合開閉原則;
- 被訪問元素集中在一個對象結構中,類的職責更清晰,利於對象結構中元素對象的複用;
缺點:
- 增加新的元素類很困難,增加新的元素時,在抽象訪問者類中需要增加一個對新增的元素方法的聲明,即要修改抽象訪問者代碼;此外還要增加新的具體訪問者以實現對新增元素的訪問,不符合開閉原則;
- 破壞了對象的封裝性,訪問者模式要求訪問者對象訪問並調用每一個元素對象的操作,那麼元素對象必須暴露自己的內部操作和狀態,否則訪問者無法訪問。