关于c++:谈-C17-里的-Visitor-模式

24次阅读

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

正文完
 0