访问者模式

U1S1, 挺复杂的一个设计模式~

意图

表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

动机

假设 一个场景,某公司的研发岗位的 所有员工 大致可以按照开发语言使用的不同分为 3 类工程师 C++开发工程师Go开发工程师Python开发工程师
A 项目来了,可能让你们的 C++开发工程师 去编写算法, Go开发工程师 去实现后端, Python开发工程师 去开发前端
B 项目来了,这个项目让 Go开发工程师 开发前端, Python开发工程师 写算法等等安排
之后可能还有 C 项目,D 项目 等等

很显然,工程师 可以先抽象基类,然后分别实现 3 个子类,当然他们也有一些自己独有的接口,比如 C++开发工程师 可以开发 音视频Python开发工程师 可以编写自动化测试等等
A 项目, B 项目等等这些项目肯定先抽象成一个 项目 的基类
那么剩下的就是如何让 工程师项目 关联起来呢?

而这就是 访问者模式 的核心,首先先看 UML

UML 图

uml

  • Visitor (访问者, 如前面提到的 项目 基类)
    为该对象结构中的 ConcreteElement 的每一种子类声明一个 visit 操作,该操作的名字和特征标识了发送 visit 请求给该访问者的那个类,这使得访问者可以确定正被访问元素的具体的类,这样访问者者就可以通过该元素的特定接口直接访问它
  • ConcreteVisitor (具体访问者,如 项目A项目B 等等)
    实现每个由 Visitor 声明的操作
  • Element (元素,如抽象的 工程师 基类)
    定义一个 Accept 操作,它以一个访问者为参数
  • ConcreteElement (具体元素,如 C++开发工程师Go开发工程师 等)
    实现 Accept 操作,它以一个访问者为参数
  • ObjectStructure (对象结构,比如用一个 list 来管理所有的研发岗位的员工)
    是一个包含元素角色的容器,提供让访问者对象遍历容器中的所有元素的方法

UML 中可以看出,访问者中提供的接口是针对具体的 ConcreteElement 来确定的,并且传入了具体的 ConcreteElement 作为参数,所以缺点很明显

  • 访问者模式依赖了具体类,而没有依赖抽象类,违反了 依赖倒置原则 (程序要依赖于抽象接口,不要依赖于具体实现)
  • 此外增加新的 ConcreteElement 类的工作量会很大,每增加一种新的 ConcreteElement 类都要在每一个具体访问者类中增加相应的具体操作,也不符合 开闭原则 (软件实体应当对扩展开放,对修改关闭)

但是优点也很明显:

  • 将操作 ConcreteElement 的行为 封装Visitor 中,使类的功能比较单一,或者可以看成将数据结构与作用于结构上的操作解耦,更加灵活
  • 在不修改 ConcreteElement 的基础上,单方面在 Visitor 这一方向有很好的扩展性

应用场景

在下列情况下使用 Visitor 模式:

  • 一个对象结构包含很多类对象,它们有不同的接口,而你想对这些对象实施一些依赖于其具体类的操作。
  • 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而你想避免让这些操作“污染”这些对象的类。 Visitor 使得你可以将相关的操作集中起来定义在一个类中。当该对象结构被很多应用共享时,用 Visitor 模式让每个应用仅包含需要用到的操作。
  • 定义对象结构的类很少改变,但经常需要在此结构上定义新的操作。改变对象结构类需要重定义对所有访问者的接口,这可能需要很大的代价。如果对象结构类经常改变,那么可能还是在这些类中定义这些操作较好。

举个例子

直接使用动机里的例子,首先分别定义 员工 的基类

#ifndef ELEMENT_H
#define ELEMENT_H

#include <string>
using namespace std;

class Visitor;

class Element
{
public:
    Element(const string name) : m_name(name) {}
    virtual ~Element() {}

    string getName() { return m_name; }
    virtual void accept(Visitor *) = 0;

private:
    string m_name;
};

#endif // ELEMENT_H

然后是按照使用语言不同的具体员工子类

#ifndef CONCRETEELEMENT_H
#define CONCRETEELEMENT_H

#include "element.h"
#include "visitor.h"

#include <iostream>
using namespace std;

class CppDevEngineer : public Element{
public:
    CppDevEngineer(string name): Element(name) {}

    virtual void accept(Visitor *visitor) {
        cout << "我是 C++ 软件开发师" << endl;
        visitor->AssignedTasks(this);
    }
};

class GoDevEngineer : public Element{
public:
    GoDevEngineer(string name): Element(name) {}
    virtual void accept(Visitor *visitor) {
        cout << "我是 Go 软件开发师" << endl;
        visitor->AssignedTasks(this);
    }
};

class PythonDevEngineer : public Element{
public:
    PythonDevEngineer(string name): Element(name) {}
    virtual void accept(Visitor *visitor) {
        cout << "我是 Python 软件开发师" << endl;
        visitor->AssignedTasks(this);
    }
};

#endif // CONCRETEELEMENT_H

接着我们定义项目这个访问者的基类 Visitor, 它是依赖于具体的员工子类的

#ifndef VISITOR_H
#define VISITOR_H

#include "concreteelement.h"

class CppDevEngineer;
class GoDevEngineer;
class PythonDevEngineer;

class Visitor
{
public:
    virtual ~Visitor() {}
    virtual void AssignedTasks(CppDevEngineer *) = 0;
    virtual void AssignedTasks(GoDevEngineer *) = 0;
    virtual void AssignedTasks(PythonDevEngineer *) = 0;
};

#endif // VISITOR_H

然后我们需要实现具体的项目对象

#ifndef CONCRETEVISITOR_H
#define CONCRETEVISITOR_H

#include "visitor.h"
#include "concreteelement.h"
#include <iostream>

using namespace std;

class ProjectA : public Visitor {
public:
    virtual void AssignedTasks(CppDevEngineer *element) {
        cout << element->getName() << ", ProjectA 需要你做后端开发" << endl;
    }

    virtual void AssignedTasks(GoDevEngineer *element) {
        cout << element->getName() << ", ProjectA 需要你做算法开发" << endl;
    }

    virtual void AssignedTasks(PythonDevEngineer *element) {
        cout << element->getName() << ", ProjectA 需要你自动化测试" << endl;
    }
};


class ProjectB : public Visitor {
public:
    virtual void AssignedTasks(CppDevEngineer *element) {
        cout << element->getName() << ", ProjectB 暂时不需要C++开发工程师开发" << endl;
    }

    virtual void AssignedTasks(GoDevEngineer *element) {
        cout << element->getName() << ", ProjectB 需要你做后端开发" << endl;
    }

    virtual void AssignedTasks(PythonDevEngineer *element) {
        cout << element->getName() << ", ProjectB 需要你做前端开发" << endl;
    }
};

#endif // CONCRETEVISITOR_H

最后就是看一下 main() 函数的调用,并且简单实用 list 来管理所有的员工,也很方便遍历

#include "concreteelement.h"
#include "concretevisitor.h"

#include <list>
#include <iostream>
using namespace std;

template <typename Container>
inline void DeleteAll(const Container &c)
{
    auto begin = c.begin();
    auto end = c.end();
    while (begin != end) {
        delete *begin;
        ++begin;
    }
}

int main()
{
    list<Element *> staffs;
    staffs.push_back(new CppDevEngineer("xiaoA"));
    staffs.push_back(new GoDevEngineer("xiaoB"));
    staffs.push_back(new PythonDevEngineer("xiaoC"));

    ProjectA a;
    cout << "项目 A 安排 : " << endl;
    for (auto i : staffs) {
        i->accept(&a);
    }

    cout << "-------------------------------" << endl;

    ProjectB b;
    cout << "项目 B 安排 : " << endl;
    for (auto i : staffs) {
        i->accept(&b);
    }

    DeleteAll(staffs);
    return 0;
}

运行结果:

项目 A 安排 : 
我是 C++ 软件开发师
xiaoA, ProjectA 需要你做后端开发
我是 Go 软件开发师
xiaoB, ProjectA 需要你做算法开发
我是 Python 软件开发师
xiaoC, ProjectA 需要你自动化测试
-------------------------------
项目 B 安排 : 
我是 C++ 软件开发师
xiaoA, ProjectB 暂时不需要C++开发工程师开发
我是 Go 软件开发师
xiaoB, ProjectB 需要你做后端开发
我是 Python 软件开发师
xiaoC, ProjectB 需要你做前端开发

注意点:

Visitor 基类使用的是函数重载, 所有 Visitor 中分配函数的名称都是 AssignedTasks,区分只有传入对象,这样在具体员工中的调用起来更简单了,但是如果不需要传参的话,重载就没有意义了,就需要通过不同的函数名称来区分

思考

其实这个给我的感觉有点神似 建造者模式(Builder),建造者是将复杂的 创建行为 封装进 指挥者(Director) 中,因为 Director 是依赖于抽象,所以可以直接通过给 Director 传入元素,就能对该元素进行定义的行为

Visitor 是针对不同子类的行为封装,子类实现的时候是清楚 Visitor 对应的行为接口,所以我们是通过调用 Elementaccept(Visitor *) 函数

那为什么不直接像 建造者模式 一样直接操作封装好行为的 DirectorVisitor 呢?

我觉得可以做到,但是得不偿失

原因一: Vistor 常常都会配合组合模式与迭代器模式,以例子为例遍历 list<Element *> 及使用 for 这种方式遍历对象结构,如果使用 Visitor ,代码可能是这样的

for (auto i : staffs) {
    if (auto p = dynamic_cast<CppDevEngineer *>(i)) {
        a.AssignedTasks(p);
        continue;
    }

    if (auto p = dynamic_cast<GoDevEngineer *>(i)) {
        a.AssignedTasks(p);
        continue;
    }

    if (auto p = dynamic_cast<PythonDevEngineer *>(i)) {
        a.AssignedTasks(p);
        continue;
    }
}

从迭代器中重新取出的对象并不是具体的子类,需要做显式类型转化

原因二: 本来客户端使用的时候是可以不关心 Visitor 任何接口的,也就是破坏了 Visitor 的封装性

总结

Element 在具有稳定子类的前提下,针对将每一种子类的不同处理行为封装进 Visitor 中,本来每次添加一种新的处理行为,应该是 Element 上实现一个对应的接口,现在将这一系列的接口抽象成一个 accept(Visitor *) 的接口,真正的行为交给 Visitor 实现,很厉害的抽象思维~