设计模式:访客
介绍多态需要一个类层次结构,其中层次结构的接口在基类中。 虚函数允许派生类覆盖基类函数。 使用多态的应用程序通常具有将基类指针或引用作为参数的函数。 然后创建派生对象,并将其用作这些函数的参数。 在函数内部,只能调用派生对象的基类方法。
这需要一个
同构层次 。 也就是说,基类和派生类都具有相同的方法。但是,在现实世界中,很少发生基类和所有派生类(包括那些直到几年后才知道的派生类)都具有相同的方法的情况。
更糟糕的是,可能会临时需要某种方法。 就像“汽车”类别的成本方法一样,该汽车只需要计算汽车销售当年的销售税,作为一次分析的一部分。 更糟糕的是,销售年份和税表不在“汽车”类别中。
然后,有一个新的要求,即方法不是内置在原始层次结构中的。 也许是Serialize()方法,用于将对象移入和移出数据库,由于格式不兼容,该方法不能使用派生类的operator <<插入器。
将所有这些放在一起,想象一下已经有成千上万的客户群,您就会开始了解正在运行的C ++应用程序。
像上面概述的那样,需要进行更改。 但是在制作它们时,不能更改原始类,因为a)更改并不适用于所有类,b)更改是暂时的,c)无法重新安装客户基础d)任何新功能只能具有基类指针或引用参数,以与应用程序的其余部分兼容。
死角是动态铸造。
动态转换(RTTI)是一个较差的解决方案,因为RTTI本身很昂贵,并且需要安装包含派生类名的代码。 随着添加新的派生类,或者随着类的消失,需要更改此代码,并且这需要重新安装客户群,这是不允许的。
访客设计模式解决了这些问题。
实施访客访客必须包含在原始层次结构设计中。 原始层次结构的基类需要一个Accept()方法,该方法接受指向VehicleVisitor基类的指针或引用。
以下是具有VehicleVisitor的Accept()方法的基类Vehicle。 由于Vehicle :: Accept()方法是基类接口的一部分,因此它不是虚拟的。 这样做是为了将接口与实现分开。 也就是说,派生类可以覆盖Vehicle :: Accept()方法处理的一部分或全部,但派生类不能覆盖基类方法本身,并且可以完全执行其他操作。
class VehicleVisitor; //forward reference
class Vehicle
{
private:
string owner;
public:
Vehicle(string in);
void Accept(VehicleVisitor* v);
};
而是将Vehicle :: Accept()实现为hook 。
钩子是一种无能为力的方法。
也就是说,派生类可以重写Accept()以获取VehicleVisitor指针或引用,但并非必须这样做。
这使派生类摆脱了不仅仅满足C ++的方法。
在下面的示例中,这是使用私有Vehicle :: DoAccept()方法完成的。 这是挂钩方法。 如果派生类未覆盖Vehicle :: DoAccept(),则Vehicle :: DoAccept()将不做任何事情而返回。
否则,派生类可以实现Derived :: DoAccept()并获取VehicleVisitor指针或引用。
class VehicleVisitor; //forward reference
class Vehicle
{
private:
string owner;
virtual void DoAccept(VehicleVisitor* v);
public:
Vehicle(string in);
string GetOwner();
void Accept(VehicleVisitor* v);
};
Vehicle::Vehicle(string in) : owner(in)
{
}
string Vehicle::GetOwner()
{
return this->owner;
}
void Vehicle::Accept(VehicleVisitor* v)
{
this->DoAccept(v);
}
//Hook method. Not required to be overriden
void Vehicle::DoAccept(VehicleVisitor* v)
{
}
现在可以从Vehicle派生Automobile类。
在此示例中,汽车除拥有者外还具有许可证编号。
此类将重写Vehicle :: DoAccept()以获取VehicleVisitor指针。
class Automobile : public Vehicle
{
private:
void DoAccept(VehicleVisitor* v);
string LicenseNumber;
public:
Automobile(string owner, string license);
string GetLicenseNumber();
};
Automobile::Automobile(string owner, string license)
: Vehicle(owner), LicenseNumber(license)
{
}
string Automobile::GetLicenseNumber()
{
return LicenseNumber;
}
此时,需要定义VehicleVisitor层次结构。
此层次结构将与Vehicle层次结构平行。
如果存在Vehicle基类,则将有VehicleVisitor基类。
如果有汽车类的派生类,将有汽车汽车类的派生类,等等。
并非所有VehicleVisitor对象都需要VisitVimobile()方法,因此该方法也实现为钩子。 如果不被覆盖,它什么也不做。
class VehicleVisitor
{
public:
virtual void VisitAutomobile(Automobile* c);
protected:
VehicleVisitor(); //only derived classes can create objects
};
VehicleVisitor::VehicleVisitor()
{
}
//Hook method. Need not be implemented by all derived classes
void VehicleVisitor::VisitAutomobile(Automobile* c)
{
}
由于已知VehicleVisitor具有VisitAutomobile()方法,因此可以实现Automobile :: DoAccept()。
下面的this指针是Automobile *,因为这是Automobile方法。
这是模式的核心。 正在使用派生的类指针对VehicleVisitor方法进行调用。 这意味着VehicleVisitor :: VisitAutomobile具有指向派生对象的指针。 即,汽车*。
void Automobile::DoAccept(VehicleVisitor* v)
{
v->VisitAutomobile(this);
}
剩下的就是从VehicleVisitor派生AutomobileVisitor并实现挂钩方法VisitAutomobile()。
此类显示如下。
VisitAutomobile()方法保存将Automobile *用作参数。 AutomobileVisitor可以使用此指针来调用Automobile上的方法。
其他AutomobileVisitor方法是Automobile方法的包装。 他们使用VisitAutomobile获得的指针来调用Automobile对象上的相应方法。
除了包装器Automobile方法之外,您还可以向AutomobileVisitor添加其他方法(和数据)。 这有效地扩展了汽车类,而无需更改汽车类。
class AutomobileVisitor :public VehicleVisitor
{
public:
void VisitAutomobile(Automobile* c);
//Use the Car interface
string GetLicenseNumber();
//Use the Vehicle Interface
string GetOwner();
private:
//The Automobile last visited;
Automobile* theAutomobile;
};
void AutomobileVisitor::VisitAutomobile(Automobile*a)
{
this->theAutomobile = a; //save the Automobile*
}
string AutomobileVisitor::GetLicenseNumber()
{
return this->theAutomobile->GetLicenseNumber();
}
string AutomobileVisitor::GetOwner()
{
return this->theAutomobile->GetOwner();
}
下面是一个小型驱动程序。
在此,将创建一个汽车对象并将其保留为汽车*。
接下来,创建一个AutomobileVisitor,并将其地址用于Vehicle :: Accept()。
回想一下Vehicle :: Accept()调用Vehicle :: DoAccept(),并且此函数被Automobile :: DoAccept()覆盖,因此真正调用的是Automobile :: DoAccept()。
反过来,Automobile :: DoAccept()调用VehicleVisitor :: VisitAutomobile()(已被AutomobileVisitor :: VisitAutomobile()覆盖),并且真正调用了此函数。
最终结果是AutomobileVisitor对象已获取Automobile对象的地址。 然后,它将使用该地址来调用Automobile方法。 删除了在车辆中也声明汽车方法的要求。
在适当的情况下,汽车可以作为汽车参加,否则可以作为汽车参加。 需要添加方法的地方,只需使用添加的方法创建一个AutomobileVisitor对象,并使用其地址来调用Vehicle :: Accept()。 然后将AutomobileVisitor用作扩展的Automobile。
请特别注意,main()中仅使用了Vehicle *,并且此模式中的任何地方都没有类型转换或RTTI。
int main()
{
Vehicle* ptr = new Automobile("John Smith", "XYZ-123");
AutomobileVisitor* v = new AutomobileVisitor;
ptr->Accept(v);
cout << v->GetOwner() << endl; //from Vehicle
cout << v->GetLicenseNumber() << endl; //from Automobile
}
使用访问者向类添加方法
想象一下,已经要求将车辆序列化为光盘。 Vehicle上没有Serialize()方法。 因为文件格式与显示格式不同,所以需要一种方法来代替<< <<插入器。 在这种情况下,汽车需要以文本形式写入磁盘,并且每个数据成员都在其自己的记录中。 这意味着必须同时写出所有者和许可证号。
使用上面的代码示例,可以将AutomobileArchive类编写为具有Serialize方法的AutomobileVisitor。
class AutomobileArchive :public VehicleVisitor
{
public:
AutomobileArchive(string filename);
void VisitAutomobile(Automobile* c);
//Use the Car interface
string GetLicenseNumber();
//Use the Vehicle Interface
string GetOwner();
//Added methids for Automobile
void Serialize();
fstream& GetArchive();
private:
//The Automobile last visited;
Automobile* theAutomobile;
fstream archive;
};
AutomobileArchive::AutomobileArchive(string filename)
{
archive.open(filename.c_str(), ios_base::out | ios_base::app);
}
void AutomobileArchive::VisitAutomobile(Automobile*a)
{
this->theAutomobile = a;
}
string AutomobileArchive::GetLicenseNumber()
{
return this->theAutomobile->GetLicenseNumber();
}
string AutomobileArchive::GetOwner()
{
return this->theAutomobile->GetOwner();
}
void AutomobileArchive::Serialize()
{
string temp = this->GetOwner();
temp += '\n';
archive.write(temp.c_str(), temp.size());
temp = this->GetLicenseNumber();
temp += '\n';
archive.write(temp.c_str(), temp.size());
}
上面的AutomobileArchive是VehicleVisitor。
添加了一个构造函数以以附加模式打开光盘文件。
添加了Serialize()方法,以从Automobile对象获得所有者和许可证号,并在每一个后面附加一个\ n,然后将两个字符串写入光盘。
驱动程序已扩展,可以使用以下新的访问者:
int main()
{
Vehicle* ptr = new Automobile("John Smith", "XYZ-123");
AutomobileVisitor* v = new AutomobileVisitor;
ptr->Accept(v);
cout << v->GetOwner() << endl; //from Vehicle
cout << v->GetLicenseNumber() << endl; //from Automobile
AutomobileArchive save("C:\\scratch\\instructor\\archive.txt");
ptr->Accept(&save);
save.Serialize();
}
如您所见,所需要做的就是创建AutomobileArchive对象并随其访问Vehicle *。
接受AutomobileVisitor后,可以使用Serialize方法()将Automobile保存到光盘。
在要保存许多车辆对象的地方,必须先检查每个对象,然后将它们序列化到光盘上。
上面的代码将需要针对各种车辆进行复制。
本示例仅出于概念目的。 不同类型车辆的序列化细节已被省略。
使用手柄由于指针语法是众所周知的,因此本文中的示例使用指针。 但是,建议在实际使用的应用程序中使用。 您应该参考“ C / C ++文章”部分中有关“句柄”的文章。
更多的信息请参阅Erich Fromm等人的《设计模式》一书,Addison-Wesley,1994年。
本文仅显示“访客”模式的概念基础,而不显示使用此模式的动机和后果。
版权所有2007 Buchmiller Technical Associates美国北华盛顿本德
From: https://bytes.com/topic/c/insights/674645-design-patterns-visitor