浅谈 C++17 里的 Visitor 模式

目录
  • 一、Visitor Pattern
    • 1、组成
    • 2、接口
    • 3、场景
    • 4、特点
    • 5、实现
  • 二、Epilogue

一、Visitor Pattern

访问者模式是一种行为模式,允许任意的分离的访问者能够在管理者控制下访问所管理的元素。访问者不能改变对象的定义(但这并不是强制性的,你可以约定为允许改变)。对管理者而言,它不关心究竟有多少访问者,它只关心一个确定的元素访问顺序(例如对于二叉树来说,你可以提供中序、前序等多种访问顺序)。

1、组成

Visitor 模式包含两个主要的对象Visitable 对象和 Vistor 对象。此外,作为将被操作的对象,在 Visitor 模式中也包含 Visited 对象。

一个 Visitable 对象,即管理者,可能包含一系列形态各异的元素(Visited),它们可能在 Visitable 中具有复杂的结构关系(但也可以是某种单纯的容纳关系,如一个简单的 vector)。Visitable 一般会是一个复杂的容器,负责解释这些关系,并以一种标准的逻辑遍历这些元素。当 Visitable 对这些元素进行遍历时,它会将每个元素提供给 Visitor 令其能够访问该 Visited 元素。

这样一种编程模式就是 Visitor Pattern

2、接口

为了能够观察每个元素,因此实际上必然会有一个约束:所有的可被观察的元素具有共同的基类 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

3、场景

以一个实例来说,假设我们正在设计一套矢量图编辑器,在画布(Canvas)中,可以有很多图层(Layer),每一图层包含一定的属性(例如填充色,透明度),并且可以有多种图元(Element)。图元可以是 PointLineRectArc 等等。

为了能够将画布绘制在屏幕上,我们可以有一个 Screen 设备对象,它实现了 Visitor 接口,因此画布可以接受 Screen 的访问,从而将画布中的图元绘制到屏幕上。

如果我们提供 Printer 作为观察者 ,那么画布将能够把图元打印出来。

如果我们提供 Document 作为观察者,那么画布将能够把图元特性序列化到一个磁盘文件中去。

如果今后需要其它的行为,我们可以继续增加新的观察者,然后对画布及其所拥有的图元进行类似的操作。

4、特点

  • 如果你需要对一个复杂对象结构 (例如对象树) 中的所有元素执行某些操作, 可使用访问者模式。
  • 访问者模式将非主要的功能从对象管理者身上抽离,所以它也是一种解耦手段。
  • 如果你正在制作一个对象库的类库,那么向外提供一个访问接口,将会有利于用户无侵入地开发自己的 visitor 来访问你的类库——他不必为了自己的一点点事情就给你 issue/pull request
  • 对于结构层级复杂的情况,要善于使用对象嵌套与递归能力,避免反复编写相似逻辑。

请查阅 canvalayergroup 的参考实现,它们通过实现 drawable vistiable<drawable> 的方式完成了嵌套性的自我管理能力,并使得 accept() 能够递归地进入每一个容器中。

5、实现

我们以矢量图编辑器的一部分为示例进行实现,采用了前面给出的基础类模板。

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 的全部能力,所以面对访问者它的做法是相同的。图层的属性部分(maskoverlay 等等)被略过了。

画布 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 模式有时候能够被迭代器模式所代替。但是迭代器常常会有一个致命缺陷而影响了其实用性:迭代器本身可能是僵化的、高代价的、效率低下的——除非你做出了最恰当的设计时选择并实现了最精巧的迭代器。 它们两者都允许用户无侵入地访问一个已知的复杂容器的内容。

到此这篇关于浅谈 C++17 里的 Visitor 模式的文章就介绍到这了,更多相关 C++17 里的 Visitor 模式内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C++基于OpenCV实现手势识别的源码

    先给大家上效果图: 源码在下面 使用 RGB 值分割手部区域,即手部的 GB 值将与背景不同 或者使用边缘检测 或者 背景减法. 我这里使用了背景减法模型.OpenCV为我们提供了不同的背景减法模型,codebook   它的作用是对某些帧进行一段时间的精确校准.其中对于它获取的所有图像:它计算每个像素的平均值和偏差,并相应地指定框. 在前景中它就像一个黑白图像,只有手是白色的 用 Convex Hull 来找到指尖.Convex hull 基本上是包围手部区域的凸集. 包围手的红线是凸包.基本

  • C++内存模型和名称空间详解

    目录 1. 单独编译 2.存储持续性.作用域和链接性 2.1 作用域和链接 2.2 自动存储持续性 2.3 静态持续变量 2.4 静态持续性.外部链接性 2.5 静态持续性.内部链接性 2.6 静态存储持续性.无链接性 2.7 说明符和限定符 2.8 函数和链接性 2.9 语言链接性 2.10 存储方案和动态分配 3. 名称空间 3.1 传统的C++名称空间 3.2 新的名称空间特性 3.3 名称空间及其前途 4 .总结 本章内容包括: 单独编译 存储持续性.作用域和链接性 定位new运算符 名

  • C++的命名空间详解

    目录 C++ | C++命名空间 C++命名空间 定义命名空间 实例1: using 指令 实例2: 实例3: 不连续的命名空间 嵌套的命名空间 实例4: 实例5: 笔记: 实例6: 实例7: 总结 C++ | C++命名空间 C++命名空间 一个中大型软件往往由多名程序员共同开发,会使用大量的变量和函数,不可避免地会出现变量或函数的命名冲突. 当所有人的代码都测试通过,没有问题时,将它们结合到一起就有可能会出现命名冲突. 例如小李和小韩都参与了一个文件管理系统的开发,它们都定义了一个全局变量

  • 学习php设计模式 php实现访问者模式(Visitor)

    访问者模式表示一个作用于某对象结构中各元素的操作.它可以在不修改各元素类的前提下定义作用于这些元素的新操作,即动态的增加具体访问者角色. 访问者模式利用了双重分派.先将访问者传入元素对象的Accept方法中,然后元素对象再将自己传入访问者,之后访问者执行元素的相应方法. 访问者模式多用在聚集类型多样的情况下.在普通的形式下必须判断每个元素是属于什么类型然后进行相应的操作,从而诞生出冗长的条件转移语句.而访问者模式则可以比较好的解决这个问题.对每个元素统一调用$element->accept($v

  • Java设计模式之访问模式(Visitor者模式)介绍

    Visitor定义:作用于某个对象群中各个对象的操作.它可以使你在不改变这些对象本身的情况下,定义作用于这些对象的新操作. 在Java中,Visitor模式实际上是分离了collection结构中的元素和对这些元素进行操作的行为. 为何使用Visitor模式 Java的Collection(包括Vector和Hashtable)是我们最经常使用的技术,可是Collection好象是个黑色大染缸,本来有各种鲜明类型特征的对象一旦放入后,再取出时,这些类型就消失了.那么我们势必要用If来判断,如:

  • Java 的双重分发与 Visitor 模式实例详解

    双重分发(Double Dispatch) 什么是双重分发? 谈起面向对象的程序设计时,常说起的面向对象的「多态」,其中关于多态,经常有一个说法是「父类引用指向子类对象」. 这种父类的引用指向子类对象的写法类似下面这种: Animal animal = new Dog(); animal.bark(); 另一种常用的形式是 public class Keeper { public void say(Animal a) { System.out.println("Animal say")

  • C#设计模式之Visitor访问者模式解决长隆欢乐世界问题实例

    本文实例讲述了C#设计模式之Visitor访问者模式解决长隆欢乐世界问题.分享给大家供大家参考,具体如下: 一.理论定义 访问者模式 提供了 一组 集合 对象 统一的 访问接口,适合对 一个集合中的对象,进行逻辑操作,使 数据结构  和 逻辑结构分离. 二.应用举例 需求描述:暑假来啦!三个小伙子组团,开车来 长隆欢乐世界玩. 每个人想玩的项目都不一样, 旅游者 1   想玩:十环过山车,龙卷风暴,梦幻旋马 旅游者 2   想玩:空中警察,欢乐摩天轮,超级水战 旅游者 3   想玩:四维影院,垂

  • php设计模式 Visitor 访问者模式

    复制代码 代码如下: <?php /** * 访问者模式 * * 表示一个作用于某对象结构中的各元素的操作,可以在不改变各元素的类的前提下定义作用于这些元素的新操作 * */ abstract class Visitor { abstract public function visitCroncreteElementA($element); abstract public function visitCroncreteElementB($element); } class ConcreteVis

  • 实例讲解iOS应用的设计模式开发中的Visitor访问者模式

    为了方便向大家展示,先给出简短的定义: 访问者模式(Visitor),表示一个作用于某对象结构中的各元素的操作.它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作. 紧接着,给出其类结构图. 访问者模式适用于数据结构相对稳定的系统,它把数据结构和作用于结构上的操作之间的耦合解脱开,使得操作结合可以相对自由地演化. 访问者模式的目的是要把处理从数据结构分离出来.很多系统可以按照算法和数据结构分开,如果这样的系统有比较稳定的数据结构,又有易于变化的算法的话,使用访问者模式就是比较合适的,

  • 浅谈 C++17 里的 Visitor 模式

    目录 一.Visitor Pattern 1.组成 2.接口 3.场景 4.特点 5.实现 二.Epilogue 一.Visitor Pattern 访问者模式是一种行为模式,允许任意的分离的访问者能够在管理者控制下访问所管理的元素.访问者不能改变对象的定义(但这并不是强制性的,你可以约定为允许改变).对管理者而言,它不关心究竟有多少访问者,它只关心一个确定的元素访问顺序(例如对于二叉树来说,你可以提供中序.前序等多种访问顺序). 1.组成 Visitor 模式包含两个主要的对象:Visitab

  • 浅谈在Vue-cli里基于axios封装复用请求

    本文介绍了浅谈在Vue-cli里基于axios封装复用请求,分享给大家,具体如下: 安装 只用安装一个axios就可以了. npm install axios --save 接口代理设置 为了请求可以正常发送,我们一般要进行一个接口代理的配置,这样可以避免请求跨域,项目打包之后,后端一般也要搭建一个nginx之类的东西进行转发请求,不然请求会因为跨域问题失败的. //文件位置:config/index.js proxyTable: { '/api': { target: 'http://47.9

  • 浅谈原型对象的常用开发模式

    1.构造函数和原型组合使用模式:原型对象虽然可以对所有实例的属性和方法共享,但是也有它的局限性,正是因为可以共享,也导致某一个实例对象若改变了共享的属性和方法,其他对象在使用时会收到影响. 所以可以组合使用构造函数式和原型模式,在实际开发中这种模式也应用的最为广泛. 2.动态原型模式:就是把属性和方法都封装到构造函数中 例如: function Person(name,age,sex){ this.name=name; this.age=age; this.sex=sex; if(typeof

  • 浅谈Python类里的__init__方法函数,Python类的构造函数

    如果某类里没有__init__方法函数,通过类名字创建的实例对象为空,切没有初始化:如果有此方法函数,通常作为类的第一个方法函数,有点像C++等语言里的构造函数. class Ca: def __init__(self, v): # 注意前后各两个下划线 self.name = v def pr(self): print "a--->", self.name ia = Ca("Jeapedu") # 本质调用的是__init__方法函数 ia.pr() Ca.

  • 浅谈JAVA设计模式之享元模式

    享元模式(Flyweight Pattern)主要用于减少创建对象的数量,以减少内存占用和提高性能.这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结构的方式. 享元模式尝试重用现有的同类对象,如果未找到匹配的对象,则创建新对象.我们将通过创建 5 个对象来画出 20 个分布于不同位置的圆来演示这种模式.由于只有 5 种可用的颜色,所以 color 属性被用来检查现有的 Circle 对象. 介绍 意图: 运用共享技术有效地支持大量细粒度的对象. 主要解决: 在有大量

  • 浅谈JavaScript 中的延迟加载属性模式

    目录 一.前言 二.按需属性模式 三.凌乱的延迟加载属性模式 四.类的唯一自己的延迟加载属性模式 五.对象字面量的延迟加载属性模式 六.结论 一.前言 传统上,开发人员在 JavaScript 类中为实例中可能需要的任何数据创建属性.对于在构造函数中随时可用的小块数据来说,这不是问题.但是,如果在实例中可用之前需要计算某些数据,您可能不想预先支付该费用.例如,考虑这个类: class MyClass { constructor() { this.data = someExpensiveCompu

  • 浅谈PHP设计模式之对象池模式Pool

    目录 目的 UML 类图 代码 测试 目的 在初始化实例成本高,实例化率高,可用实例不足的情况下,对象池可以极大地提升性能.在创建对象(尤其是通过网络)时间花销不确定的情况下,通过对象池在可期时间内就可以获得所需的对象. 无论如何,对象池模式在需要耗时创建对象方面,例如创建数据库连接,套接字连接,线程和大型图形对象(比方字体或位图等),使用起来都是大有裨益的.在某些情况下,简单的对象池(无外部资源,只占内存)可能效率不高,甚至会有损性能. UML 类图 代码 WorkerPool.php <?p

  • 浅谈webpack devtool里的7种SourceMap模式

    我们先来看看文档对这 7 种模式的解释: 模式 解释 eval 每个module会封装到 eval 里包裹起来执行,并且会在末尾追加注释 //@ sourceURL. source-map 生成一个SourceMap文件. hidden-source-map 和 source-map 一样,但不会在 bundle 末尾追加注释. inline-source-map 生成一个 DataUrl 形式的 SourceMap 文件. eval-source-map 每个module会通过eval()来执

  • 浅谈iOS开发如何适配暗黑模式(Dark Mode)

    暗黑模式 原理 将同一个资源,创建出两种模式的样式.系统根据当前选择的样式,自动获取该样式的资源 每次系统更新样式时,应用会调用当前所有存在的元素调用对应的一些重新方法,进行重绘视图,可以在对应的方法做相应的改动 资源文件适配 创建一个Assets文件(或在现有的Assets文件中) 新建一个图片资源文件(或者颜色资源文件.或者其他资源文件) 选中该资源文件, 打开 Xcode ->View ->Inspectors ->Show Attributes Inspectors (或者Opt

  • 浅谈Java设计模式系列-装饰器模式

    一.概述 装饰器模式作用是针对目标方法进行增强,提供新的功能或者额外的功能. 不同于适配器模式和桥接模式,装饰器模式涉及的是单方,和代理模式相同,而且目标必须是抽象的. 而实际上,装饰器模式和代理模式的实现方式基本一致,只在目标的存在上有些差别,这个后面我们具体讲述. 二.初步分析 上面提到了两点: 涉及的是单方 目标是抽象的 我们来想一下,所谓单方主要指的是在整个装饰器模式中不存在双方调用,要解决的也不是双方调用的问题,而是解决单方提供对外服务的问题,这个单方在自行对外提供服务时,功能不足,或

随机推荐