考慮一下設計一個可以包含長方形、正方形、圓形以及文字的視圖類。因為視圖中每種元素的顯示方式都不一樣,所以我們很容易做出如下的設計:
在這裡,我使用了COMPOSITE模式。對於COMPOSITE模式,可以參考我那篇《重讀《設計模式》之學習筆記(五)--我對COMPOSITE模式的理解》。
現在,我們來分析一下上面的這個設計方案。如果說,由於客戶需求的改變,視圖中每個元素的顯示方法變了,那麼我就就要更改每個類的Show方法(當然,客戶還有把需求改回去的可能,那樣我們還得把Show方法給改回去。雖然這樣會使程式員痛苦不堪,但是這種可能還是存在的。我想,做開發的人都會遇到類似的情況。本書中第7章就有這樣兩句話:“軟體開發最重要的事實之一:需求總是在變化。”,“在大多數軟體項目中最不穩定的東西就是需求。”)。另外一種情況,客戶要求視圖可以通過印表機列印,那我們就得給每一個類就要加上一個Print方法。或者,客戶要求可以統計視圖中每種元素的個數,那麼我們還得給類CTView添加一個統計的方法。每次更改,所有使用這些類的代碼都要重新編譯。
從上面的分析可以看出,我們這個設計是不成功的。在這個時候我們就應該使用VISITOR模式。VISITOR模式的好處就是可以在不改變現有類階層的情況下向其中增加新方法。聽起來似乎很神奇,下面我就用代碼來說明:
首先,建立一個CVisitor的抽象類別:
class CVisitor
{
public:
virtual ~CVisitor();
virtual void VisitRectangle(CTRectangle *pRectangle) = 0;
virtual void VisitSquare(CTSquare *pSquare) = 0;
virtual void VisitCircle(CTCircle *pCircle) = 0;
virtual void VisitText(CTText *pText) = 0;
virtual void VisitView(CTView *pView) = 0;
};
我們要通過該類的子類來訪問視圖中的各個元素。
下面是重新設計後視圖及其各個元素的代碼:
class CContext
{
public:
virtual ~CContext();
virtual void Accept(CVisitor& v) = 0;
};
class CTRectangle : public CContext
{
public:
void Accept(CVisitor& v) { v.VisitRectangle(this); };
};
class CTSquare : public CContext
{
public:
void Accept(CVisitor& v) { v.VisitSquare(this); };
};
class CTCircle : public CContext
{
public:
void Accept(CVisitor& v) { v.VisitCircle(this); };
};
class CTText : public CContext
{
public:
void Accept(CVisitor& v) { v.VisitText(this); };
};
class CTView : public CContext
{
public:
~CTView();
void Accept(CVisitor& v);
void Add(CContext *pContext);
private:
vector<CContext*> m_vContext;
};
CTView::~CTView()
{
while(!m_vContext.empty())
{
CContext *pContext = (CContext*)m_vContext.back();
m_vContext.pop_back();
delete pContext;
}
}
void CTView::Accept(CVisitor& v)
{
for(vector<CContext*>::iterator i = m_vContext.begin(); i != m_vContext.end(); ++i)
{
(*i)->Accept(v);
}
v.VisitView(this);
}
void CTView::Add(CContext* pContext)
{
m_vContext.push_back(pContext);
}
下面,我們為上面的類添加一個顯示視圖中各個元素並且計算各個元素個數的visitor:
class CShowContextVisitor : public CVisitor
{
public:
CShowContextVisitor();
void VisitRectangle(CTRectangle *pRectangle);
void VisitSquare(CTSquare *pSquare);
void VisitCircle(CTCircle *pCircle);
void VisitText(CTText *pText);
void VisitView(CTView *pView);
private:
int m_iRectangleCount;
int m_iSquareCount;
int m_iCircleCount;
int m_iTextCount;
};
CShowContextVisitor::CShowContextVisitor()
: m_iRectangleCount(0)
, m_iSquareCount(0)
, m_iCircleCount(0)
, m_iTextCount(0)
{}
// 下面以輸出一句話來代替具體的顯示
void CShowContextVisitor::VisitRectangle(CTRectangle *pRectangle)
{
cout << "A Rectangle is Showed!" << endl;
m_iRectangleCount++;
}
void CShowContextVisitor::VisitSquare(CTSquare *pSquare)
{
cout << "A Square is Showed!" << endl;
m_iSquareCount++;
}
void CShowContextVisitor::VisitCircle(CTCircle *pCircle)
{
cout << "A Circle is Showed!" << endl;
m_iCircleCount++;
}
void CShowContextVisitor::VisitText(CTText *pText)
{
cout << "A Text is Showed!" << endl;
m_iTextCount++;
}
void CShowContextVisitor::VisitView(CTView *pView)
{
cout << "A View is Showed!" << endl;
cout << "Rectangle count: " << m_iRectangleCount << endl;
cout << "Square count: " << m_iSquareCount << endl;
cout << "Circle count: " << m_iCircleCount << endl;
cout << "Text count: " << m_iTextCount << endl;
}
我們可以用下面的測試函數來驗證我們的設計是否正確:
void Test()
{
CTView TestView;
CTRectangle *pRectangle = new CTRectangle;
TestView.Add(pRectangle);
CTSquare *pSquare = new CTSquare;
TestView.Add(pSquare);
CTCircle *pCircle = new CTCircle;
TestView.Add(pCircle);
CTText *pText = new CTText;
TestView.Add(pText);
CShowContextVisitor TestVisitor;
TestView.Accept(TestVisitor);
}
當然,輸出跟我們期望的一樣:
A Rectangle is Showed!
A Square is Showed!
A Circle is Showed!
A Text is Showed!
A View is Showed!
Rectangle count: 1
Square count: 1
Circle count: 1
Text count: 1
如果客戶提出其他需求,我們只要添加一個相應的visitor就行了,而不用修改我們設計好的類。就算客戶把需求改回去,只要重新使用我們以前寫好的visitor的檔案就行了。
VISISTOR模式最大的好處就是:很容易增加新的操作,而且能把這些相關的操作應該集中在一起,也就是visitor類裡。
但是,VISITOR模式有一個致命的弱點,那就是添加相關的元素類比較困難。比如上面的例子中,如果客戶要求可以在視圖中添加三角形,那麼我們除了要寫一個CTTriangle類以外,還得修改抽象類別CVisitor和它所有的子類。
所以,我們應該在類的階層已經固定而操作需要較多的添加或修改時才選擇使用VISITOR模式。