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