【C++11】 改进我们的设计模式---访问者模式

2021-11-16 14:44:00 浏览数 (2)

1 了解访问者模式

访问者模式的应用场景不多,它可以在不改变类成员的前提下定义作用于这些元素的新的操作,是一种数据元素和数据操作分离的设计模式。

2 访问者模式的使用场景

在《设计模式》书籍中,访问者模式的使用场景主要有以下几个方面:

  • 需要对对象结构中的对象进行分别处理,又不想影响或者改变原有的对象结构。
  • 类结构非常稳定,但是有需要新增新的操作
  • 一个对象结构中有很多类对象且包含不同的接口,又想对这些对象实施一些依赖于具体类的操作。

3 访问者模式的类结构

图1 经典访问者模式类结构

类结构说明如下:

  • Vistor :访问者抽象类,通过VistorConcrete方法决定可以访问的对象;
  • ConcreteVistor:访问者具体类,实现抽象类中的方法,决定访问到该对象类之后实际干什么。
  • Element:被访问者抽象类,定义了支持接受访问得到类,通过Accept方法进行获取。
  • ConcereElementA:实现基类中的方法。

4 经典访问者模式的实现

代码语言:javascript复制
#include <iostream>
#include <memory>
class ConcreteElementA;
class ConcreteElementB;
//访问者类
class Visitor
{
public:
   virtual void VisitorConcreteElementA(ConcreteElementA* pElementA)=0;
   virtual void VisitorConcreteElementB(ConcreteElementB* pElementB)=0;
};
//访问者类具体实现
class ConcreteVisitorA : public Visitor
{
public:
  void VisitorConcreteElementA(ConcreteElementA* pElementA){
      std::cout<<"visitor ConcreteElementA"<<std::endl;
  };
  void VisitorConcreteElementB(ConcreteElementB* pElementB){
      std::cout<<"visitor ConcreteElementB"<<std::endl;
  };
};
//被访问基类
class Element
{
public:
    virtual void Accept(Visitor &pVisitor)=0;
};
//被访问类实例类
class ConcreteElementA : public Element
{
public:
   void Accept(Visitor &pVisitor){
       pVisitor.VisitorConcreteElementA(this);
   };
};
int main(){
    std::unique_ptr<Element> pConEle(new ConcreteElementA());
    ConcreteVisitorA Convis;
    pConEle->Accept(Convis);
    return 0;
}

上面的代码就实现了一个经典的访问者模式类,代码运行结果为:

代码语言:javascript复制
visitor ConcreteElementA

代码发布上线后,如果改动频繁,就会给生产带来很多的风险,尤其是没有经过充分测试、验证的代码,轻则产生业务故障,重则影响整个业务。在《设计模式》一书中,也强调了访问者模式中被访问者应该是一个稳定的继承结构,如果经常发生变更,就需要经常变更代码。如在上面的代码中,如果需要新增一个被访问对象,就需要修改虚基类Visitor的结构。

5 改进后的访问者模式

根据面向对象的原则,在实际开发中应该是依赖于接口,但是不要依赖于接口的实现为原则,上面经典模式的实现就违反了这一个原则。要想解决这一问题,就需要将Visitor定义成一个稳定的接口层。既:不会因为接口的增加而修改。使用C 11的可变参数模板就可以帮助我们解决这一问题。

可变参数模版可以让接口支撑任意个参数,这一特性也有助于我们实现一个稳定的接口层,下面的代码就通过可变参数模板实现一个稳定的接口层。

代码语言:javascript复制
template <typename... Types>
class Visitor;

template <typename T, typename... Types>
class Visitor<T,Types ...>:public Visitor<Types...>{
public:
    using Visitor<Types...>::Visit;
    virtual void Visit(const T&)=0;
}

template <typename T>
class Visitor <T>{
public:
    virtual void Visit(const T&) = 0;
}

上面的代码为每一种类型都定义了一个纯虚函数Visit。下面的代码演示了经过改进后的访问者模式。

代码语言:javascript复制
//定义被访问者基类
class ConcreteA;
class ConcreteB;
class Element{
public:
    typedef Visitor<ConcreteA,ConcreteB> MyVisitor;
    virtual void Accept(MyVisitor&)=0;
};
//具体的被访问者
class ConcreteA:public Element{
public:
    std::string strName;
    void Accept(Element::MyVisitor &v){
        v.Visit(*this);
    }
};

class ConcreteB:public Element{
public:
    std::string strName;
    void Accept(Element::MyVisitor &v){
        v.Visit(*this);
    } 
};
//具体的访问者
class ConcreteVisitor:public Element::MyVisitor{
public:
    void Visit(const ConcreteA &concreteA){
        cout<<"访问者:"<<concreteA.strName<<endl;
    }
    
    void Visit(const ConcreteB &concreteB){
        cout<<"访问者:"<<concreteB.strName<<endl;
    }
};
int main()
{
   ConcreteA vA;
    vA.strName="nameA";
    ConcreteB vB;
    vB.strName="nameB";
    Element *ele = &vA;
    //定义一个访问者
    ConcreteVisitor conVisitor;
    ele->Accept(conVisitor);
    ele = &vB;
    ele->Accept(conVisitor);
    return 0;
}

上述代码运行结果如下:

代码语言:javascript复制
访问者:nameA
访问者:nameB

在上面的代码中,typedef Visitor<ConcreteA,ConcreteB> MyVisitor会自动生成ConcreteA和ConcreteB的Visit虚拟函数。如下所示:

代码语言:javascript复制
struct Visitor<ConcreteA, ConcreteB>
{
    virtual void Visit(const ConcreteA&) = 0;
    virtual void Visit(constConcreteB&) = 0;
};

如果需要新增新的被访问者,只需要在typedef Visitor<ConcreteA,ConcreteB>中增加新的类型即可。如下所示:

代码语言:javascript复制
typedef Visitor<ConcreteA,ConcreteB,ConcreteC,ConcreteD>

新的类型也会重新生成新的访问者接口。

6 总结

从上面的代码示例可知,改进后的Visitor可自动生成虚函数,增加新的被访问者后,不需要修改Visitor访问接口层的代码。和经典的访问者模式实现相比,接口层更加稳定。C 11实现的访问者模式,将之前接口层的变化大部分转移到了被访问者的修改,接口层只需要增加新的类型即可。从维护角度来说,由于变更导致的风险变小了,也更加易于维护。

0 人点赞