完善资料让更多小伙伴认识你,还能领取20积分哦, 立即完善>
前阵子隔壁组来了个Rust开发的架构师,讨论过如何设计方便易用扩展性高的接口。C++不像Java有接口的概念,但是C++可以实现接口的功能。下面就总结一下实际项目工程中实现C++接口的方法。
接口分为调用接口与回调接口,调用接口主要实现模块解耦的作用,只要保持接口兼容性,模块内部的升级对用户可以做到无感知。良好的接口分层有助于各业务团队高效率开发。 回调接口主要用于系统有异步事件需要通知用户。系统预定义接口形式,并由用户注册,具体调用时机由系统决定。 调用接口 假设有一个网络发送模块类Network,类定义如下: class Network { public:bool send(); } 虚函数 最常用的就是虚函数,可以使用虚函数定义Network接口类: class Network { public: virtual bool send()=0 static Network* New(); static void Delete(Network* network_); } 将send定义为虚函数,由继承类去实现(比如由PLC模块或者以太模块继承),以静态方法创建子类对象,以基类Network的指针返回给业务使用。资源遵循谁创建谁销毁的原则,基类还提供Delete方法销毁对象。因为对象销毁封装在接口内部,因此Network接口类可以不需要虚析构函数。 代码使用虚函数易读性较高,但是虚函数开销较大(需要使用虚函数表指针间接调用),无法在编译期间内联优化,而事实上调用接口在编译期就能确定使用哪个函数,不需要用到虚函数的动态特性。此外由于虚函数使用虚函数表指针间接调用的原因,增加虚函数会导致函数地址表索引变化,新增接口不能在二进制层面兼容老接口。而且由于用户可能继承了Network接口类,在末尾增加虚函数也有风险,因此虚函数接口一旦发布上线就基本无法修改。 指向实现的指针 可以使用指向实现的指针来定义Network接口类: class NetworkImpl; class Network { public: bool send(); static Network* New(); Network() ~Network(); private: NetworkImpl* impl; } Network的具体实现由NetworkImpl完成,通过使用指向实现的指针的方式来定义接口,接口类对象的创建和销毁可以由用户负责,更好的管理对象生命周期。 此外该方法通用性强,新增接口不会影响二进制兼容性,有利于项目快速迭代。 但是该方法还是增加了一层调用,对性能还是略微有影响,不符合C++的零开销原则。 隐藏的子类 隐藏的子类思想很简单,接口要实现的目标就是解耦,主要就是隐藏实现,也就是隐藏接口类的成员变量。如果能将接口类的成员变量都转移到另一个隐藏类中,那么接口类就不需要任何成员变量,那么就达到了隐藏实现的目的。具体实现方法如下: class Network { public: bool send(); static Network* New(); static void Delete(Network* network_); protected: Network(); ~ Network(); } Network接口类只有成员函数,没有成员变量。提供静态方法New创建对象,Delete方法销毁对象。New方法的实现中会创建隐藏的子类NetworkImol的对象,并以父类Network指针的形式返回。NetworkImol类中定义了Network类的成员变量,并将Network类声明为friend: class NetworkImol:public Network { friend class Network ; private: // 定义Network类的成员变量 } Network类的实现中创建NetworkImol子类对象,并以父类指针形式返回,通过将this强制转换为子类NetworkImol类型的指针来访问成员变量: bool Network::send() { NetworkImpl* impl = (NetworkImpl*)this; //通过impl访问成员变量,实现Network的功能 } static Network* New() { return new NetworkImpl(); } static Delete(Network* network) { delete (NetworkImpl*)network; } 该方法符合C++零开销原则,且同样符合二进制兼容性。 Rust语言中有一种Trait功能,可以在类外面实现一个Trait(不需要修改类代码),那么C++同样可以参考实现Trait功能假设需要在Network类中实现发送序列化数据,重新设计Network接口,Serializable类定义如下: class Serializable { public: virtual void serialize()const =0; }; Network类定义如下: class Network { public: bool send(const char* host, uint16_t port,constSerializable& buf); } Serializable接口类似于Rust中的Trait,现在任何实现了Serializable接口类的对象都可以通过调用Network类接口完成数据发送功能。那么问题来了,加入项目迭代需要增加通过Network类发送int型数据,如何在不修改类定义的同时实现Serializable接口呢?很简单: class IntSerializable :public Serializable { public: IntSerializable(const int i): intthis(i){} virtual void serialize() const override { buffer += std::to_string(*intthis); } private: const int* const intthis; }; 之后就可以通过Network发送int型数据了: Network* network = Network::New(); int i=1; network-》send(ip,port, IntSerializable(i)); 非侵入式接口将类和接口区分开来,类中的数据只包含成员变量,不包含虚函数表指针,因此类不会因为实现了n个接口而引入n个虚函数表指针。而接口中只包含虚函数表指针,不包含数据成员,类和接口之间通过实现类进行类型转换,类只有在充当接口使用的时候才会引入虚函数表指针,不充当接口的时候不会引入,符合C++零开销原则。 Rust编译器通过impl关键字记录了每个类实现了哪些Trait,因此在赋值时编译器可以自动完成将对象转换为对应的Trait类型。而g++等C++编译器并没有记录这些转换信息,因此需要手动转换类型。本质上还是通过代码帮编译器记录每个接口类实现了哪些Trait,使用模板类的继承,在编译期实现类似“静态多态”的功能。 |
|
|
|
只有小组成员才能发言,加入小组>>
878浏览 0评论
小黑屋| 手机版| Archiver| 电子发烧友 ( 湘ICP备2023018690号 )
GMT+8, 2024-11-22 06:02 , Processed in 0.680981 second(s), Total 79, Slave 59 queries .
Powered by 电子发烧友网
© 2015 bbs.elecfans.com
关注我们的微信
下载发烧友APP
电子发烧友观察
版权所有 © 湖南华秋数字科技有限公司
电子发烧友 (电路图) 湘公网安备 43011202000918 号 电信与信息服务业务经营许可证:合字B2-20210191 工商网监 湘ICP备2023018690号