• C++对象间通信组件,让C++对象“无障碍交流”


    介绍

    这是很久之前的一个项目了,最近刚好有些时间,就来总结一下吧!
    推荐初步熟悉项目后阅读本文: https://gitee.com/smalldyy/easy-msg-cpp

    从何而来

    这要从我从事Qt开发的那些日子说起了,项目说大不大,说小也不小,人倒是一茬又一茬,需求也换了又换,后来的事情大家都懂了,项目变成了一坨浓Shit,且不说其中的设计、构架、以及需求问题,单说说我对这个项目的直观感受,在我看来,整个程序仿佛一颗大树,从某点作为根然后一直向上延伸,在没有足够时间重构的情况下,它的层级越来越深,这时候问题来了,如果想让树木的两个不同分支的叶子节点发生关系,事情就马上会变得十分痛苦!

    这两个想要联系的对象根本不再一个地方,我可能要将其中一个对象的指针在这颗大树的节点上倒退3层然后再前进2层才能让他们见面,然后暗戳戳的写下一个connect。

    这时候我就想,如果有一个专门的通信组件负责传递各种消息,让两个对象中间产生一个媒介作为他们通信的桥梁,获取这件事情就会变得更加轻松了,我不用再费尽心思的将两个对象引用到同一个作用域,甚至还要考虑哪个作用域更加合理。

    诚然,如果在前期就对项目的各个组件进行全盘规划,我想这种困境可能不会或者很少出现,但是并非所有事情都会按照美好的方向前进,就如曾经堆在我面前的那坨浓Shit,尽管我也为它的存在出过不少力…………

    设计目标

    • 提供C++对象进程内通信功能 可进行消息传递;
    • 将已经存在的结构体定义为消息时,不能破坏已经存在的结构体本身的结构;
    • 处理消息的类无需继承任何基类;
    • 足够简单的订阅方法;
    • RAII形式的取消订阅,但也支持手动取消订阅。

    你可能注意到了,我特意强调了不破坏原有结构。目的很简单,就是为了保证项目不会因为引入这个组件而发生太大的变化。众所周知,大部分程序员都是懒癌晚期,如果引入一个组件会导致工作量激增,程序员就会开始衡量shit的臭味和工作量之间的关系了。

    总之,核心特征只有两个:易用,改动小。

    原理分析

    我首先将这个组件设计为一个基于订阅分发方式的通信组件,它有三个主要角色,订阅者,发布者,和消息。

    首先考虑最简单的发布者,发布者的功能非常直观——发送消息,也就是说用户只要在需要的位置调用一个sendMsg之类的函数即可,这个函数的功能就是将用户给定的消息发送出去。

    然后便是订阅者,我们要求订阅的宿主类型不可以继承任何基类,这个要求决定了我们订阅的方式,我们需要提供一个函数,它接受一个对象的指针(我称之为宿主)和它的成员函数,将两者包装成一个std::function,将这个包装好的回调函数与一个定义好的消息关联并记录下来,这就形成了订阅关系。

    当发布者发送消息时,我们的组件需要查询订阅关系,找到消息对应的回调函数,将消息作为参数调用它!此时,对象间就完成了一次通信。我们的组件就是信使,这样就无需发信人四处奔波了。

    我们还要求不破坏原本的结构体的结构,这也就意味着我们不能改动已经存在的结构体,比如果让它继承一个消息基类然后就能作为消息传递之类的操作——虽然很好,但是我们得对这个设计说拜拜了。但是,上树订阅分发的流程必然要求消息拥有一个统一的基类类型,否则我们无法统一回调函数的函数签名,存储订阅关系也就无从谈起了!因为参数类型不同的函数,是很难存储到一个容器中以供查询的!

    为了解决这个闹人的问题,我们不妨反向思考一下,既然我们不能让一个已经存在的消息结构继承我们的基类,那么就创建一个新的类型同时继承两者吧!

     class NewExistMsg : public ExistMsg, public em::EasyMsg
    

    用户可以使用 NewExistMsg 来创建消息体,就像使用 ExistMsg一样,回调函数可以使用EasyMsg*作为参数,来达到类型的统一,并可以安全的进行多态设计。

    至此,消息的问题也解决了。

    你可能会感兴趣的技术细节

    以下是EasyMsg的头文件:

    class EASYMSG_API EasyMsg {
    public:
      EasyMsg();
      virtual ~EasyMsg() = default;
      virtual std::string id() const = 0;
    
      template <typename T> struct is_easymsg {
        template <typename U> static char test(typename U::MsgType *x);
        template <typename U> static long test(U *x);
        static const bool value = sizeof(test<T>(0)) == 1;
      };
    
      // c++17 support constexpr if
    #if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || __cplusplus >= 201703L)
      template <typename EASY_MSG_ID> bool match() {
        is_easymsg<EASY_MSG_ID> test_easymsg;
        if constexpr (test_easymsg.value) { // c++17
          return id() == EASY_MSG_ID::value;
        } else {
          std::cerr << "匹配消息ID时发生错误,检查是否使用了未定义的消息? 检查:"
                    << typeid(EASY_MSG_ID).name() << std::endl;
          return false;
        }
      }
    #else
      template <class MSGID>
      typename std::enable_if<!is_easymsg<MSGID>::value, bool>::type match() {
        std::cerr
            << "匹配消息ID时发生错误,检查是否使用了未定义的消息? typeinfo : "
            << typeid(MSGID).name() << std::endl;
        return false;
      }
    
      template <class MSGID>
      typename std::enable_if<is_easymsg<MSGID>::value, bool>::type match() {
        return id() == MSGID::value;
      }
    
    #endif
    };
    

    这里边有一些有意思的东西可以学习一下,首先映入眼帘的就像是经典的虚析构函数,这是作为多态基类的必要手续。接下来就是SFINAE的经典用法,我是用这个技巧实现了match函数,这个函数的主要作用就是判断给定的EASY_MSG_ID是否和传入的消息指针是同一种消息类型。

    match根据c++标准分成了两个实现,C++17版本借助了 constexpr if特性。以前的版本则用了经典的std::enable_if。

    SFINAE不甚了解的人应该很难理解这些代码,SFINAE中文含义为“匹配失败不是错误”,这对模板变成来说非常重要,不过这已经超出了本文范围,我仅仅是抛砖引玉,之后我可能会更新文章对此段代码进行详解,从而让大家了解这些惯用法。

    其他的便没有什么技术细节了,都是些常规的东西,无非是用map记录下订阅关系,然后send时执行回调之类的东西,不值细说。

    结论

    本文向大家介绍了一个侵入性较低的C++对象间通信组件,或许可以帮助你解决一些头疼的通信问题,并展示了一些你可能感兴趣的技术细节,如果能引发更多的思考那就更好不过了!

  • 相关阅读:
    URL编码及解码
    Javascript解析URL
    为什么在JavaScript中0.1+0.2不等于0.3?
    void 0 与 undefined
    Windows7、Windows10下把Git Bash Here 添加到右键菜单(ContextMenu)
    [菜鸟]C++创建类对象时(无参)后不加括号与加括号的区别
    git常用命令
    git 出错及解决
    Vim升华之树形目录插件NERDTree安装图解
    rails.vim环境安装(ubuntu)
  • 原文地址:https://www.cnblogs.com/xdblog/p/easymsgcpp.html
Copyright © 2020-2023  润新知