哈喽,各位好!今天咱们来聊聊C++的访问者模式,这玩意儿听起来好像很高大上,其实理解起来也没那么难,而且在AST(抽象语法树)遍历和代码生成里,那可是相当实用。
啥是访问者模式?别慌,先讲故事
想象一下,你是个博物馆馆长,博物馆里摆满了各种各样的文物,比如雕塑、画作、青铜器等等。每个文物都有自己的特点,比如雕塑有材质、高度,画作有作者、风格。
现在,来了几波游客:
- 第一波: 想给所有文物拍照留念。
- 第二波: 想给所有文物做价值评估。
- 第三波: 想给所有青铜器进行防氧化处理。
如果让每个文物自己去实现这些功能,那文物类就得不断膨胀,而且如果以后再来一波“想给所有画作做修复”的游客,那就又得改文物类。这显然不符合“开闭原则”(对扩展开放,对修改关闭)。
这时候,访问者模式就派上用场了。
我们可以定义一个“访问者”接口,里面包含针对每种文物类型的访问方法,比如visit(Sculpture& sculpture)
、visit(Painting& painting)
、visit(BronzeWare& bronzeWare)
。
然后,每个游客(也就是每个操作)都实现一个具体的访问者类,比如Photographer
、ValuationExpert
、AntioxidantHandler
。这些访问者类会实现visit
方法,在这些方法里完成具体的操作。
最后,文物类只需要提供一个accept(Visitor& visitor)
方法,这个方法接收一个访问者对象,然后调用访问者对象对应的visit
方法。
这样,我们就可以灵活地添加新的操作,而无需修改文物类本身。
访问者模式的结构
用UML图来表示一下,大概是这样:
+---------------+ +----------------+ +----------------+
| Element |------>| Visitor |------>| ConcreteVisitor|
+---------------+ +----------------+ +----------------+
| accept(Visitor)| | visit(ElementA) | | visit(ElementA) |
+---------------+ | visit(ElementB) | | visit(ElementB) |
+----------------+ +----------------+
(具体的操作)
+---------------+
| ConcreteElementA |
+---------------+
| accept(Visitor)|
+---------------+
+---------------+
| ConcreteElementB |
+---------------+
| accept(Visitor)|
+---------------+
- Element (元素): 定义
accept
方法,接受一个访问者对象。 - ConcreteElement (具体元素): 实现
accept
方法,将自身传递给访问者。 - Visitor (访问者): 定义访问每个具体元素的方法。
- ConcreteVisitor (具体访问者): 实现访问者接口,定义针对每个具体元素的操作。
C++ 代码示例 (博物馆文物的故事)
#include <iostream>
#include <vector>
// 前置声明
class Sculpture;
class Painting;
class BronzeWare;
// 访问者接口
class Visitor {
public:
virtual void visit(Sculpture& sculpture) = 0;
virtual void visit(Painting& painting) = 0;
virtual void visit(BronzeWare& bronzeWare) = 0;
virtual ~Visitor() {} // 记得加虚析构函数,这是个好习惯
};
// 元素接口
class Artifact {
public:
virtual void accept(Visitor& visitor) = 0;
virtual ~Artifact() {}
};
// 具体元素 - 雕塑
class Sculpture : public Artifact {
public:
Sculpture(std::string material, int height) : material_(material), height_(height) {}
void accept(Visitor& visitor) override {
visitor.visit(*this);
}
std::string getMaterial() const { return material_; }
int getHeight() const { return height_; }
private:
std::string material_;
int height_;
};
// 具体元素 - 画作
class Painting : public Artifact {
public:
Painting(std::string artist, std::string style) : artist_(artist), style_(style) {}
void accept(Visitor& visitor) override {
visitor.visit(*this);
}
std::string getArtist() const { return artist_; }
std::string getStyle() const { return style_; }
private:
std::string artist_;
std::string style_;
};
// 具体元素 - 青铜器
class BronzeWare : public Artifact {
public:
BronzeWare(int age) : age_(age) {}
void accept(Visitor& visitor) override {
visitor.visit(*this);
}
int getAge() const { return age_; }
private:
int age_;
};
// 具体访问者 - 摄影师
class Photographer : public Visitor {
public:
void visit(Sculpture& sculpture) override {
std::cout << "Photographer: Taking a photo of sculpture (Material: "
<< sculpture.getMaterial() << ", Height: " << sculpture.getHeight() << ")n";
}
void visit(Painting& painting) override {
std::cout << "Photographer: Taking a photo of painting (Artist: "
<< painting.getArtist() << ", Style: " << painting.getStyle() << ")n";
}
void visit(BronzeWare& bronzeWare) override {
std::cout << "Photographer: Taking a photo of bronze ware (Age: "
<< bronzeWare.getAge() << " years)n";
}
};
// 具体访问者 - 价值评估专家
class ValuationExpert : public Visitor {
public:
void visit(Sculpture& sculpture) override {
std::cout << "Valuation Expert: Evaluating sculpture (Material: "
<< sculpture.getMaterial() << ", Height: " << sculpture.getHeight()
<< ") - Estimated value: $" << sculpture.getHeight() * 100 << "n";
}
void visit(Painting& painting) override {
std::cout << "Valuation Expert: Evaluating painting (Artist: "
<< painting.getArtist() << ", Style: " << painting.getStyle()
<< ") - Estimated value: $" << (painting.getArtist().length() + painting.getStyle().length()) * 50 << "n";
}
void visit(BronzeWare& bronzeWare) override {
std::cout << "Valuation Expert: Evaluating bronze ware (Age: "
<< bronzeWare.getAge() << " years) - Estimated value: $" << bronzeWare.getAge() * 20 << "n";
}
};
int main() {
std::vector<Artifact*> artifacts;
artifacts.push_back(new Sculpture("Marble", 150));
artifacts.push_back(new Painting("Van Gogh", "Post-Impressionism"));
artifacts.push_back(new BronzeWare(500));
Photographer photographer;
ValuationExpert valuationExpert;
for (Artifact* artifact : artifacts) {
artifact->accept(photographer);
artifact->accept(valuationExpert);
}
for (Artifact* artifact : artifacts) {
delete artifact;
}
return 0;
}
访问者模式在 AST 遍历中的应用
现在,让我们把这个模式应用到AST遍历中。AST是编译器前端的重要组成部分,它将源代码转换成一种树状结构,方便后续的分析和代码生成。
AST的节点类型很多,比如表达式节点、语句节点、声明节点等等。我们需要对AST进行遍历,执行各种操作,比如:
- 类型检查: 检查变量类型是否匹配。
- 代码优化: 移除冗余代码。
- 代码生成: 将AST转换成目标代码。
这时候,访问者模式就可以大显身手了。
AST 节点结构
假设我们有如下简单的AST节点类型:
// 抽象节点类
class ASTNode {
public:
virtual void accept(Visitor& visitor) = 0;
virtual ~ASTNode() {}
};
// 表达式节点
class ExpressionNode : public ASTNode {
public:
ExpressionNode(std::string value) : value_(value) {}
void accept(Visitor& visitor) override;
std::string getValue() const { return value_; }
private:
std::string value_;
};
// 语句节点
class StatementNode : public ASTNode {
public:
StatementNode(ASTNode* child) : child_(child) {}
void accept(Visitor& visitor) override;
ASTNode* getChild() const { return child_; }
private:
ASTNode* child_;
};
// 声明节点
class DeclarationNode : public ASTNode {
public:
DeclarationNode(std::string name, std::string type) : name_(name), type_(type) {}
void accept(Visitor& visitor) override;
std::string getName() const { return name_; }
std::string getType() const { return type_; }
private:
std::string name_;
std::string type_;
};
访问者实现
我们定义一个访问者接口,针对每种节点类型都有一个visit
方法:
class Visitor {
public:
virtual void visit(ExpressionNode& node) = 0;
virtual void visit(StatementNode& node) = 0;
virtual void visit(DeclarationNode& node) = 0;
virtual ~Visitor() {}
};
然后,在具体的节点类中实现accept
方法:
void ExpressionNode::accept(Visitor& visitor) {
visitor.visit(*this);
}
void StatementNode::accept(Visitor& visitor) {
visitor.visit(*this);
}
void DeclarationNode::accept(Visitor& visitor) {
visitor.visit(*this);
}
代码生成器
现在,我们可以创建一个代码生成器,它继承自Visitor
类:
#include <sstream>
class CodeGenerator : public Visitor {
public:
void visit(ExpressionNode& node) override {
codeStream_ << node.getValue();
}
void visit(StatementNode& node) override {
// 递归访问子节点
node.getChild()->accept(*this);
codeStream_ << ";n"; // 假设语句以分号结尾
}
void visit(DeclarationNode& node) override {
codeStream_ << node.getType() << " " << node.getName() << ";n";
}
std::string getCode() const {
return codeStream_.str();
}
private:
std::stringstream codeStream_;
};
使用示例
int main() {
// 构建一个简单的AST
DeclarationNode* declaration = new DeclarationNode("x", "int");
ExpressionNode* expression = new ExpressionNode("x + 1");
StatementNode* statement = new StatementNode(expression);
// 创建代码生成器
CodeGenerator generator;
// 遍历AST并生成代码
declaration->accept(generator);
statement->accept(generator);
// 输出生成的代码
std::cout << generator.getCode() << std::endl;
// 释放内存
delete declaration;
delete expression;
delete statement;
return 0;
}
这段代码会生成如下代码:
int x;
x + 1;
访问者模式的优点
- 解耦: 将数据结构(AST节点)和操作(代码生成、类型检查)分离。
- 扩展性: 可以方便地添加新的操作,而无需修改AST节点类。
- 灵活性: 可以对不同的节点类型执行不同的操作。
- 符合开闭原则: 对扩展开放,对修改关闭。
访问者模式的缺点
- 复杂性: 如果节点类型很多,访问者类的数量也会很多,会增加代码的复杂性。
- 节点类型变化敏感: 如果添加了新的节点类型,需要修改所有的访问者类。
访问者模式的适用场景
- 当需要对一个对象结构中的对象进行很多不同的操作,并且希望避免让这些操作“污染”这些对象类的时候。
- 当一个对象结构包含很多类对象,它们有不同的接口,而你想对这些对象实施一些依赖于其具体类的操作。
- 当定义在该对象结构上的操作需要经常变化的时候。
访问者模式与其他模式的比较
模式 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
访问者模式 | 解耦数据结构和操作,扩展性好,灵活性高,符合开闭原则。 | 复杂性较高,节点类型变化敏感。 | 需要对对象结构进行多种不同操作,且操作经常变化,或者需要避免操作“污染”对象类。 |
策略模式 | 将算法封装成独立的类,可以动态切换算法,易于扩展。 | 需要定义多个策略类,增加代码量。 | 需要在运行时动态选择算法,或者需要将算法从上下文中解耦。 |
模板方法模式 | 定义算法的骨架,将一些步骤延迟到子类实现,可以避免代码重复。 | 子类需要实现抽象方法,可能会导致代码分散。 | 算法的步骤基本固定,但某些步骤需要根据具体情况进行调整。 |
迭代器模式 | 提供一种访问聚合对象元素的方法,而无需暴露其内部结构。 | 需要定义迭代器类,增加代码量。 | 需要遍历聚合对象,且不想暴露其内部结构。 |
总结
访问者模式是一种强大的设计模式,它可以将数据结构和操作分离,提高代码的灵活性和可扩展性。在AST遍历和代码生成等场景中,访问者模式可以发挥重要作用。当然,访问者模式也有其缺点,需要根据具体情况进行权衡。
希望这次讲解能帮助大家理解访问者模式,并在实际项目中灵活运用。以后有机会再和大家分享其他的设计模式和编程技巧。
各位,下次再见!