访问者模式
(Visitor)
访问者 是一种行为设计模式,它能将算法与其所作用的对象隔离开来。
1. 问题
假如你的团队开发了一款能够使用巨型图像中地理信息的应用程序。图像中的每个节点既能代表复杂实体(例如一座城市), 也能代表更精细的对象(例如工业区和旅游景点等)。如果节点代表的真实对象之间存在公路,那么这些节点就会相互连接。在程序内部,每个节点的类型都由其所属的类来表示,每个特定的节点则是一个对象。
将图像导出为 XML。
一段时间后, 你接到了实现将图像导出到 XML 文件中的任务。这些工作最初看上去非常简单。你计划为每个节点类添加导出函数,然后递归执行图像中每个节点的导出函数。解决方案简单且优雅:使用多态机制可以让导出方法的调用代码不会和具体的节点类相耦合。
但你不太走运,系统架构师拒绝批准对已有节点类进行修改。他认为这些代码已经是产品了,不想冒险对其进行修改,因为修改可能会引入潜在的缺陷。
所有节点的类中都必须添加导出至 XML 文件的方法,但如果在修改代码的过程中引入了任何缺陷,那么整个程序都会面临风险。
此外,他还质疑在节点类中包含导出 XML 文件的代码是否有意义。这些类的主要工作是处理地理数据。导出 XML 文件的代码放在这里并不合适。
还有另一个原因,那就是在此项任务完成后,营销部门很有可能会要求程序提供导出其他类型文件的功能,或者提出其他奇怪的要求。这样你很可能会被迫再次修改这些重要但脆弱的类。
2. 解决方案
访问者模式建议将新行为放入一个名为访问者的独立类中,而不是试图将其整合到已有类中。现在,需要执行操作的原始对象将作为参数被传递给访问者中的方法,让方法能访问对象所包含的一切必要数据。
如果现在该操作能在不同类的对象上执行会怎么样呢?比如在我们的示例中,各节点类导出 XML 文件的实际实现很可能会稍有不同。因此,访问者类可以定义一组(而不是一个)方法,且每个方法可接收不同类型的参数。
但我们究竟应该如何调用这些方法(尤其是在处理整个图像方面)呢?这些方法的签名各不相同,因此我们不能使用多态机制。 为了可以挑选出能够处理特定对象的访问者方法,我们需要对它的类进行检查。这是不是听上去像个噩梦呢?
你可能会问,我们为什么不使用方法重载呢?就是使用相同的方法名称,但它们的参数不同。不幸的是,即使我们的编程语言(例如 Java 和 C#)支持重载也不行。由于我们无法提前知晓节点对象所属的类,所以重载机制无法执行正确的方法。方法会将 节点 基类作为输入参数的默认类型。
但是,访问者模式可以解决这个问题。它使用了一种名为双分派的技巧,不使用累赘的条件语句也可下执行正确的方法。与其让客户端来选择调用正确版本的方法,不如将选择权委派给作为参数传递给访问者的对象。由于该对象知晓其自身的类,因此能更自然地在访问者中选出正确的方法。它们会“接收”一个访问者并告诉其应执行的访问者方法。
我承认最终还是修改了节点类,但毕竟改动很小,且使得我们能够在后续进一步添加行为时无需再次修改代码。
现在,如果我们抽取出所有访问者的通用接口,所有已有的节点都能与我们在程序中引入的任何访问者交互。如果需要引入与节点相关的某个行为,你只需要实现一个新的访问者类即可。
3. 结构
访问者模式的结构
其中:
*Concrete Visitor(具体访问者) 实现每个有Visitor声明的操作, 每个操作实现本算法的一部分, 而该算法片段乃是对应于结构中对象的类。Concrete Visitor为该算法提供了上下文并存储它的局部状态。这一状态常常在遍历该结构的过程中累积结果。
*Element(元素) 定义以一个访问者为参数的Accept操作。
*Concrete Element(具体元素) 实现以一个访问者为参数的Accept操作。
*Object Structure(对象结构) 能枚举它的元素; 可以提供一个高层的接口以允许该访问者访问它的元素;可以是一个组合或者一个集合,如一个列表或一个无序集合。
4. 实现方式
1. 在访问者接口中声明一组“访问”方法,分别对应程序中的每个具体元素类。
2. 声明元素接口。
如果程序中已有元素类层次接口,可在层次结构基类中添加抽象的“接收”方法。该方法必须接受访问者对象作为参数。
3. 在所有具体元素类中实现接收方法。
这些方法必须将调用重定向到当前元素对应的访问者对象中的访问者方法上。
4. 元素类只能通过访问者接口与访问者进行交互。
不过访问者必须知晓所有的具体元素类,因为这些类在访问者方法中都被作为参数类型引用。
5. 为每个无法在元素层次结构中实现的行为创建一个具体访问者类并实现所有的访问者方法。
你可能会遇到访问者需要访问元素类的部分私有成员变量的情况。在这种情况下,你要么将这些变量或方法设为公有,这将破坏元素的封装;要么将访问者类嵌入到元素类中。后一种方式只有在支持嵌套类的编程语言中才可能实现。
6. 客户端必须创建访问者对象并通过“接收”方法将其传递给元素。
5. 代码示例
visitor.h
#ifndef DESIGN_PATTERNS_VISITOR_H
#define DESIGN_PATTERNS_VISITOR_H
#include <iostream>
#include <string>
#include <vector>
using namespace std;
//------------------------------//
class Action; //"行动"类
//------------------------------//
class Person //"人员"类
{
public:
virtual ~Person() {}
virtual void Accept(Action*) = 0;//接受行动
};
class Man : public Person //"男性"类
{
public:
void Accept(Action*);
};
class Woman : public Person //"女性"类
{
public:
void Accept(Action*);
};
//------------------------------//
class ObjectStructure //"对象结构"类
{
public:
void Attach(Person*);//加载
void Detach(Person*);//卸载
void Display(Action*);//显示
private:
vector <Person*> people;
};
//------------------------------//
class Action //"行动"类
{
public:
virtual void GetManConclusion(Person*) = 0;//男性行动结果
virtual void GetWomanConclusion(Person*) = 0;//女性行动结果
};
class Success : public Action //"成功行动"类
{
void GetManConclusion(Person*);//男性行动结果
void GetWomanConclusion(Person*);//女性行动结果
};
class Failure : public Action //"失败行动"类
{
void GetManConclusion(Person*);//男性行动结果
void GetWomanConclusion(Person*);//女性行动结果
};
//------------------------------//
#endif //DESIGN_PATTERNS_VISITOR_H
visitor.c
#include "visitor.h"
//------------------------------//
void Man::Accept(Action *action)
{
action->GetManConclusion(this);
}
void Woman::Accept(Action *action)
{
action->GetWomanConclusion(this);
}
//------------------------------//
void ObjectStructure::Attach(Person *person)
{
people.push_back(person);
}
void ObjectStructure::Detach(Person *person)
{
for (vector <Person*>::iterator it = people.begin(); it != people.end(); ++it)
{
if (*it == person)
{
people.erase(it);
return;
}
}
}
void ObjectStructure::Display(Action *action)
{
for (vector<Person *>::iterator it = people.begin(); it != people.end(); ++it)
{
(*it)->Accept(action);
}
}
//------------------------------//
void Success::GetManConclusion(Person *person)
{
cout << "男性获得成功" << endl;
}
void Success::GetWomanConclusion(Person *person)
{
cout << "女性获得成功" << endl;
}
void Failure::GetManConclusion(Person *person)
{
cout << "男性行动失败" << endl;
}
void Failure::GetWomanConclusion(Person *person)
{
cout << "女性行动失败" << endl;
}
//------------------------------//
Main.c
//------------------------------//
#include <iostream>
#include "visitor.h"
using namespace std;
//------------------------------//
// Created by Cls on 2024/04/01.
//------------------------------//
int main(int argc, char *argv[])
{
Person *man_;
Person *woman_;
ObjectStructure *object_structure_;
Action *success_;
Action *failure_;
//-----------------//
man_ = new Man();
woman_ = new Woman();
object_structure_ = new ObjectStructure();
success_ = new Success();
failure_ = new Failure();
object_structure_->Attach(man_);
object_structure_->Attach(woman_);
//-----------------//
object_structure_->Display(success_);
cout << "------------------" << endl;
object_structure_->Display(failure_);
cout << "------------------" << endl;
//-----------------//
delete man_;
delete woman_;
delete object_structure_;
delete success_;
delete failure_;
//-----------------//
return 0;
}
//------------------------------//
打印输出
6. 应用场景
如果你需要对一个复杂对象结构(例如对象树)中的所有元素执行某些操作,可使用访问者模式。
访问者模式通过在访问者对象中为多个目标类提供相同操作的变体,让你能在属于不同类的一组对象上执行同一操作。
可使用访问者模式来清理辅助行为的业务逻辑。
该模式会将所有非主要的行为抽取到一组访问者类中,使得程序的主要类能更专注于主要的工作。
当某个行为仅在类层次结构中的一些类中有意义,而在其他类中没有意义时,可使用该模式。
你可将该行为抽取到单独的访问者类中,只需实现接收相关类的对象作为参数的访问者方法并将其他方法留空即可。
7. 优缺点
√ 开闭原则。你可以引入在不同类对象上执行的新行为,且无需对这些类做出修改
√ 单一职责原则。可将同一行为的不同版本移到同一个类中。
√ 访问者对象可以在与各种对象交互时收集一些有用的信息。当你想要遍历一些复杂的对象结构(例如对象树),并在结构中的每个对象上应用访问者时,这些信息可能会有所帮助。
× 每次在元素层次结构中添加或移除一个类时,你都要更新所有的访问者。
× 在访问者同某个元素进行交互时,它们可能没有访问元素私有成员变量和方法的必要权限。
8. 与其他模式的关系
• 你可以将访问者视为命令模式的加强版本,其对象可对不同类的多种对象执行操作。
• 你可以使用访问者对整个组合树执行操作。
• 可以同时使用访问者和迭代器来遍历复杂数据结构,并对其中的元素执行所需操作,即使这些元素所属的类完全不同。