Visitor Pattern

The accessor pattern is a behavioral pattern that allows arbitrary detached visitors to be able to access managed elements under the control of the manager. The visitor cannot change the definition of the object (but this is not mandatory, you can agree to allow changes). For the manager, it does not care how many visitors there actually are, it only cares about a defined order of access to the elements (for example, for a binary tree, you can provide multiple access orders such as mid-order, pre-order, etc.).

Composition

The Visitor schema contains two main objects: the Visitable object and the Vistor object. In addition, Visited objects are included in the Visitor schema as objects that will be manipulated.

A Visitable object, i.e., a manager, may contain a series of elements (Visited) of various shapes and sizes that may have complex structural relationships within the Visitable (but can also be some kind of simple containment relationship, such as a simple vector.) The Visitable will generally be a complex container that is responsible for interpreting these relationships and traversing them with A standard logic is used to traverse these elements. When Visitable traverses these elements, it provides each element to the Visitor so that it can access the Visited element.

Such a programming pattern is the Visitor Pattern.

Interface

In order to be able to observe each element, there is therefore a practical necessity to have a constraint: all observable elements have a common base class Visited.

All Visitors must be derived from Visitor in order to be available to the Visitable.accept(visitor&) interface.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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

For example, suppose we are designing a set of vector editors. In the Canvas, there can be many Layers, each containing certain properties (e.g. fill color, transparency), and there can be multiple Elements. Elements can be Point, Line, Rect, Arc, etc.

To be able to draw the canvas on the screen, we can have a Screen device object that implements the Visitor interface, so the canvas can accept access to the Screen and thus draw the elements of the canvas to the screen.

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

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

If we need additional behavior in the future, we can go ahead and add new observers and then do something similar with the canvas and the tuples it holds.

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 takes the non-primary functionality away from the object manager, so it is also a means of decoupling.
  • If you are making a class library for an object library, then providing an access interface to the outside will facilitate the user to develop his own visitor to access your class library without intrusion - he doesn’t have to give you issue/pull request for his own little thing.
  • For complex structural hierarchies, make good use of object nesting and recursion capabilities to avoid writing similar logic over and over again.

Check out the reference implementations of canva, layer, and group, which accomplish nesting self-management capabilities by implementing drawable and vistiable<drawable>, and enabling accept() to go recursively into each container.

Realization

We implemented a part of the vector image editor as an example, using the base class template given earlier.

drawable and base elements

First do the basic declaration of drawable/shape and the base tuples.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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 cut down on repetitive code keystrokes. In the MAKE_DRAWABLE macro, we get the class name via hicc::debug::type_name<T>() and return this as a string from drawable::type_name().

For simplicity reasons the base tuples are not hierarchical, but are derived in parallel from drawable.

Composite elements and layers

The following declares a group object, which contains a set of tuples. Since we want to have as much recursive structure as possible, layers are also considered to be a combination of a group of tuples

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 group class already implements the visitable interface, and its accept can accept visitors, at which point the tuples group iterates through all of its tuples and makes them available to visitors.

You can also create compound tuples based on the group class, which allows several tuples to be combined into a new tuples component, the difference being that groups are generally temporary objects for UI operations, while compound tuples can be selected and used by the user as part of a component library.

By default, guest accesses elements of the form visited const &, which is read-only.

The layer has at least all the capabilities of a group, so it does the same thing for visitors. The properties part of the layer (mask, overlay, etc.) is omitted.

Canvas

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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;
    }
  };
}

Where add will create a new layer with default arguments and the layer order follows an upward stacking pattern. get and the [] operator can access a layer with a positive integer subscript. However, the code does not contain a layer order management function. If you wish, you can add a std::vector<draw_id> helper structure to help manage the layer order.

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

It’s time to create the visitors

screen or printer

Both implement simple visitor interfaces.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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 stream wrapper that does the following core logic.

1
2
3
4
5
6
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 graph elements and then accesses them schematically.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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 look something like this.

1
2
3
4
5
6
7
--- 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

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 most appropriate design choices and implement the most elaborate iterator. They both allow the user to access the contents of a known complex container without intrusion.

BTW, some people say that it is because languages like Java do not support double assignment that the visitor pattern was born. This is pure fucking bullshit: Java was officially released in 1995, when it was a fairly rudimentary prototype and there were no patterns to speak of, and it wasn’t until 1998, when JDK 1.2 came out, that EJB, J2EE, etc., and then its design patterns took off in a big way. It is not too much to say that Java is riding on the east wind of GoF summary, without the milestone experience summary of GoF in 1994, there would not be Design Patterns terminology in the following decades until today, so too many kids are ignorant and fearless to open their mouths.

For C++, the definition of double-distribution or whatever is completely meaningless. I’m so sick of Java.