Visitor Pattern

The visitor pattern is a behavior pattern that allows arbitrarily isolated visitors to access managed elements under the control of the manager. Visitors cannot change the definition of objects (but this is not mandatory; you can agree to allow changes). To the manager, it doesn’t care how many visitors there are, it only cares about a certain element access order (for example, for binary trees, you can provide middle-order, pre-order, and so on).

composition

The Visitor pattern contains two main objects: Visitable objects and Vistor objects. Also, Visited objects are included in the Visitor schema as objects to be operated on.

A Visitable object, the manager, may contain a range of elements of various forms (Visited), which may have complex structural relationships within Visitable (but can also be a simple containment relationship, such as a simple vector). Visitable is typically a complex container that interprets these relationships and traverses the elements with a standard logic. When Visitable traverses the elements, it provides each element to a Visitor that can access the Visited element.

One such programming Pattern is the Visitor Pattern.

interface

In order to observe every element, there is actually a constraint: all observable elements have the common base class Visited.

All Visitors must derive from Visitor to provide 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
Copy the code

scenario

As an example, suppose we are designing a set of vector graphics editor. In the Canvas, there can be many layers, each Layer contains certain properties (such as fill color, transparency), and there can be multiple elements. Primitives can be Point, Line, Rect, Arc, etc.

To be able to draw a canvas on the Screen, we can have a Screen device object that implements the Visitor interface so that the canvas can be accessed by a Screen to draw primions from the canvas onto the Screen.

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

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

If additional behavior is needed later, we can continue to add new observers and perform similar operations on the canvas and its own primitives.

The characteristics of

  • If you need to perform certain operations on all elements in a complex object structure, such as a tree of objects, use the Visitor pattern.

  • The visitor pattern is also a means of decoupling by taking non-essential functionality away from the object manager.

  • If you are making an object library library, providing an access interface to the outside world will help users develop their own visitor to access your library without incursion — he doesn’t have to give you an issue/pull Request just for his own little thing.

  • In the case of complex structure hierarchy, we should be good at using object nesting and recursion ability, and avoid repeatedly writing similar logic.

    Check out the reference implementations of Canva, Layer, and Group, which accomplish nested self-management by implementing drawable and Vistiable

    , and make Accept () recursively enter each container.

implementation

We use part of the vector diagram editor as an example, using the basic class template given earlier.

Drawable and base primitives

Drawable/Shape: drawable/shape: 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)
 }
Copy the code

For debugging purposes, we overloaded the ‘<<‘ stream output operator and utilized the macro MAKE_DRAWABLE to cut keystroke input from repetitive code. In the MAKE_DRAWABLE macro, we get the class name via hicc::debug::type_name

() and return this as a string from drawable::type_name().

The base primitives are not hierarchical for the sake of simplification, but are derived parallel to drawable.

Compound elements and layers

Next declare the Group object, which contains a set of primitives. Since we want as much recursion as possible, layers are also thought of as a group 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, ... }; }Copy the code

The Visitable interface has been implemented in the Group Class. Its Accept accepts visitors, in which case the tuple group iterates through all its elements and provides them to visitors.

You can also create compound primitives based on the Group class, which allows you to combine primitives into a new element. The difference is that groups are usually temporary objects in UI operations, while compound primitives can be selected and used as members of a component library.

By default, guest accesses the visited primitives in the form of const &, that is, read-only.

A layer has at least the full power of a group, so it does the same for visitors. Layer properties (masks, overlays, etc.) are skipped.

The Canvas Canvas

The canvas contains several layers, so it should also implement 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; }}; }Copy the code

Add will create a new layer with the default parameters, and the layers will be stacked up. The get and [] operators can access a layer with a positive integer subscript. The code does not include layer order management, however. If desired, you can add an STD ::vector

helper structure to help manage layer order.

Now to review the canvas layer-primier system, the Accept interface successfully traverses the entire system.

It’s time to build visitors

The screen or printer

Both 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] "; }}; }Copy the code

Hicc :: TO_string is a simple streaming wrapper that does the following core logic:

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

test case

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

 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);
 }
Copy the code

The output should look something like 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: : the visitor: : basic: : 1 - END OF test_visitor_basic line# -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- It took 2.813.753 msCopy the code

Epilogue

The Visitor pattern can sometimes be replaced by the iterator pattern. But iterators often have a fatal flaw that affects their usefulness: iterators themselves can be rigid, costly, and inefficient — unless you make the best design-time choices and implement the best iterators. Both allow users to access the contents of a known complex container without intrusion.