共计 6261 个字符,预计需要花费 16 分钟才能阅读完成。
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 模式有时候可能被迭代器模式所代替。然而迭代器经常会有一个致命缺点而影响了其实用性:迭代器自身可能是僵化的、高代价的、效率低下的——除非你做出了最失当的设计时抉择并实现了最精美的迭代器。它们两者都容许用户无侵入地拜访一个已知的简单容器的内容。