Visitor模式包含两个主要的对象:Visitable对象和Vistor对象。此外,作为将被操作的对象,在Visitor模式中也包含Visited对象。
一个Visitable对象,即管理者,可能包含一系列形态各异的元素(Visited),它们可能在Visitable中具有复杂的结构关系(但也可以是某种单纯的容纳关系,如一个简单的vector)。Visitable一般会是一个复杂的容器,负责解释这些关系,并以一种标准的逻辑遍历这些元素。当Visitable对这些元素进行遍历时,它会将每个元素提供给Visitor令其能够访问该Visited元素。
这样一种编程模式就是VisitorPattern。
为了能够观察每个元素,因此实际上必然会有一个约束:所有的可被观察的元素具有共同的基类Visited。
所有的Visitors必须派生于Visitor才能提供给Visitable.accept(visitor&)接口。
namespacehicc::util{structbase_visitor{virtual~base_visitor(){}};structbase_visitable{virtual~base_visitable(){}};templateclassvisitor:publicbase_visitor{public:usingreturn_t=ReturnType;usingvisited_t=std::unique_ptr;virtualreturn_tvisit(visited_tconst&visited)=0;};templateclassvisitable:publicbase_visitable{public:virtual~visitable(){}usingreturn_t=ReturnType;usingvisitor_t=visitor;virtualreturn_taccept(visitor_t&guest)=0;};}//namespacehicc::util场景以一个实例来说,假设我们正在设计一套矢量图编辑器,在画布(Canvas)中,可以有很多图层(Layer),每一图层包含一定的属性(例如填充色,透明度),并且可以有多种图元(Element)。图元可以是Point,Line,Rect,Arc等等。
为了能够将画布绘制在屏幕上,我们可以有一个Screen设备对象,它实现了Visitor接口,因此画布可以接受Screen的访问,从而将画布中的图元绘制到屏幕上。
如果我们提供Printer作为观察者,那么画布将能够把图元打印出来。
如果我们提供Document作为观察者,那么画布将能够把图元特性序列化到一个磁盘文件中去。
如果今后需要其它的行为,我们可以继续增加新的观察者,然后对画布及其所拥有的图元进行类似的操作。
对于结构层级复杂的情况,要善于使用对象嵌套与递归能力,避免反复编写相似逻辑。
我们以矢量图编辑器的一部分为示例进行实现,采用了前面给出的基础类模板。
出于简化的理由基础图元没有进行层次化,而是平行地派生于drawable。
namespacehicc::dp::visitor::basic{structgroup:publicdrawable,publichicc::util::visitable{MAKE_DRAWABLE(group)usingdrawable_t=std::unique_ptr;usingdrawables_t=std::unordered_map;drawables_tdrawables;voidadd(drawable_t&&t){drawables.emplace(t->id(),std::move(t));}return_taccept(visitor_t&guest)override{for(autoconst&[did,dr]:drawables){guest.visit(dr);UNUSED(did);}}};structlayer:publicgroup{MAKE_DRAWABLE(layer)//more:attrs,...};}在groupclass中已经实现了visitable接口,它的accept能够接受访问者的访问,此时图元组group会遍历自己的所有图元并提供给访问者。
默认时guest会访问visitedconst&形式的图元,也就是只读方式。
图层至少具有group的全部能力,所以面对访问者它的做法是相同的。图层的属性部分(mask,overlay等等)被略过了。
画布包含了若干图层,所以它同样应该实现visitable接口:
namespacehicc::dp::visitor::basic{structcanvas:publichicc::util::visitable{usinglayer_t=std::unique_ptr;usinglayers_t=std::unordered_map;layers_tlayers;voidadd(draw_idid){layers.emplace(id,std::make_unique(id));}layer_t&get(draw_idid){returnlayers[id];}layer_t&operator[](draw_idid){returnlayers[id];}virtualreturn_taccept(visitor_t&guest)override{//hicc_debug("[canva]visitingfor:%s",to_string(guest).c_str());for(autoconst&[lid,ly]:layers){ly->accept(guest);}return;}};}其中,add将会以默认参数创建一个新图层,图层顺序遵循向上叠加方式。get和[]运算符能够通过正整数下标访问某一个图层。但是代码中没有包含图层顺序的管理功能,如果有意,你可以添加一个std::vector的辅助结构来帮助管理图层顺序。
现在我们来回顾画布-图层-图元体系,accept接口成功地贯穿了整个体系。
是时候建立访问者们了
这两者实现了简单的访问者接口:
namespacehicc::dp::visitor::basic{structscreen:publichicc::util::visitor{return_tvisit(visited_tconst&visited)override{hicc_debug("[screen][draw]for:%s",to_string(visited.get()).c_str());}friendstd::ostream&operator<<(std::ostream&os,screenconst&){returnos<<"[screen]";}};structprinter:publichicc::util::visitor{return_tvisit(visited_tconst&visited)override{hicc_debug("[printer][draw]for:%s",to_string(visited.get()).c_str());}friendstd::ostream&operator<<(std::ostream&os,printerconst&){returnos<<"[printer]";}};}hicc::to_string是一个简易的串流包装,它做如下的核心逻辑:
templateinlinestd::stringto_string(Tconst&t){std::stringstreamss;ss<voidtest_visitor_basic(){usingnamespacehicc::dp::visitor::basic;canvasc;staticdraw_idid=0,did=0;c.add(++id);//addedonegraph-layerc[1]->add(std::make_unique(++did));c[1]->add(std::make_unique(++did));c[1]->add(std::make_unique(++did));screenscr;c.accept(scr);}输出结果应该类似于这样:
---BEGINOFtest_visitor_basic----------------------09/14/2100:33:31[debug]:[screen][draw]for:09/14/2100:33:31[debug]:[screen][draw]for:09/14/2100:33:31[debug]:[screen][draw]for: