Visitor Pattern
访问者模式是一种行为模式,容许任意的拆散的访问者可能在管理者管制下拜访所治理的元素。访问者不能扭转对象的定义(但这并不是强制性的,你能够约定为容许扭转)。对管理者而言,它不关怀到底有多少访问者,它只关怀一个确定的元素拜访程序(例如对于二叉树来说,你能够提供中序、前序等多种拜访程序)。
组成
Visitor 模式蕴含两个次要的对象:Visitable 对象和 Vistor 对象。此外,作为将被操作的对象,在 Visitor 模式中也蕴含 Visited 对象。
一个 Visitable 对象,即管理者,可能蕴含一系列形态各异的元素(Visited),它们可能在 Visitable 中具备简单的构造关系(但也能够是某种单纯的包容关系,如一个简略的 vector)。Visitable 个别会是一个简单的容器,负责解释这些关系,并以一种规范的逻辑遍历这些元素。当 Visitable 对这些元素进行遍历时,它会将每个元素提供给 Visitor 令其可能拜访该 Visited 元素。
这样一种编程模式就是 Visitor Pattern。
接口
为了可能察看每个元素,因而实际上必然会有一个束缚:所有的可被察看的元素具备独特的基类 Visited。
所有的 Visitors 必须派生于 Visitor 能力提供给 Visitable.accept(visitor&) 接口。
namespace hicc::util { struct base_visitor { virtual ~base_visitor() {} }; struct base_visitable { virtual ~base_visitable() {} }; template<typename Visited, typename ReturnType = void> class visitor : public base_visitor { public: using return_t = ReturnType; using visited_t = std::unique_ptr<Visited>; virtual return_t visit(visited_t const &visited) = 0; }; template<typename Visited, typename ReturnType = void> class visitable : public base_visitable { public: virtual ~visitable() {} using return_t = ReturnType; using visitor_t = visitor<Visited, return_t>; virtual return_t accept(visitor_t &guest) = 0; };} // namespace hicc::util
场景
以一个实例来说,假如咱们正在设计一套矢量图编辑器,在画布(Canvas)中,能够有很多图层(Layer),每一图层蕴含肯定的属性(例如填充色,透明度),并且能够有多种图元(Element)。图元能够是 Point,Line,Rect,Arc 等等。
为了可能将画布绘制在屏幕上,咱们能够有一个 Screen 设施对象,它实现了 Visitor 接口,因而画布能够承受 Screen 的拜访,从而将画布中的图元绘制到屏幕上。
如果咱们提供 Printer 作为观察者 ,那么画布将可能把图元打印进去。
如果咱们提供 Document 作为观察者,那么画布将可能把图元个性序列化到一个磁盘文件中去。
如果今后须要其它的行为,咱们能够持续减少新的观察者,而后对画布及其所领有的图元进行相似的操作。
特点
- 如果你须要对一个简单对象构造 (例如对象树) 中的所有元素执行某些操作, 可应用访问者模式。
- 访问者模式将非次要的性能从对象管理者身上抽离,所以它也是一种解耦伎俩。
- 如果你正在制作一个对象库的类库,那么向外提供一个拜访接口,将会有利于用户无侵入地开发本人的 visitor 来拜访你的类库——他不用为了本人的一点点事件就给你 issue/pull request。
对于构造层级简单的状况,要长于应用对象嵌套与递归能力,防止重复编写类似逻辑。
请查阅 canva,layer,group 的参考实现,它们通过实现
drawable
和vistiable<drawable>
的形式实现了嵌套性的自我管理能力,并使得accept()
可能递归地进入每一个容器中。
实现
咱们以矢量图编辑器的一部分为示例进行实现,采纳了后面给出的根底类模板。
drawable 和 根底图元
首先做 drawable/shape 的根本申明以及根底图元:
namespace hicc::dp::visitor::basic { using draw_id = std::size_t; /** @brief a shape such as a dot, a line, a rectangle, and so on. */ struct drawable { virtual ~drawable() {} friend std::ostream &operator<<(std::ostream &os, drawable const *o) { return os << '<' << o->type_name() << '#' << o->id() << '>'; } virtual std::string type_name() const = 0; draw_id id() const { return _id; } void id(draw_id id_) { _id = id_; } private: draw_id _id; }; #define MAKE_DRAWABLE(T) \ T(draw_id id_) { id(id_); } \ T() {} \ virtual ~T() {} \ std::string type_name() const override { \ return std::string{hicc::debug::type_name<T>()}; \ } \ friend std::ostream &operator<<(std::ostream &os, T const &o) { \ return os << '<' << o.type_name() << '#' << o.id() << '>'; \ } //@formatter:off struct point : public drawable {MAKE_DRAWABLE(point)}; struct line : public drawable {MAKE_DRAWABLE(line)}; struct rect : public drawable {MAKE_DRAWABLE(rect)}; struct ellipse : public drawable {MAKE_DRAWABLE(ellipse)}; struct arc : public drawable {MAKE_DRAWABLE(arc)}; struct triangle : public drawable {MAKE_DRAWABLE(triangle)}; struct star : public drawable {MAKE_DRAWABLE(star)}; struct polygon : public drawable {MAKE_DRAWABLE(polygon)}; struct text : public drawable {MAKE_DRAWABLE(text)}; //@formatter:on // note: dot, rect (line, rect, ellipse, arc, text), poly (triangle, star, polygon)}
为了调试目标,咱们重载了 '<<' 流输入运算符,而且利用宏 MAKE_DRAWABLE 来削减重复性代码的键击输出。在 MAKE_DRAWABLE 宏中,咱们通过 hicc::debug::type_name<T>()
来取得类名,并将此作为字符串从 drawable::type_name()
返回。
出于简化的理由根底图元没有进行层次化,而是平行地派生于 drawable。
复合性图元和图层
上面申明 group 对象,这种对象蕴含一组图元。因为咱们想要尽可能多的递归结构,所以图层也被认为是一种一组图元的组合模式:
namespace hicc::dp::visitor::basic { struct group : public drawable , public hicc::util::visitable<drawable> { MAKE_DRAWABLE(group) using drawable_t = std::unique_ptr<drawable>; using drawables_t = std::unordered_map<draw_id, drawable_t>; drawables_t drawables; void add(drawable_t &&t) { drawables.emplace(t->id(), std::move(t)); } return_t accept(visitor_t &guest) override { for (auto const &[did, dr] : drawables) { guest.visit(dr); UNUSED(did); } } }; struct layer : public group { MAKE_DRAWABLE(layer) // more: attrs, ... };}
在 group class 中曾经实现了 visitable 接口,它的 accept 可能承受访问者的拜访,此时 图元组 group 会遍历本人的所有图元并提供给访问者。
你还能够基于 group class 创立 compound 图元类型,它容许将若干图元组合成一个新的图元元件,两者的区别在于,group 个别是 UI 操作中的临时性对象,而 compound 图元可能作为元件库中的一员供用户筛选和应用。
默认时 guest 会拜访 visited const &
模式的图元,也就是只读形式。
图层至多具备 group 的全副能力,所以面对访问者它的做法是雷同的。图层的属性局部(mask,overlay 等等)被略过了。
画布 Canvas
画布蕴含了若干图层,所以它同样应该实现 visitable 接口:
namespace hicc::dp::visitor::basic { struct canvas : public hicc::util::visitable<drawable> { using layer_t = std::unique_ptr<layer>; using layers_t = std::unordered_map<draw_id, layer_t>; layers_t layers; void add(draw_id id) { layers.emplace(id, std::make_unique<layer>(id)); } layer_t &get(draw_id id) { return layers[id]; } layer_t &operator[](draw_id id) { return layers[id]; } virtual return_t accept(visitor_t &guest) override { // hicc_debug("[canva] visiting for: %s", to_string(guest).c_str()); for (auto const &[lid, ly] : layers) { ly->accept(guest); } return; } };}
其中,add 将会以默认参数创立一个新图层,图层程序遵循向上叠加形式。get 和 []
运算符可能通过正整数下标拜访某一个图层。然而代码中没有蕴含图层程序的治理性能,如果无意,你能够增加一个 std::vector<draw_id>
的辅助构造来帮忙治理图层程序。
当初咱们来回顾画布-图层-图元体系,accept 接口胜利地贯通了整个体系。
是时候建设访问者们了
screen 或 printer
这两者实现了简略的访问者接口:
namespace hicc::dp::visitor::basic { struct screen : public hicc::util::visitor<drawable> { return_t visit(visited_t const &visited) override { hicc_debug("[screen][draw] for: %s", to_string(visited.get()).c_str()); } friend std::ostream &operator<<(std::ostream &os, screen const &) { return os << "[screen] "; } }; struct printer : public hicc::util::visitor<drawable> { return_t visit(visited_t const &visited) override { hicc_debug("[printer][draw] for: %s", to_string(visited.get()).c_str()); } friend std::ostream &operator<<(std::ostream &os, printer const &) { return os << "[printer] "; } };}
hicc::to_string
是一个繁难的串流包装,它做如下的外围逻辑:
template<typename T>inline std::string to_string(T const &t) { std::stringstream ss; ss << t; return ss.str();}
test case
测试程序结构了微型的画布以及几个图元,而后示意性地拜访它们:
void test_visitor_basic() { using namespace hicc::dp::visitor::basic; canvas c; static draw_id id = 0, did = 0; c.add(++id); // added one graph-layer c[1]->add(std::make_unique<line>(++did)); c[1]->add(std::make_unique<line>(++did)); c[1]->add(std::make_unique<rect>(++did)); screen scr; c.accept(scr);}
输入后果应该相似于这样:
--- BEGIN OF test_visitor_basic ----------------------09/14/21 00:33:31 [debug]: [screen][draw] for: <hicc::dp::visitor::basic::rect#3>09/14/21 00:33:31 [debug]: [screen][draw] for: <hicc::dp::visitor::basic::line#2>09/14/21 00:33:31 [debug]: [screen][draw] for: <hicc::dp::visitor::basic::line#1--- END OF test_visitor_basic ----------------------It took 2.813.753ms
Epilogue
Visitor 模式有时候可能被迭代器模式所代替。然而迭代器经常会有一个致命缺点而影响了其实用性:迭代器自身可能是僵化的、高代价的、效率低下的——除非你做出了最失当的设计时抉择并实现了最精美的迭代器。 它们两者都容许用户无侵入地拜访一个已知的简单容器的内容。