头图

Visitor Pattern

The visitor pattern is a behavioral pattern that allows any separated visitor to access the managed elements under the control of the manager. The visitor cannot change the definition of the object (but it is not mandatory, you can agree to allow changes). For the manager, it does not care how many visitors there are, it only cares about a certain element access sequence (for example, for a binary tree, you can provide multiple access sequences such as middle order and preorder).

image-20210914091034034

composition

The Visitor pattern contains two main objects: Visitable objects and Vistor objects. In addition, as the object to be operated, the Visited object is also included in the Visitor mode.

A Visitable object, that is, a manager, may contain a series of elements with different shapes (Visited), and they may have a complex structural relationship in Visitable (but it can also be a simple containment relationship, such as a simple vector). Visitable will generally be a complex container responsible for explaining these relationships and traversing these elements with a standard logic. When Visitable traverses these elements, it will provide each element to the Visitor so that it can access the Visited element.

Such a programming model is the Visitor Pattern.

interface

In order to be able to observe each element, there must be a constraint in fact: all observable elements have a common base class Visited.

All Visitors must be derived from Visitor before they can be provided to the Visitable.accept(visitor&) interface.

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

Scenes

As an example, suppose we are designing a set of vector editor, in the canvas (Canvas), there can be many layers (Layer), each layer contains certain attributes (such as fill color, transparency), and There can be a variety of elements (Element). The primitive can be Point, Line, Rect, Arc, etc.

In order to be able to draw the canvas on the screen, we can have a Screen device object, which implements the Visitor interface, so the canvas can accept the access of the Screen, so that the primitives in the canvas can be drawn on the screen.

If we provide Printer as an observer, the canvas will be able to print out the primitives.

If we provide Document as the observer, the canvas will be able to serialize the primitive characteristics into a disk file.

If other behaviors are needed in the future, we can continue to add new observers, and then perform similar operations on the canvas and the primitives it owns.

Features

  • If you need to perform certain operations on all elements in a complex object structure (such as an object tree), you can use the visitor pattern.
  • The visitor pattern separates non-primary functions from the object manager, so it is also a means of decoupling.
  • If you are making an object library class library, then providing an access interface to the outside will help users develop their own visitor to access your class library non-invasively-he does not have to give you a little thing for himself issue/pull request.
  • For situations with complex structure levels, you must be good at using object nesting and recursion capabilities to avoid repetitive writing of similar logic.

    Please refer to the reference implementations of canva, layer, and group. They complete the nested self-management capabilities drawable and vistiable<drawable> accept() to enter each container recursively.

accomplish

We use a part of the vector diagram editor as an example to implement, using the basic class template given above.

drawable and basic primitives

First, make the basic declaration of drawable/shape and basic primitives:

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)
}

For debugging purposes, we overload the'<<' stream output operator, and use the macro MAKE_DRAWABLE to reduce the keystroke input of repetitive codes. In the MAKE_DRAWABLE macro, we hicc::debug::type_name<T>() , and return this as a string from drawable::type_name() .

For the sake of simplification, the basic primitives are not hierarchized, but are derived from the drawable in parallel.

Composite primitives and layers

The group object is declared below, which contains a group of primitives. Since we want as many recursive structures as possible, a layer is also considered a combination of a set of primitives:

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, ...
  };
}

The visitable interface has been implemented in the group class, and its accept can be accessed by visitors. At this time, the group of primitives will traverse all its primitives and provide them to the visitor.

You can also create a compound primitive type based on the group class, which allows several primitives to be combined into a new primitive element. The difference between the two is that group is generally a temporary object in UI operations, while compound primitives can be used as A member of the component library is for users to select and use.

By default, guest will access visited const & form of 0614095dba6328, which is read-only.

The layer has at least all the capabilities of the group, so its approach to visitors is the same. The properties of the layer (mask, overlay, etc.) have been skipped.

Canvas

The canvas contains several layers, so it should also implement the visitable interface:

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;
    }
  };
}

Among them, add will create a new layer with default parameters, and the order of the layers follows the upward stacking method. The get and [] operators can access a certain layer through a positive integer subscript. But the code does not include the management function of layer order, if you want to, you can add an std::vector<draw_id> to help manage the layer order.

Now let’s review the canvas-layer-graphic element system. The accept interface successfully runs through the entire system.

It's time to build visitors

screen or printer

These two implement a simple visitor interface:

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 is a simple streaming package, it does the following core logic:

template<typename T>
inline std::string to_string(T const &t) {
  std::stringstream ss;
  ss << t;
  return ss.str();
}

test case

The test program constructs a miniature canvas and several primitives, and then visits them schematically:

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);
}

The output should be similar to this:

--- 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 mode can sometimes be replaced by iterator mode. But iterators often have a fatal flaw that affects their practicality: the iterator itself may be rigid, costly, and inefficient-unless you make the most appropriate design-time choice and implement the most sophisticated Iterator. Both of them allow users to access the contents of a known complex container non-invasively.

:end:


hedzr
95 声望19 粉丝