VISITOR模式--《敏捷软件开发》读书笔记(三)

考虑一下设计一个可以包含长方形、正方形、圆形以及文字的视图类。因为视图中每种元素的显示方式都不一样,所以我们很容易做出如下的设计:

VISITOR模式--《敏捷软件开发》读书笔记(三)

在这里,我使用了COMPOSITE模式。对于COMPOSITE模式,可以参考我那篇《重读《设计模式》之学习笔记(五)--我对COMPOSITE模式的理解》
现在,我们来分析一下上面的这个设计方案。如果说,由于客户需求的改变,视图中每个元素的显示方法变了,那么我就就要更改每个类的Show方法(当然,客户还有把需求改回去的可能,那样我们还得把Show方法给改回去。虽然这样会使程序员痛苦不堪,但是这种可能还是存在的。我想,做开发的人都会遇到类似的情况。本书中第7章就有这样两句话:“软件开发最重要的事实之一:需求总是在变化。”,“在大多数软件项目中最不稳定的东西就是需求。”)。另外一种情况,客户要求视图可以通过打印机打印,那我们就得给每一个类就要加上一个Print方法。或者,客户要求可以统计视图中每种元素的个数,那么我们还得给类CTView添加一个统计的方法。每次更改,所有使用这些类的代码都要重新编译。
从上面的分析可以看出,我们这个设计是不成功的。在这个时候我们就应该使用VISITOR模式。VISITOR模式的好处就是可以在不改变现有类层次结构的情况下向其中增加新方法。听起来似乎很神奇,下面我就用代码来说明:
首先,创建一个CVisitor的抽象类:

classCVisitor
{
public:
virtual~CVisitor();
virtualvoidVisitRectangle(CTRectangle*pRectangle)=0;
virtualvoidVisitSquare(CTSquare*pSquare)=0;
virtualvoidVisitCircle(CTCircle*pCircle)=0;
virtualvoidVisitText(CTText*pText)=0;
virtualvoidVisitView(CTView*pView)=0;
};

我们要通过该类的子类来访问视图中的各个元素。
下面是重新设计后视图及其各个元素的代码:

classCContext
{
public:
virtual~CContext();
virtualvoidAccept(CVisitor&v)=0;
};


classCTRectangle:publicCContext
{
public:
voidAccept(CVisitor&v){v.VisitRectangle(this);};
};


classCTSquare:publicCContext
{
public:
voidAccept(CVisitor&v){v.VisitSquare(this);};
};


classCTCircle:publicCContext
{
public:
voidAccept(CVisitor&v){v.VisitCircle(this);};
};

classCTText:publicCContext
{
public:
voidAccept(CVisitor&v){v.VisitText(this);};
};


classCTView:publicCContext
{
public:
~CTView();
voidAccept(CVisitor&v);
voidAdd(CContext*pContext);

private:
vector
<CContext*>m_vContext;
};

CTView::
~CTView()
{
while(!m_vContext.empty())
{
CContext
*pContext=(CContext*)m_vContext.back();
m_vContext.pop_back();

deletepContext;
}
}

voidCTView::Accept(CVisitor&v)
{
for(vector<CContext*>::iteratori=m_vContext.begin();i!=m_vContext.end();++i)
{
(
*i)->Accept(v);
}
v.VisitView(
this);
}

voidCTView::Add(CContext*pContext)
{
m_vContext.push_back(pContext);
}

下面,我们为上面的类添加一个显示视图中各个元素并且计算各个元素个数的visitor:

classCShowContextVisitor:publicCVisitor
{
public:
CShowContextVisitor();

voidVisitRectangle(CTRectangle*pRectangle);
voidVisitSquare(CTSquare*pSquare);
voidVisitCircle(CTCircle*pCircle);
voidVisitText(CTText*pText);
voidVisitView(CTView*pView);

private:
intm_iRectangleCount;
intm_iSquareCount;
intm_iCircleCount;
intm_iTextCount;
};

CShowContextVisitor::CShowContextVisitor()
:m_iRectangleCount(
0)
,m_iSquareCount(
0)
,m_iCircleCount(
0)
,m_iTextCount(
0)
{}

//下面以输出一句话来代替具体的显示

voidCShowContextVisitor::VisitRectangle(CTRectangle*pRectangle)
{
cout
<<"ARectangleisShowed!"<<endl;

m_iRectangleCount
++;
}

voidCShowContextVisitor::VisitSquare(CTSquare*pSquare)
{
cout
<<"ASquareisShowed!"<<endl;

m_iSquareCount
++;
}

voidCShowContextVisitor::VisitCircle(CTCircle*pCircle)
{
cout
<<"ACircleisShowed!"<<endl;

m_iCircleCount
++;
}

voidCShowContextVisitor::VisitText(CTText*pText)
{
cout
<<"ATextisShowed!"<<endl;

m_iTextCount
++;
}

voidCShowContextVisitor::VisitView(CTView*pView)
{
cout
<<"AViewisShowed!"<<endl;
cout
<<"Rectanglecount:"<<m_iRectangleCount<<endl;
cout
<<"Squarecount:"<<m_iSquareCount<<endl;
cout
<<"Circlecount:"<<m_iCircleCount<<endl;
cout
<<"Textcount:"<<m_iTextCount<<endl;
}

我们可以用下面的测试函数来验证我们的设计是否正确:

voidTest()
{
CTViewTestView;

CTRectangle
*pRectangle=newCTRectangle;
TestView.Add(pRectangle);

CTSquare
*pSquare=newCTSquare;
TestView.Add(pSquare);

CTCircle
*pCircle=newCTCircle;
TestView.Add(pCircle);

CTText
*pText=newCTText;
TestView.Add(pText);

CShowContextVisitorTestVisitor;
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模式。