1 访问者模式的基本概念

C++中的访问者模式是一种行为设计模式,它允许你在不修改类层次结构的情况下增加新的操作。这种模式将数据结构与数据操作解耦,使得操作可以独立于对象的类来定义。

访问者模式的主要组成部分包括:

(1)访问者(Visitor): 这是一个接口,它声明了一个访问操作,该操作可以被应用到所有的元素上。访问操作的具体实现在访问者的子类中完成。

(2)元素(Element): 这是一个接口,它定义了一个接受操作,该操作可以被一个访问者访问。元素接口通常有一个接受访问者作为参数的接受方法。

(3)具体元素(Concrete Element): 这是实现元素接口的具体类,它实现了接受方法,该方法将调用访问者的访问方法。

(4)对象结构(Object Structure): 这是元素的集合,它以一种合适的方式包含元素。对象结构可以是一个简单的集合,也可以是一个复杂的树形结构。

2 访问者模式的实现步骤

访问者模式的实现步骤如下:

(1)定义访问者接口:

首先,需要定义一个访问者接口,这个接口声明了所有可能应用于元素对象的操作。这些操作通常作为纯虚函数存在,具体的操作实现将由具体的访问者类来完成。

(2)定义元素接口:

接下来,定义一个元素接口,这个接口声明了一个接受方法,该方法接受一个访问者对象作为参数。元素接口中的接受方法通常是虚函数,以便在元素的具体类中实现多态性。

(3)实现具体元素类:

然后,创建实现元素接口的具体元素类。这些类将包含数据,并实现接受方法,该方法将调用访问者的访问方法。

(4)实现具体的访问者类:

接着,创建实现访问者接口的具体访问者类。这些类将包含访问元素所需的具体逻辑。当访问者的访问方法被调用时,它将针对具体的元素对象执行相应的操作。

(5)创建对象结构:

创建一个对象结构,该结构负责管理元素对象的集合。这可以是一个简单的容器,如数组或向量,也可以是一个更复杂的结构,如树或图。

(6)使用访问者模式:

最后,在客户端代码中,创建具体的访问者对象,并通过对象结构访问元素对象。当需要执行某个操作时,通过对象结构的遍历方法,将具体的访问者对象传递给每个元素对象的接受方法。元素对象的接受方法将调用访问者的访问方法,从而执行相应的操作。

如下为样例代码:

#include <iostream>  
#include <vector>  
#include <memory>  

class Element;

// 步骤一:定义访问者接口  
class Visitor {
public:
	virtual ~Visitor() = default;
	virtual void visit(const std::shared_ptr<Element>& element) const = 0;
};

// 步骤二:定义元素接口  
class Element : public std::enable_shared_from_this<Element> {
public:
	virtual ~Element() = default;
	virtual void accept(const std::shared_ptr<Visitor>& visitor) = 0;
};

// 步骤三:实现具体元素类  
class ConcreteElement : public Element {
public:
	void accept(const std::shared_ptr<Visitor>& visitor) override {
		visitor->visit(shared_from_this());
	}

	void operation() const {
		std::cout << "ConcreteElement operation" << std::endl;
	}
};

// 步骤四:实现具体的访问者类  
class ConcreteVisitor : public Visitor {
public:
	void visit(const std::shared_ptr<Element>& element) const override {
		auto concreteElement = std::dynamic_pointer_cast<ConcreteElement>(element);
		if (concreteElement) {
			concreteElement->operation();
			std::cout << "Visited by ConcreteVisitor" << std::endl;
		}
		else {
			std::cout << "Element is not of type ConcreteElement" << std::endl;
		}
	}
};

// 步骤五:创建对象结构  
class ObjectStructure {
public:
	void attach(const std::shared_ptr<Element>& element) {
		m_elements.push_back(element);
	}

	void detach(const std::shared_ptr<Element>& element) {
		auto it = std::find(m_elements.begin(), m_elements.end(), element);
		if (it != m_elements.end()) {
			m_elements.erase(it);
		}
	}

	void accept(const std::shared_ptr<Visitor>& visitor) const {
		for (const auto& element : m_elements) {
			element->accept(visitor);
		}
	}

private:
	std::vector<std::shared_ptr<Element>> m_elements;
};

// 步骤六:使用访问者模式  
int main() 
{
	// 创建对象结构并添加元素  
	ObjectStructure objectStructure;
	objectStructure.attach(std::make_shared<ConcreteElement>());

	// 创建访问者  
	std::shared_ptr<Visitor> visitor = std::make_shared<ConcreteVisitor>();

	// 执行访问  
	objectStructure.accept(visitor);

	return 0;
}

上面代码的输出为:

ConcreteElement operation
Visited by ConcreteVisitor

上面的代码使用了 std::shared_ptr 智能指针来管理 Visitor 和 Element 对象的生命周期。std::dynamic_pointer_cast 用于安全地执行动态类型转换,以确认 Element 指针是否可以转换为 ConcreteElement 类型。如果转换成功,则执行相应的操作。

ObjectStructure 类负责管理 Element 对象的集合,并提供 attach 和 detach 方法来添加和删除元素。accept 方法则遍历所有元素,并将访问者传递给每个元素的 accept 方法,从而执行访问操作。

在 main 函数中,创建了一个 ObjectStructure 对象,并向其添加了一个 ConcreteElement 对象。然后,创建了一个 ConcreteVisitor 对象,并通过 ObjectStructure 的 accept 方法将访问者传递给所有元素。这样,就实现了访问者模式的使用。

3 访问者模式的应用场景

C++ 访问者模式的应用场景广泛,主要适用于以下情况:

(1)双层分派场景: 访问者模式实现了一种称为“双层分派”的机制。第一层分派发生在客户端代码中,当客户端将访问者对象传递给元素对象的 accept 方法时。第二层分派发生在元素对象的 accept 方法内部,它根据访问者的类型调用相应的访问方法。这种双层分派机制使得操作可以在元素对象的上下文中执行,并且可以根据元素的类型进行不同的处理。

(2)数据结构稳定但操作易变的场景: 如果数据结构相对稳定,但需要在其上执行的操作经常变化,访问者模式可以帮助将这些操作与数据结构解耦,使得操作的变化不会影响到数据结构的定义。

(3)操作需要扩展的场景: 当需要对一个对象结构中的元素执行多种不同的、不相关的操作时,且这些操作可能会在未来发生变化或增加时,使用访问者模式非常合适。通过将操作封装在访问者中,可以在不修改元素类的情况下添加新的操作。

(4)需要对对象结构中的对象进行不同操作的场景: 当需要在运行时决定对对象结构中的哪些元素执行哪些操作时,访问者模式可以很容易地实现这种动态行为。

(5)复杂的对象结构遍历: 当需要遍历一个复杂的对象结构(如树形结构、图结构等),并对结构中的每个元素执行特定的操作时,访问者模式可以简化代码结构,使得遍历和操作逻辑更加清晰和分离。

3.1 访问者模式应用于双层分派场景

双层分派允许根据对象类型和访问者类型动态地决定执行哪个操作。以下是一个简单的游戏场景,其中游戏角色可以被不同的访问者(例如渲染器或AI处理器)访问。

首先,定义访问者接口和元素接口:

#include <iostream>  
#include <memory>  
#include <vector>  
#include <string>  

class Character;

// 访问者接口  
class Visitor {
public:
	virtual ~Visitor() = default;
	virtual void visit(const std::shared_ptr<Character>& character) const = 0;
};

// 元素接口  
class Character : public std::enable_shared_from_this<Character> {
public:
	virtual ~Character() = default;
	virtual void accept(const std::shared_ptr<Visitor>& visitor) = 0;
	virtual std::string getName() const = 0;
};

接着,定义具体的游戏角色类和访问者类:

// 具体游戏角色类A  
class Warrior : public Character {
public:
	std::string getName() const override { return "Warrior"; }
	void accept(const std::shared_ptr<Visitor>& visitor) override {
		visitor->visit(shared_from_this());
	}
	void fight() const {
		std::cout << getName() << " is fighting!" << std::endl;
	}
};

// 具体游戏角色类B  
class Mage : public Character {
public:
	std::string getName() const override { return "Mage"; }
	void accept(const std::shared_ptr<Visitor>& visitor) override {
		visitor->visit(shared_from_this());
	}
	void castSpell() const {
		std::cout << getName() << " is casting a spell!" << std::endl;
	}
};

// 具体访问者类:渲染器  
class Renderer : public Visitor {
public:
	void visit(const std::shared_ptr<Character>& character) const override {
		std::cout << "Rendering " << character->getName() << std::endl;
	}
};

// 具体访问者类:AI处理器  
class AIProcessor : public Visitor {
public:
	void visit(const std::shared_ptr<Character>& character) const override {
		std::cout << "Processing AI for " << character->getName() << std::endl;
		// 这里可以根据角色类型执行不同的AI逻辑  
	}
};

最后,定义一个管理游戏角色的对象结构类,并使用访问者进行双层分派:

// 对象结构类:游戏世界  
class GameWorld {
public:
	void addCharacter(const std::shared_ptr<Character>& character) {
		m_characters.push_back(character);
	}

	void render(const std::shared_ptr<Visitor>& visitor) const {
		for (const auto& character : m_characters) {
			character->accept(visitor);
		}
	}

private:
	std::vector<std::shared_ptr<Character>> m_characters;
};

int main() 
{
	// 创建游戏角色  
	auto warrior = std::make_shared<Warrior>();
	auto mage = std::make_shared<Mage>();

	// 创建游戏世界并添加角色  
	GameWorld gameWorld;
	gameWorld.addCharacter(warrior);
	gameWorld.addCharacter(mage);

	// 创建渲染器访问者  
	auto renderer = std::make_shared<Renderer>();
	// 使用渲染器访问者渲染所有角色  
	gameWorld.render(renderer);

	// 创建AI处理器访问者  
	auto aiProcessor = std::make_shared<AIProcessor>();
	// 使用AI处理器访问者处理所有角色的AI  
	gameWorld.render(aiProcessor);

	// 不需要手动释放对象,智能指针会自动处理  
	return 0;
}

上面代码的输出为:

Rendering Warrior
Rendering Mage
Processing AI for Warrior
Processing AI for Mage

在这个示例中,Warrior 和 Mage 类继承了 Character 接口,并实现了各自的 accept 方法和特定行为(如 fight 和 castSpell)。Renderer 和 AIProcessor 类继承了 Visitor 接口,并实现了针对 Character 的 visit 方法。

GameWorld 类管理着游戏中的角色,并提供了 render 方法,该方法接受一个 Visitor 智能指针,并使用 accept 方法触发双层分派。当调用 gameWorld.render(renderer) 时,所有角色都会被渲染器访问,并调用它们各自的 accept 方法。由于 accept 方法内部调用了 visitor->visit(shared_from_this()),这就会触发对应的 visit 方法,在这里是 Renderer 类的 visit 方法,负责渲染角色。

3.2 访问者模式应用数据结构稳定但操作易变的场景

该场景是指:对于一组稳定的数据结构(元素),在这些数据结构上执行各种易变的操作(访问者)时,每个操作可能需要访问数据结构的不同部分,并且可能以不同的方式处理这些部分。

下面是一个使用访问者模式的示例,它模拟了一个简单的形状集合,其中形状是稳定的数据结构,而不同的操作(如计算面积、周长等)是易变的访问者。

首先,定义形状接口和访问者接口:

#include <iostream>  
#include <memory>  
#include <vector>  

class Visitor;

// 形状接口  
class Shape : public std::enable_shared_from_this<Shape> {
public:
	virtual ~Shape() = default;
	virtual void accept(const std::shared_ptr<Visitor>& visitor) = 0;
};

class Circle;
class Rectangle;

// 访问者接口  
class Visitor {
public:
	virtual ~Visitor() = default;
	virtual void visit(const std::shared_ptr<Circle>& circle) const = 0;
	virtual void visit(const std::shared_ptr<Rectangle>& rectangle) const = 0;
};

接着,定义具体的形状类和访问者类:

// 圆形类  
class Circle : public Shape {
	double radius;
public:
	Circle(double r) : radius(r) {}
	void accept(const std::shared_ptr<Visitor>& visitor) override {
		visitor->visit(std::dynamic_pointer_cast<Circle>(shared_from_this()));
	}
	double getRadius() const { return radius; }
};

// 矩形类  
class Rectangle : public Shape {
	double width, height;
public:
	Rectangle(double w, double h) : width(w), height(h) {}
	void accept(const std::shared_ptr<Visitor>& visitor) override {
		visitor->visit(std::dynamic_pointer_cast<Rectangle>(shared_from_this()));
	}
	double getWidth() const { return width; }
	double getHeight() const { return height; }
};

// 计算面积的访问者  
class AreaVisitor : public Visitor {
public:
	void visit(const std::shared_ptr<Circle>& circle) const override {
		std::cout << "Circle area: " << 3.14159 * circle->getRadius() * circle->getRadius() << std::endl;
	}
	void visit(const std::shared_ptr<Rectangle>& rectangle) const override {
		std::cout << "Rectangle area: " << rectangle->getWidth() * rectangle->getHeight() << std::endl;
	}
};

// 计算周长的访问者  
class PerimeterVisitor : public Visitor {
public:
	void visit(const std::shared_ptr<Circle>& circle) const override {
		std::cout << "Circle perimeter: " << 2 * 3.14159 * circle->getRadius() << std::endl;
	}
	void visit(const std::shared_ptr<Rectangle>& rectangle) const override {
		std::cout << "Rectangle perimeter: " << 2 * (rectangle->getWidth() + rectangle->getHeight()) << std::endl;
	}
};

最后,创建一个管理形状的对象结构类,并使用访问者进行操作:

// 对象结构类:形状集合  
class ShapeCollection {
public:
	void addShape(const std::shared_ptr<Shape>& shape) {
		m_shapes.push_back(shape);
	}

	void process(const std::shared_ptr<Visitor>& visitor) const {
		for (const auto& shape : m_shapes) {
			shape->accept(visitor);
		}
	}

private:
	std::vector<std::shared_ptr<Shape>> m_shapes;
};

int main() 
{
	// 创建形状  
	auto circle = std::make_shared<Circle>(5.0);
	auto rectangle = std::make_shared<Rectangle>(4.0, 6.0);

	// 创建形状集合并添加形状  
	ShapeCollection collection;
	collection.addShape(circle);
	collection.addShape(rectangle);

	// 创建并使用面积访问者  
	auto areaVisitor = std::make_shared<AreaVisitor>();
	collection.process(areaVisitor);

	// 创建并使用周长访问者  
	auto perimeterVisitor = std::make_shared<PerimeterVisitor>();
	collection.process(perimeterVisitor);

	// 不需要手动释放对象,智能指针会自动处理  
	return 0;
}

上面代码的输出为:

Circle area: 78.5397
Rectangle area: 24
Circle perimeter: 31.4159
Rectangle perimeter: 20

在这个示例中,Circle 和 Rectangle 类继承了 Shape 接口,并实现了各自的 accept 方法和特定行为(半径以及长宽)。AreaVisitor 和 PerimeterVisitor 类继承了 Visitor 接口,并实现了计算圆形面积周长以及计算矩形面积周长的 visit 方法。随后在 ShapeCollection 的 process 进行访问调用。

4 访问者模式的优点与缺点

C++ 访问者模式的优点主要包括:

(1)扩展性好: 访问者模式使得在不修改已有类的情况下,可以添加新的操作。这意味着数据结构本身保持稳定,而新的操作可以很容易地添加到系统中。

(2)操作集中管理: 访问者模式将相关的操作集中到一个访问者类中,这有助于操作的封装和复用。

(3)双分派: 访问者模式通过对象的“接受”方法和访问者的“访问”方法实现了双分派,即运行时根据对象的实际类型和访问者的类型来确定执行的操作。

(4)灵活性: 访问者模式允许我们根据需要灵活地定义不同的访问者类,实现不同的操作逻辑。

(5)符合开闭原则: 访问者模式符合开闭原则,即对扩展开放,对修改封闭。当需要添加新的操作时,我们只需要添加新的访问者类,而不需要修改已有的数据结构。

然而,C++ 访问者模式也存在一些缺点:

(1)增加复杂性: 访问者模式增加了类的数量,使得系统变得更加复杂。每个元素类都需要实现接受方法,每个访问者类都需要实现访问方法,这可能导致代码量的增加。

(2)破坏封装性: 访问者模式要求元素类暴露其内部状态给访问者类,这可能会破坏元素的封装性。如果访问者类不恰当地使用这些内部状态,可能会导致错误或不可预见的行为。

(3)类型安全: 在访问者模式中,由于访问者类需要访问元素类的内部状态,因此需要确保类型安全。否则,可能会导致运行时错误。

(4)性能开销: 由于访问者模式涉及到双分派,因此在某些情况下可能会引入额外的性能开销。

03-18 22:09