github private链接访问_Hands-On Design Patterns With C++(十八)访问者模式与多分派(上)...

2b9cdae1fa9d87292f7817a7a883b389.png

目录:

trick:Hands-On Design Patterns With C++(零)前言​zhuanlan.zhihu.com
17443b49fa04f8ba4f46de6cc1e70abf.png

本文概要:本文是访问者模式第一篇,将对基本的访问者模式及对复杂对象的应用进行讨论,下一篇文章将会着重讲解现代C++中访问者模式的应用。

本文代码链接:

https://github.com/PacktPublishing/Hands-On-Design-Patterns-with-CPP/tree/master/Chapter18​github.com

访问者模式与多分派

访问者模式是另一种经典的面向对象设计模式,它也是在《设计模式-可复用面向对象软件的基础》一书中介绍的设计模式之一。在面向对象编程兴起的黄金时代,它是最受欢迎的模式之一,原因是它使大型类层次结构更易于维护。 近年来,由于大型复杂的层次结构不是很普遍,所以C++中对访问者模式的使用有所减少,并且访问者模式是一个相当复杂的实现模式。 泛型编程(尤其是C++ 11和C++ 14中添加的新功能)使实现和维护访问者更加容易,其中有很多有意思的内容。

本章将讨论以下问题

  • 什么是访问者模式
  • 如何使用C++实现访问者模式
  • 泛型编程如何简化访问者类
  • 使用访问者模式组合对象
  • 编译期的访问者模式与反射

访问者模式

什么是访问者模式

访问者模式是一种将算法与对象结构分离的模式,对象结构是算法的数据。通过访问者模式我们可以向类层次结构中添加新操作而无需修改类本身。访问者模式遵循开闭原则(对扩展开放,对修改关闭)。一旦类向其客户端提供接口,该接口就应该保持稳定,不应为维护软件或继续开发而修改类。同时,应该对扩展开放:可以添加新功能以满足新需求。当然实际项目中很难严格的遵循其规则,但是我们一年更改尽量遵守。

访问者模式允许我们向类或整个类层次结构在添加功能的同时无需修改类。在处理公共API时,访问者模式起了作用:使用API的用户可以通过其他操作扩展它,而无需修改源代码。我们用示例来说明此问题:

class Base {
    virtual void f() = 0;
};
class Derived1 : public Base {
    void f() override;
};
class Derived2 : public Base {
    void f() override;
};

如果我们通过指向b基类的指针调用b-> f()虚拟函数,则根据对象的实际类型,将调用分派到Derived1::f()Derived2::f()。 这是单个调度,实际调用的函数由对象类型确定。现在,我们假设f()函数也带有一个参数,并且它也是指向基类的指针:

class Base {
    // 传入基类指针
    virtual void f(Base* p) = 0;
};
class Derived1 : public Base {
    void f(Base* p) override;
};
class Derived2 : public Base {
    void f(Base* p) override;
};

*p对象的实际类型也是派生类之一。 现在,b->f(p)调用可以具有四个不同的版本(Derived1Derived2分别输入为Derived1*Derived2*,四种版本),四个版本的内部实现分别不同是合理的。 这是双重调度:最终运行的代码由两个独立的因素决定。 虚函数没有提供直接实现双重调度的方法,但是访问者模式恰好做到了。(后面几段举例了使用访问者模式可解决的场景,比如序列化、反序列化操作等,不同的序列化反序列化方式可以变为多个访问者来解决,不再翻译)

下面我们从基本访问者模式开始进行讲解吧。

C++实现的基本访问者模式

我们通过一个例子讲解访问者模式,宠物基类为Pet,两个继承类为Cat与Dog:

class Pet {
public:
    virtual ~Pet() {}
    Pet(const std::string& color) : color_(color) {}
    const std::string& color() const { return color_; }
    virtual void accept(PetVisitor& v) = 0;
private:
    std::string color_;
};

class Cat : public Pet {
    public:
    Cat(const std::string& color) : Pet(color) {}
    void accept(PetVisitor& v) override { v.visit(this); }
};

class Dog : public Pet {
    public:
    Dog(const std::string& color) : Pet(color) {}
    void accept(PetVisitor& v) override { v.visit(this); }
};

现在,我们要在类中添加一些操作,比如喂宠物和玩宠物,实现方式取决与宠物实际的类型。如果基类中声明虚函数,必须将实现添加到每个类中,这样做费时费力(比如上述两个操作喂/玩宠物,需要在基类中添加两个虚接口)。但是通过访问者模式就可以解决次问题,首先我们创建一个PetVisitor类:

class Cat;
class Dog;
class PetVisitor {
public:
    virtual void visit(Cat* c) = 0;
    virtual void visit(Dog* d) = 0;
};

我们必须前向声明Pet层次结构类,因为必须在具体的Pet类之前声明PetVisitor。 现在,我们需要使Pet层次结构可访问,我们需要在基类中增加虚进口,但无论以后要添加多少操作,只需修改一次虚接口。 我们需要添加一个虚函数来接受访问者模式到每个可以访问的类:

class Pet {
public:
    virtual void accept(PetVisitor& v) = 0;
    .....
};
class Cat : public Pet {
public:
    void accept(PetVisitor& v) override { v.visit(this); }
    .....
};
class Dog : public Pet {
public:
    void accept(PetVisitor& v) override { v.visit(this); }
    .....
};

现在我们的Pet层次结构是可访问的,并且我们有了一个抽象的PetVisitor类。下面我们要对抽象类进行实现。 (请注意,到目前为止,我们所做的任何事情都取决于要添加的操作;我们已经创建了必须实施一次的访问基础结构)。 通过实现派生自PetVisitor的具体访问者类来添加操作:

class FeedingVisitor : public PetVisitor {
public:
    void visit(Cat* c) override { std::cout << "Feed tuna to the " << c->color() << " cat" << std::endl; }
    void visit(Dog* d) override { std::cout << "Feed steak to the " << d->color() << " dog" << std::endl; }
};

class PlayingVisitor : public PetVisitor {
public:
    void visit(Cat* c) override { std::cout << "Play with feather with the " << c->color() << " cat" << std::endl; }
    void visit(Dog* d) override { std::cout << "Play fetch with the " << d->color() << " dog" << std::endl; }
};

根据accept()传入的访问者类型,我们就可以实现其需要的操作:

Cat c("orange");
FeedingVisitor fv;
c.accept(fv); // Feed tuna to the orange cat

我们还可以按如下方式多态进行调用:

std::unique_ptr<Pet> p(new Cat("orange")); // 基类指针
.....
FeedingVisitor fv;
p->accept(fv);

此外,访问者也可以多态使用:

std::unique_ptr<Pet> p(new Cat("orange"));
.....
std::unique_ptr<PetVisitor> v(new FeedingVisitor); // 多态使用Visitor
.....
p->accept(*v);

我们可以通过一个dispatch()函数将PetPerVisitor结合起来:

void dispatch(Pet* p, PetVisitor* v) { p->accept(*v); } // 入参是Pet及PetVisitor
Pet* p = .....; // 多态Pet
PetVisitor* v = .....; // 多态Visitor
dispatch(p, v); // Double dispatch

上面就是C++实现的基本访问者模式的示例,它展示了访问者模式的两方面功能:(1) 在不对类本身实现更改的情况下添加新的功能 (2) 通过调用accept()方法实现了双重调度。

我们同时也发现了一个缺点,对于基类访问者,如果增加一个纯虚新方法,那其下所有派生访问者无论是否需要此方法,都需要进行更新。后面有一节我们将讨论缓解此问题的一种实现方式。

访问者归纳及其限制

我们改造Petaccept()函数定义,Pet类中通过vector跟踪其子对象,同时增加add_child成员函数向vector中增加对象,最后对accept方法进行修改:

class Pet {
public:
    ....
    void add_child(Pet* p) { children_.push_back(p); }
    virtual void accept(PetVisitor& v, Pet* p = nullptr) = 0;
private:
    std::string color_;
    std::vector<Pet*> children_; // 更改
};

子类实现如下:

class Cat : public Pet {
public:
    Cat(const std::string& color) : Pet(color) {}
    void accept(PetVisitor& v, Pet* p = nullptr) override {
        v.visit(this, p);
    }
};

class Dog : public Pet {
public:
    Dog(const std::string& color) : Pet(color) {}
    void accept(PetVisitor& v, Pet* p = nullptr) override {
        v.visit(this, p);
    }
};

下面我们对访问者基类进行更改,访问者现在要支持add_child,所以访问者基类PetVisitor需要接受两个参数:

class PetVisitor {
public:
    // 增加第二参数Pet*
    virtual void visit(Cat* c, Pet* p) = 0;
    virtual void visit(Dog* d, Pet* p) = 0;
};

我们实现BirthVisitor表示增加Pet操作:

class BirthVisitor : public PetVisitor {
    public:
    void visit(Cat* c, Pet* p) override { 
        // assert保证p是Cat类型
        assert(dynamic_cast<Cat*>(p)); 
        c->add_child(p);
    }
    void visit(Dog* d, Pet* p) override { 
        // assert保证p是Dog类型
        assert(dynamic_cast<Dog*>(p)); 
        d->add_child(p); 
    }
};

上述代码我们通过assert保证visit函数的第二参数Pet* p与第一参数类型一致。现在的访问者使用方式如下:

Pet *parent(new Cat("father")); // A cat;
BirthVisitor bv;
Pet *child(new Cat("child"));
parent->accept(bv, child);

下面我们实现打印Pet Family的访问者:

class FamilyTreeVisitor : public PetVisitor {
public:
    void visit(Cat* c, Pet*) override {
        std::cout << "Kittens: "; 
        for (auto k : c->children_) {
            std::cout << k->color() << " ";
        }
        std::cout << std::endl;
    }
    void visit(Dog* d, Pet*) override {
        std::cout << "Puppies: "; 
        for (auto p : d->children_) {
            std::cout << p->color() << " ";
        }
        std::cout << std::endl;
    }
};

这时候按照BirthVisitor的调用方式将会编译出错,原因是FamilyTreeVisitor访问了Pet的私有成员变量。这时候我们只能在Pet类中将FamilyTreeVisitor声明为友元,声明为友元后可以正常使用FamilyTreeVisitor

class Pet {
    ......
    friend class FamilyTreeVisitor;
};
......
Pet *parent(new Cat("father")); // A cat;
FamilyTreeVisitor fv;
Pet *child(new Cat("child"));
parent->accept(fv, child); // 声明友元后此行不会报错

上述BirthVisitorFamilyTreeVisitor返回值都是void,下面的例子将改造FamilyTreeVisitor,让它返回family中child的个数,我们在派生类中增加私有成员变量:

class FamilyTreeVisitor : public PetVisitor {
public:
    FamilyTreeVisitor() : child_count_(0) {}

    void visit(Cat* c, Pet*) override {
        visit_impl(c, "Kittens: ");
    }
    void visit(Dog* d, Pet*) override {
        visit_impl(d, "Puppies: ");
    }

    void reset() { child_count_ = 0; } // 重置child_count_
    size_t child_count() const { return child_count_; } // 返回child_count_
private:
    template <typename T>
    void visit_impl(T* t, const char* s) {
        std::cout << s;
        for (auto p : t->children_) {
            std::cout << p->color() << " ";
            ++child_count_;
        }
        std::cout << std::endl;
    }

    size_t child_count_; // 私有成员变量
};

注意:上述代码计算child_count_的方式是有问题的,此时单一FamilyTreeVisitor计算的是调用它的对象宠物个数的总和,如下代码:

std::unique_ptr<Pet> c(new Cat("orange"));
std::unique_ptr<Pet> d(new Dog("brown"));

BirthVisitor bv;
Cat c1("tabby"), c2("calico");
Dog d1("black");
dispatch(*c, bv, &c1); // cat增加c1
dispatch(*c, bv, &c2); // cat增加c2
dispatch(*d, bv, &d1); // dog增加d1

FamilyTreeVisitor tv;
d->accept(tv); 
std::cout << tv.child_count() << " children" << std::endl; // 返回1
c->accept(tv);
std::cout << tv.child_count() << " children" << std::endl; // 返回3

所以如果想分别统计Cat与Dog的个数,需要使用两个FamilyTreeVisitor对象分别传给Cat与Dog。下一节我们将讲解访问者模式对复杂对象的处理,包括复杂对象的序列化/反序列化问题!

访问复杂对象

上一节中我们回顾了基本的访问者模式,本节我们来访问复杂对象结构,我们将考虑复杂对象引起的问题,同时讨论有效的序列化/反序列化解决方案。

访问复合对象

访问复杂对象的难点在于访问对象本身时,我们通常不知道如何处理每个组件或其包含对象的所有细节。而访问者仅仅是对当前对象的处理,对于复合对象,正确做法是通过委托的方式将Visitor委托给其内部的每个对象:

举个例子,我们创建一个容器Shelter类,它包含任意类型的宠物:

class Shelter {
public:
    void add(Pet* p) { // 增加宠物
        pets_.emplace_back(p);
    }
    void accept(PetVisitor& v) { // 对每个宠物执行访问者的方法
        for (auto& p : pets_) {
            p->accept(v);
        }
    }
private:
    std::vector<std::unique_ptr<Pet>> pets_; // 宠物集合
};

关注Shelteraccept方法,我们并没有在它的内部直接调用Visitor,而是委托给vector中的每个对象。

复合对象又几个较小的对象组成,我们必须对每一个小对象进行访问,比如如下的Family类:

class Family {
public:
    Family(const char* cat_color, const char* dog_color) :
            cat_(cat_color), dog_(dog_color)
    {}
    void accept(PetVisitor& v) {
        cat_.accept(v);
        dog_.accept(v);
    }
private:
    Cat cat_;
    Dog dog_;
};

Family类包含猫和狗两种宠物,同样,accept方法将Visitor分别委托给Cat与Dog进行处理。下面我们就要进入序列化/反序列化问题的讨论!

序列化与反序列化

为了展示使用访问者模式的类层次结构的序列化/反序列化,我们使用一个更加复杂的类结构进行示例,考虑一下二维几何对象的层次结构,示例中几何对象基类Geometry包含三个派生类:点(Point),圆(Circle),线('Line')

class Geometry {
public:
    virtual ~Geometry() {}
    virtual void accept(Visitor& v) = 0;
};

class Point : public Geometry {
public:
    Point() = default;
    Point(double x, double y) : x_(x), y_(y) {}
    void accept(Visitor& v) override {
        v.visit(x_);
        v.visit(y_);
    }
private:
    double x_;
    double y_;
};

class Circle : public Geometry {
public:
    Circle() = default;
    Circle(Point c, double r) : c_(c), r_(r) {}
    void accept(Visitor& v) override {
        v.visit(c_);
        v.visit(r_);
    }
private:
    Point c_;
    double r_;
};

class Line : public Geometry {
public:
    Line() = default;
    Line(Point p1, Point p2) : p1_(p1), p2_(p2) {}
    void accept(Visitor& v) override {
        v.visit(p1_);
        v.visit(p2_);
    }
private:
    Point p1_;
    Point p2_;
};

所有对象都是Geometry基类派生,每个派生类属性都更复杂。 Point对象由两个double组成、Circle由一个Point对象和一个double类型的半径组成、直线由两个Point对象定义。所以我们的Visitor基类必须包含上述所有类型:

class Visitor {
public:
    virtual void visit(double& x) = 0;
    virtual void visit(Point& p) = 0;
    virtual void visit(Circle& c) = 0;
    virtual void visit(Line& l) = 0;
};

double是基本属性,我们没有办法为其单独设置一个accept方法,但是可以通过派生类的accept方法将double类型传递给Visitor。所以我们的Geometry类及其派生类的accept方法实现如下:

class Geometry {
public:
    virtual ~Geometry() {}
    virtual void accept(Visitor& v) = 0;
};
void Point::accept(Visitor& v) {
    v.visit(x_); // double
    v.visit(y_); // double
}
void Circle::accept(Visitor& v) {
    v.visit(c_); // Point
    v.visit(r_); // double
}
void Point::accept(Visitor& v) {
    v.visit(p1_); // Point
    v.visit(p2_); // Point
}

下面是序列化为字符串的Visitor:

class StringSerializeVisitor : public Visitor {
public:
    void visit(double& x) override { S << x << " "; }
    void visit(Point& p) override { p.accept(*this); } // 使用
    void visit(Circle& c) override { c.accept(*this); }
    void visit(Line& l) override { l.accept(*this); }
    std::string str() const { return S.str(); }
private:
    std::stringstream S;
};

上述visit除了方法除了入参为double&的函数外都直接调用传入对象的accept方法,是因为最后其他对象都会转化到double实现的方法中来。比如Circle::accept分别调用了Point::accept和double,而Point::accept调用了两次double。

其使用方法如下:

Line l(Point(1, 2), Point(5, 2));
Circle c(Point(1, 2), 3);
StringSerializeVisitor serializer;
serializer.visit(l); // visitor给line使用
serializer.visit(c); // visitor给circle使用
std::string s(serializer.str());

下面是反序列化操作,输入序列化后的字符串反解出对象:

class StringDeserializeVisitor : public Visitor {
public:
    StringDeserializeVisitor(const std::string& s) { S.str(s); }
    void visit(double& x) override { S >> x; }
    void visit(Point& p) override { p.accept(*this); }
    void visit(Circle& c) override { c.accept(*this); }
    void visit(Line& l) override { l.accept(*this); }

private:
    std::stringstream S;
};

其反序列化实现如下:

Line l1;
Circle c1;
StringDeserializeVisitor deserializer(s); // String from serializer
deserializer.visit(l1); // 恢复 Line l
deserializer.visit(c1); // 恢复 Circle c

注意,Demo中序列化过程我们是先存Line再存Circle,反序列化过程我们也要先恢复Line再恢复Circle,否则会出错。

实际情况中,我们在反序列化时并不知道对象类型是什么,这些对象存储在可访问的容器中,类似于前面示例中的Shelter,它必须确定对象序列化和反序列化的顺序。 例如下面这个类Intersection,该类存储两个几何图形交集的几何图形:

class Intersection : public Geometry {
public:
    Intersection() = default;
    Intersection(Geometry *g1, Geometry *g2) : g1_(g1), g2_(g2) {}

    void accept(Visitor &v) override {
        Geometry::type_tag tag;
        if (g1_) tag = g1_->tag();
        v.visit(tag);
        if (!g1_) g1_.reset(Visitor::make_geometry(tag));
        g1_->accept(v);
        if (g2_) tag = g2_->tag();
        v.visit(tag);
        if (!g2_) g2_.reset(Visitor::make_geometry(tag));
        g2_->accept(v);
    }

    type_tag tag() const override { return INTERSECTION; }

private:
    std::unique_ptr<Geometry> g1_; // 图形g1
    std::unique_ptr<Geometry> g2_; // 图形g2
};

我们仍然将visitor委托给复合对象g1_g2_,但是我们无法直接调用accept,因为我们不知道* g1_* g2_的类型且其可能为空。所以我们需要在Visitor中添加两个方法,一个是获取tag来表明对象类型,一个是当对象为空时创建新对象的方法:

class Geometry {
public:
    // 两个虚函数保持不变
    virtual ~Geometry() {}
    virtual void accept(Visitor& v) = 0;
    enum type_tag { POINT = 100, CIRCLE, LINE, INTERSECTION }; // 增加type_tag枚举
    virtual type_tag tag() const = 0; // 基类添加返回tag的纯虚方法
};

class Visitor {
public:
    static Geometry* make_geometry(Geometry::type_tag tag); // visitor增加创建新对象的方法(静态方法,基类实现)
    virtual void visit(Geometry::type_tag& tag) = 0; // 新增访问tag的方法,将类型值进行序列化(纯虚,需要派生类实现)
    ...... 
    virtual void visit(Intersection& l) = 0;
};

class StringSerializeVisitor : public Visitor {
public:
    void visit(Geometry::type_tag& tag) override { S << size_t(tag) << " "; } // 派生类实现visit(type_tag)方法,将自己的类型值进行序列化
    ......
}

class Point : public Geometry {
public:
    .....
    type_tag tag() const override { return POINT; } // 每个派生类要实现tag方法表明自己的类型
};

Visitor创建新对象方法实现如下:

Geometry* Visitor::make_geometry(Geometry::type_tag tag) {
    switch (tag) {
        case Geometry::POINT: return new Point;
        case Geometry::CIRCLE: return new Circle;
        case Geometry::LINE: return new Line;
        case Geometry::INTERSECTION: return new Intersection;
    }
    return NULL; // 仅做示例,外部需要判空!本例中省去了这些异常处理操作
}

我们现在回顾Intersection::accept方法:

class Intersection : public Geometry {
public:
    Intersection() = default;
    Intersection(Geometry* g1, Geometry* g2) : g1_(g1), g2_(g2) {}
    void accept(Visitor& v) override {
        Geometry::type_tag tag;
        
        // 对g1_的操作
        if (g1_) tag = g1_->tag(); // 有g1的情况取g1的tag
        v.visit(tag); // 序列化g1_的类型值
        if (!g1_) g1_.reset(Visitor::make_geometry(tag)); // 没有g1_先通过make_geometry创建g1_再reset一下
        g1_->accept(v); // 序列化g1_本身
        
        // g2_操作与g1_一致
        if (g2_) tag = g2_->tag();
        v.visit(tag);
        if (!g2_) g2_.reset(Visitor::make_geometry(tag));
        g2_->accept(v);
    }
    type_tag tag() const override { return INTERSECTION; }
private:
    std::unique_ptr<Geometry> g1_;
    std::unique_ptr<Geometry> g2_;
};

所以此时反序列化操作如下,将tag从序列化字符串中取出:

class StringDeserializeVisitor : public Visitor {
public:
    StringDeserializeVisitor(const std::string& s) { S.str(s); }
    void visit(Geometry::type_tag& tag) override {
        size_t t;
        S >> t;
        tag = Geometry::type_tag(t); // 输出tag
    }
    void visit(double& x) override { S >> x; }
    ......
    void visit(Intersection& x) override { x.accept(*this); }

private:
    std::stringstream S;
};

一旦StringDeserializeVisitor恢复了tag,Intersection对象就可以调用工厂构造函数来构造正确的几何对象。 现在,我们可以从流中反序列化此对象,并且将Intersection恢复为我们序列化的对象的精确副本。

下一篇文章我们将讨论访问者模式在现代C++中的应用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
项目:使用AngularJs编写的简单 益智游戏(附源代码)  这是一个简单的 javascript 项目。这是一个拼图游戏,也包含一个填字游戏。这个游戏玩起来很棒。有两个不同的版本可以玩这个游戏。你也可以玩填字游戏。 关于游戏 这款游戏的玩法很简单。如上所述,它包含拼图和填字游戏。您可以通过移动图像来玩滑动拼图。您还可以选择要在滑动面板中拥有的列数和网格数。 另一个是填字游戏。在这里你只需要找到浏览器左侧提到的那些单词。 要运行此游戏,您需要在系统上安装浏览器。下载并在代码编辑器中打开此项目。然后有一个 index.html 文件可供您修改。在命令提示符中运行该文件,或者您可以直接运行索引文件。使用 Google Chrome 或 FireFox 可获得更好的用户体验。此外,这是一款多人游戏,双方玩家都是人类。 这个游戏包含很多 JavaScript 验证。这个游戏很有趣,如果你能用一点 CSS 修改它,那就更好了。 总的来说,这个项目使用了很多 javascript 和 javascript 库。如果你可以添加一些具有不同颜色选项的级别,那么你一定可以利用其库来提高你的 javascript 技能。 演示: 该项目为国外大神项目,可以作为毕业设计的项目,也可以作为大作业项目,不用担心代码重复,设计重复等,如果需要对项目进行修改,需要具备一定基础知识。 注意:如果装有360等杀毒软件,可能会出现误报的情况,源码本身并无病毒,使用源码时可以关闭360,或者添加信任。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值