乱砍设计模式之七
最后更新于:2022-04-01 14:36:57
VISITOR模式 —— 齐天大圣闹天宫
junguo
Visitor模式的中文名称是访问者模式,该模式的目的是提供一个类来操作其它类型中的对象结构中的元素(也就是专门帮助其它类来实现原本属于它的函数)。它使你可以在不改变各元素类的前提下定义作用于这些元素的新操作。是不是不明白这段话的意思?没关系,还是通过例子来理解该模式。我们先来简述一下例子。
呵呵,好不容易想到这么个土的掉渣的例子。别见怪,我实在想不出更好的例子。例子的背景大家应该都非常熟悉,在这儿就不扯淡了。简要描述一下我们要实现的功能。大家都知道在大闹天宫中,有二郎神和孙悟空打斗的情节。他们两个都有七十二变,七十二变在我们的例子里相当于七十二个方法。但他们的变化并不相同,如孙悟空变成庙的时候,尾巴变不掉,会变成一个旗杆;而二郎神没有尾巴。所以这里把他们各自封装。帮它们各自提供一个类。妖怪类Sprite和神仙类God看以下的类图。
好了,有了类图,开始开发。此次要实现的功能主要是帮助这两个类来实现它的七十二个方法。我们知道程序肯定不是一次写完的,每填加几个函数,我们就想进行一下单元测试,看看自己的代码有没有错误。这时候,我们就需要重新编译程序。由于这两个类的方法比较多,这样每填加一个函数,就可能需要把这整个类文件都重新编译一次。这是件耗费时间的事,你不想看到。那么有没有办法帮我们解决该问题呢?有的,就是现在提到的Visitor模式。我们可以把Sprite和God的所有操作提炼成一个一个单独的类,在这些类中完成原本属于它们的方法。怎么做呢?先来看看类图。
在类图中,你可以看到我们提炼了一个新类Visitor,它有两个子类Change1Vistor和Change2Vistor(如果有其它方法的话,我们可以添加新的类)。Visitor就是我们所说的访问者类了,就是要通过它来帮助我们把所有的方法都提炼到单独的类中。而Change1Vistor和Change2Vistor就是我们所要的具体类,用它来帮助我们实现神仙和妖怪的变化。我们说过了它们可能有七十二种变化,那么我们再填加新的变化的时候,就不需要去修改Sprite和God类的内容了。(当然了,如果真填加七十二中变化的话,这代码也够受的,估摸也好不到哪儿去,真有这样的系统,你可能需要去寻找其它方法了。)
我们再看看我们的Sprite和God,它们拥有共同的基类SuperMan,它只有一个虚拟函数 Accept(Visitor &) ,就是通过它来实现对Visitor类的调用了。Sprite和God类各自实现该方法。为了体现Visitor的真正意义,我们给Sprite和God各自添加了成员变量,其实Visitor的目的主要是帮助处理类中的数据成员了。在后面我们将讲述这个问题。好了,还是先来看看具体的代码,所先来看Visitor的代码:
~~~
// Visitor基类
class Visitor
{
public:
//抽象出来针对于Sprite对象的方法
virtual void VisitorSprite(Sprite *p) = 0;
//抽象出来针对God对象的方法
virtual void VisitorGod(God *p) = 0;
protected:
Visitor(){}
};
//针对于SuperMan的第一个操作
class Change1Vistor : public Visitor
{
public:
void VisitorSprite(Sprite *p)
{
cout << "这是妖怪 " << p->GetName() << " 的变化1" << endl;
};
void VisitorGod(God *p)
{
cout << "这是神仙 " << p->GetType() <<" 的变化1" << endl;
};
};
//针对于SuperMan的第一个操作
class Change2Vistor : public Visitor
{
public:
void VisitorSprite(Sprite *p)
{
cout << "这是妖怪的变化2" << endl;
};
void VisitorGod(God *p)
{
cout << "这是神仙的变化2" << endl;
};
};
~~~
这样,当我们有新的操作需要的时候,我们就可以重新生成一个类Change3Visitor,Change4Visitor等等,只要它们都继承于Visitor就可以了。这样你可以产生新的文件,而无需重新编译以前的类文件了。
我们再来看一下SuperMan的实现:
~~~
//超人类
class SuperMan
{
public:
virtual ~SuperMan(){}
//抽象出来的调用方法的接口
virtual void Accept(Visitor &) = 0;
protected:
SuperMan(){}
};
//妖怪类
class Sprite : public SuperMan
{
private:
string m_strName;
public:
Sprite(string strName):m_strName(strName){}
string GetName(){return m_strName;}
void Accept(Visitor &v);
};
//神仙类
class God : public SuperMan
{
private:
string m_strType;
public:
God(string strType):m_strType(strType){}
string GetType(){return m_strType;}
void Accept(Visitor &v);
};
//Sprite类的Accept实现
void Sprite::Accept(Visitor &v)
{
v.VisitorSprite(this);
}
//God类的Accept实现
void God::Accept(Visitor &v)
{
v.VisitorGod(this);
}
~~~
此处,我们需要注意的问题就是Accept的具体实现了。Sprite和God类需要分别调用针对于自己的接口函数。这里我们还应该考虑到一个问题,就是SuperMan的子类应该相对固定,不应该太多的变动。当它增加一个子类的时候,Visitor接口就需要变化,而相应的Visitor的所有子类也需要进行相应的变化。那么这样与该模式的初衷节省编译时间就背道而驰了,可能要花费更多的编译时间。
再来看看它的使用方法:
~~~
int main(int argc, char* argv[])
{
Sprite sp("孙悟空");
God g("天神,不是地府之神");
//变化1
Change1Vistor c1;
sp.Accept(c1);
g.Accept(c1);
//变化2
Change2Vistor c2;
sp.Accept(c2);
g.Accept(c2);
return 0;
}
~~~
我们可以看到Sprite和God对象调用相应函数的方法都可以通过Accept来实现。我们这里的实现比较简单,只是生成了一个Sprite和一个God类,而实际应用中它可能是一个列表,数组或者是一个组合(Composite),不过原理一致。其它方式大不过就是需要遍历所有的元素,并调用该方法。
再简单介绍一下《设计模式》中对该模式提供的例子。在编译器的实现过程中,会将所有源程序组合成一个语法树。该语法树中包括变量,赋值语句,判断语句等内容,这些内容都是一个单独的类,而该些类有一个统一的基类。这些类通过Composite模式组织成一个结构,也就是语法树。
在这样的语法树上,可能有这样一些操作:类型检查,代码优化等等,可能还有牵涉打印等,也就是操作是在不断变化的。而树的内容相对来说是比较固定的。这样的话,使用Visitor就可以把这些操作独立出来。使新的操作不至于影响原有的类。也不会使单个的类变的臃肿。
好了,这就是这次所要说的Visitor模式了。我们可以看到Visitor的初衷是为了节省编译时间也产生的。所以也可以这么说:设计模式是开发经验的总结,学习它的目的也就是能帮入门者更快的达到真正理解面向对象的水平。
参考书目:
1, 设计模式——可复用面向对象软件的基础(Design Patterns ——Elements of Reusable Object-Oriented Software) Erich Gamma 等著 李英军等译 机械工业出版社
2, Head First Design Patterns(影印版)Freeman等著 东南大学出版社
3, 道法自然——面向对象实践指南 王咏武 王咏刚著 电子工业出版社