26. 访问者模式
一、访问者模式
访问者模式(Visitor Pattern)是一种将数据操作与数据结构分离的设计模式,可以在不修改数据结构的前提下增加新的操作。访问者模式的核心思想是将数据结构中的元素操作分离出来,封装在单独的访问者对象中。这样,当需要添加新的操作时,只需要创建新的访问者类,而无需修改原有的数据结构。这种设计使得操作集合可以相对自由地演化,从而提高了系统的灵活性和可扩展性。
访问者模式包含访问者和被访问元素两个主要组成部分。这些被访问的元素通常具有不同的类型,且不同的访问者可以对它们进行不同的访问操作。在使用访问者模式时,被访问的元素通常不是单独存在的,它们存储在一个集合中,这个集合称为 “对象结构”。访问者通过遍历对象结构实现对其中存储的元素的逐个操作。
访问者模式的主要角色如下:
- 抽象访问者(Visitor):抽象访问者为对象结构中每个具体元素类声明一个访问操作,从这个操作的名称或参数类型可以清楚知道需要访问的具体元素的类型。具体访问者需要实现这些操作方法,提供对这些元素的访问操作。
- 具体访问者(Concrete Visitor):具体访问者实现了每个由抽象访问者声明的操作,每个操作用于访问对象结构中一种类型的元素。
- 抽象元素(Element):抽象元素一般是抽象类或者接口,它定义一个 accept() 方法,该方法通常以一个抽象访问者作为参数。
- 具体元素(Concrete Element):具体元素实现了 accept() 方法,在 accept() 方法中调用访问者的访问方法以便完成对一个元素的操作。
- 对象结构(Object Structure):对象结构是一个元素的集合,它用于存放元素对象,并且提供了遍历其内部元素的方法。它可以结合组合模式来实现,也可以是一个简单的集合对象。
访问者模式中对象结构存储了不同类型的元素对象,以供不同访问者访问。访问者模式包括两个层次结构:一个是 访问者层次结构,提供了抽象访问者和具体访问者;另一个是 元素层次结构,提供了抽象元素和具体元素。相同的访问者可以以不同的方式访问不同的元素,相同的元素可以接受不同访问者以不同访问方式访问。在访问者模式中,增加新的访问者无须修改原有系统,系统具有较好的可扩展性。
总的来说,访问者模式是一种强大的设计模式,它可以在不修改数据结构的前提下增加新的操作,从而提高系统的灵活性和可扩展性。然而,在使用时需要根据具体场景和需求进行权衡和选择。
二、C++实现访问者模式
在访问者模式中,抽象访问者定义了访问元素对象的方法。通常为每一种类型的元素对象都提供一个访问方法,而具体访问者可以实现这些访问方法。这些访问方法的命名一般有两种方式:一种是直接在方法名中标明待访问元素对象的具体类型。另一种是给访问方法统一取名,通过参数类型的不同来定义一系列重载的访问方法。当然,如果所有的访问者对某一类型的元素的访问操作都相同,则可以将操作代码移到抽象访问者类中。
抽象访问者为对象结构中每个具体元素类声明一个访问操作。
// 访问者角色
class Person
{
private:
std::string name;
public:
Person(std::string name);
virtual void feed(Cat * cat) = 0;
virtual void feed(Dog * dog) = 0;
std::string getName(void);
void setName(std::string name);
};
Person::Person(std::string name) : name(name) {}
std::string Person::getName(void)
{
return name;
}
void Person::setName(std::string name)
{
this->name = name;
}
具体访问者实现了每个由抽象访问者声明的操作,每个操作用于访问对象结构中一种类型的元素。
// 具体访问者角色
class Owner : public Person
{
public:
using Person::Person;
void feed(Cat * cat) override;
void feed(Dog * dog) override;
};
void Owner::feed(Cat * cat)
{
std::cout << "主人给小猫【" << cat->getName() << "】喂食" << std::endl;
}
void Owner::feed(Dog * dog)
{
std::cout << "主人给小狗【" << dog->getName() << "】喂食" << std::endl;
}
// 具体访问者角色
class Someone : public Person
{
public:
using Person::Person;
void feed(Cat * cat) override;
void feed(Dog * dog) override;
};
void Someone::feed(Cat * cat)
{
std::cout << "路人给小猫【" << cat->getName() << "】喂食" << std::endl;
}
void Someone::feed(Dog * dog)
{
std::cout << "路人给小狗【" << dog->getName() << "】喂食" << std::endl;
}
对于元素类而言,在其中一般都定义了一个 accept() 方法,用于接受访问者的访问。需要注意的是,该方法传入了一个抽象访问者类型的参数,即针对抽象访问者进行编程,而不是具体访问者。在程序运行时再确定具体访问者的类型,并调用具体访问者对象的访问方法实现对元素对象的操作。
// 抽象元素角色
class Animal
{
private:
std::string name;
public:
Animal(std::string name);
// 接收访问者访问的功能
virtual void accept(Person * person) = 0;
std::string getName(void);
void setName(std::string name);
};
Animal::Animal(std::string name) : name(name) {}
std::string Animal::getName(void)
{
return name;
}
void Animal::setName(std::string name)
{
this->name = name;
}
在抽象元素类的子类中实现了 accept() 方法,用于接受访问者的访问。
// 具体元素
class Cat : public Animal
{
public:
using Animal::Animal;
void accept(Person * person) override;
};
void Cat::accept(Person * person)
{
person->feed(this); // 访问者给猫喂食
std::cout << person->getName() << "给小猫【" << getName() << "】喂食成功" << std::endl;
}
// 具体元素
class Dog : public Animal
{
public:
using Animal::Animal;
void accept(Person * person) override;
};
void Dog::accept(Person * person)
{
person->feed(this);
std::cout << person->getName() << "给小狗【" << getName() << "】喂食成功" << std::endl;
}
在具体元素类中还可以定义不同类型的元素所特有的业务方法。在具体元素类的 accept() 方法中,通过调用抽象访问者类的访问方法实现对元素的访问,并以当前对象作为 visit() 方法的参数。其具体的执行流程如下:
- 调用具体元素类的 accept(Visitor visitor) 方法,并将 Visitor 子类对象作为其参数。
- 在具体元素类 accept(Visitor visitor) 方法内部调用传入的 Visitor 对象的访问方法。
- 执行 Visitor 对象的访问方法,在其中还可以调用具体元素对象的业务方法。
这种调用机制也称为 “双重分派”。正因为使用了双重分派机制,使得增加新的访问者无须修改现有类库代码,只需将新的访问者对象作为参数传入具体元素对象的 accept() 方法。程序运行时将回调在新增 Visitor 类中定义的访问方法,从而增加新的元素访问方式。
对象结构是一个元素的集合,它用于存放元素对象,并且提供了遍历其内部元素的方法。
// 结构对象角色
class House
{
private:
std::vector<Animal *> animals; // 声明一个集合对象,用来存储元素对象
public:
void addAnimal(Animal * animal);
void action(Person * person);
};
void House::addAnimal(Animal * animal)
{
animals.push_back(animal);
}
void House::action(Person * person)
{
// 遍历集合对象,获取每一个元素,让访问者访问每一个元素
for (auto animal : animals)
{
animal->accept(person);
}
std::cout << std::endl;
}
main() 函数:
int main(void)
{
House house;
Cat cat("汤姆");
Dog dog("斯派克");
// 添加元素对象到Home中
house.addAnimal(&cat);
house.addAnimal(&dog);
Owner owner("女主人");
house.action(&owner); // 让女主人喂食全部宠物
Someone someone("路人甲");
house.action(&someone); // 让路人喂食全部宠物
return 0;
}
如果要在系统中增加一种新的访问者,无须修改源代码,只要增加一个新的具体访问者类即可,在该具体访问者中封装了新的操作元素对象的方法。从增加新的访问者的角度来看,访问者模式符合开闭原则。
如果要在系统中增加一种新的具体元素,由于原有系统并未提供相应的访问接口,因此必须对原有系统进行修改,在原有的抽象访问者类和具体访问者类中增加相应的访问方法。从增加新的元素的角度来看,访问者模式违背了开闭原则。
综上所述,访问者模式与抽象工厂模式类似,对开闭原则的支持具有倾斜性,可以很方便地添加新的访问者,但是添加新的元素较为麻烦。
三、访问者模式的总结
3.1、访问者模式的优点
- 增加新的访问操作很方便。使用访问者模式,增加新的访问操作就意味着增加一个新的具体访问者类,实现简单,无须修改源代码,符合开闭原则。
- 将有关元素对象的访问行为集中到一个访问者对象中,而不是分散在一个个的元素类中。类的职责更加清晰,有利于对象结构中元素对象的复用,相同的对象结构可以供多个不同的访问者访问。
- 让用户能够在不修改现有元素类层次结构的情况下,定义作用于该层次结构的操作。
3.2、访问者模式的缺点
- 增加新的元素类很困难。在访问者模式中,每增加一个新的元素类都意味着要在抽象访问者角色中增加一个新的抽象操作,并在每一个具体访问者类中增加相应的具体操作,这违背了开闭原则的要求。
- 破坏封装。访问者模式要求访问者对象访问并调用每一个元素对象的操作,这意味着元素对象有时候必须暴露一些自己的内部操作和内部状态,否则无法供访问者访问。
3.3、访问者模式的适用场景
- 一个对象结构包含多种类型的对象,希望对这些对象实施一些依赖其具体类型的操作。在访问者中针对每一种具体的类型都提供了一个访问操作,不同类型的对象可以有不同的访问操作。
- 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而且需要避免让这些操作 “污染” 这些对象的类,也不希望在增加新操作时修改这些类。访问者模式将相关的访问操作集中起来定义在访问者类中,对象结构可以被多个不同的访问者类所使用,将对象本身与对象的访问操作分离。
- 对象结构中元素对象对应的类很少改变,但经常需要在此对象结构上定义新的操作。