传统的访问者模式需要在访问者基类里面定义各个被访问类的处理函数,这样如果每增加一个被访问类就要修改所有基类和派生类。特别是有多个派生类的时候就更不方便了,因为很多派生类可能不需要访问对应的数据类型。此处的访问者模式则不存在这个问题,可以根据需要定义需要访问的类,就很棒。
#ifndef _SOME_FRAME_H_
#define _SOME_FRAME_H_
#include <functional>
#include <type_traits>
template<typename target_t, typename cur_type_t, typename...remains_types_t>
constexpr int tag_2_id = 1 + tag_2_id<target_t, remains_types_t...>;
template<typename target_t, typename...remains_types_t>
constexpr int tag_2_id<target_t, target_t, remains_types_t...> = 0;
template<int N, typename base_type_t, typename cur_derived_type, typename...remains_types_t>
inline int object_type_index_(base_type_t* p)
{
return typeid(*p) == typeid(cur_derived_type) ? N : object_type_index_<N + 1, base_type_t, remains_types_t...>(p);
}
template<int N, typename base_type_t>
inline int object_type_index_(base_type_t* p)
{
/* 匹配失败 */
return N;
}
template<typename K, typename...Ts>
inline int object_type_index(K* p)
{
return object_type_index_<0, K, Ts...>(p);
}
namespace some_frame
{
template<typename base_t>
class i_visitor
{
public:
virtual bool do_visit(base_t* ptr_base) = 0;
};
template<typename impl_t, typename base_t, typename... derived_types_t>
class visitor:public i_visitor<base_t>
{
protected:
template<typename derived_t>
void regist()
{
constexpr int i_idx = tag_2_id<derived_t, derived_types_t ...>;
static_assert(i_idx < sizeof...(derived_types_t), "regist越界");
m_callbacks[i_idx] = [impl = static_cast<impl_t*>(this)](base_t* ptr_base)
{
constexpr void(impl_t::*memberFunc)(derived_t*) = &impl_t::do_type_visit;
return (impl->*memberFunc)(static_cast<derived_t*>(ptr_base));
};
}
template<typename type_t, typename...rest_t>
void regist_all()
{
regist<type_t>();
if constexpr (sizeof...(rest_t) != 0)
{
regist_all<rest_t...>();
}
}
public:
visitor()
{
regist_all<derived_types_t...>();
}
bool do_visit(base_t* ptr_base)
{
auto i_idx = object_type_index<base_t, derived_types_t ...>(ptr_base);
if (i_idx >= sizeof...(derived_types_t))
{
/* 匹配失败 */
return false;
}
try {
m_callbacks[i_idx](ptr_base);
return true;
}
catch (std::bad_function_call& e)
{
/* 对应的回调没有注册 */
(void)e; // 解决4101未使用变量警告
return false;
}
return false;
}
private:
std::function<void(base_t*)> m_callbacks[sizeof...(derived_types_t)];
};
};
#endif
实现原理也并不复杂。首先接口类i_visitor只需要定义指定基类的do_visit方法,实现类visitor中使用object_type_index找到对应子类在子类列表中的索引,而成员m_callbacks中按照索引,保存了各子类对应实际调用方法的lambda函数,从而实现对各子类的调用。在抽象层,只需关注基类与基类的关系,无需关心子类,子类的处理完全由派生类实现。
试想有如下需求:有一些图形,这些图形后期可能需要增加。我们需要对这些图形进行一些操作。这些图形和操作方法可能会进行增加。操作和图形又上层框架代码进行调用,我们不知道什么时候哪个操作会作用在哪种图形上。
常规的解决方法:对于图形和操作分别写两个父类Figure和Action。由于Action要针对不同的Figure进行不同的处理,比较好的解决方案是,不定义具体的操作,在Figure中定义基础操作,在Action实现类中调用这些操作实现功能。但是有一个问题,不同图形的操作是不一样的,比如圆形你能设置半径,但是对于矩形你设置半径就没有意义了。因此就需要在Figure类中定义所有可能的基础操作。这样就又涉及到了底层具体的实现。
本文解决办法:定义一个图形父类Figure,其中包含一些公共的图形信息;定义操作父类i_visitor;操作父类i_visitor只传入一个Figure指针,并不知道Figure具体有哪些,Figure也只定义一个accept方法,也不知道子类会包含哪些基础操作。然后用模板生成各个操作类的父类并继承声明子类,在子类中针对各个图形进行具体处理。
具体的使用方法如下:
#include <stdlib.h>
class Figure
{
public:
void accept(some_frame::i_visitor<Figure>* p)
{
p->do_visit(this);
}
virtual ~Figure()
{}
};
class Sphere:public Figure{};
class Polygon:public Figure{};
class Drawer:public some_frame::visitor<Drawer, Figure, Sphere, Polygon>
{
public:
Drawer() {
regist<Sphere>();
regist<Polygon>();
}
void do_type_visit(Sphere*)
{
printf("do_type_visit(Sphere*)\r\n");
}
void do_type_visit(Polygon*)
{
printf("do_type_visit(Polygon*)\r\n");
}
};
class Print :public some_frame::visitor<Print, Figure, Polygon>
{
public:
Print()
{
regist<Polygon>();
}
void do_type_visit(Polygon*)
{
printf("%s(Polygon*)\r\n", __FUNCTION__);
}
};
#include <conio.h>
int main()
{
Figure* pf = nullptr;
Sphere sp;
pf = &sp;
Drawer dr;
Print pt;
some_frame::i_visitor<Figure>* pv = (&dr);
pf->accept(pv);
pv = (&pt);
pf->accept(pv); // 匹配失败,pt不支持Sphere操作
Polygon poly;
pf = &poly;
pv = (&dr);
pf->accept(pv);
pv = (&pt);
pf->accept(pv);
_getch();
return 0;
}
可见,Drawer方法实现了Sphere和Polygon具体图形对象的实现,而父类完全可以在不知道子类具体类别的情况下进行正确处理。该访问者模式的有点在于访问者可以直接写具体被访问派生对象的处理方法而不用管如何判断派生类类别。从而,在上层屏蔽了底层实现,真正实现了类型解耦。如果新增被访问对象,只需要修改具体的实现类(如Drawer)增加对应的处理单元即可。
直白地说,这个实现方法只是在每个visitor父类中为自己需要处理的Figure子类定义序号,当传入的Figure进来之后,Figure的具体实现类会把具体的类型传给visitor父类,visitor根据其Figure子类的具体类型找到其处理的lambda函数,然后再lambda函数中调用实现类的实现方法。