https://blog.csdn.net/myw31415926/article/details/127722899
抛砖引玉
试想一个问题,如果有一套收发数据的网络接口,需要提供给其他同事或厂家使用,包含头文件和动态库,假设头文件如下:
// 版本1
class NetworkV1 {
public:
int Send(const std::string str);
int Recv(std::string &str);
private:
int sockfd;
char buf[1024];
};
用户直接 include 头文件,链接库文件即可。方法上没有问题,但问题是头文件中暴露的信息太多了,比如 private 成员变量,而且如果以后的版本中需要增加或删除某些变量,还需要通知用户修改头文件,太麻烦了。
为了解决这个问题,实现接口与实现分离,所以引入了 IMPL 模式。
C++ IMPL 模式
这里的 IMPL 其实就是 implement,即实现的意思,个人觉得 IMPL 严格上来讲,并不算一个设计模式,只是一个更好的 隐藏实现的方法 。已 C++ 为例,它不仅仅是将类的声明和实现放在不同的文件中,更重要的是隐藏细节,只暴露用户必须的接口部分。先看一版改进的代码:
// network.h
// 版本2
class NetworkV2 {
public:
int Send(const std::string str);
int Recv(std::string &str);
private:
struct Impl;
std::shared_ptr<Impl> impl;
};
// network.cpp
// 版本2
struct NetworkV2::Impl {
int sockfd;
char buf[1024];
};
int NetworkV2::Send(const std::string str) {
// TODO ...
// send(impl->sockfd, str.c_str(), str.size(), 0);
return str.size();
}
int NetworkV2::Recv(std::string &str) {
// TODO ...
// recv(impl->sockfd, impl->buf, 1024, 0);
return str.size();
}
这样就做到了隐藏类中的成员变量了,核心思想就是 将成员变量打包放在一个结构体中 ,无论以后的版本中有无删减成员变量,都不会对头文件造成任何影响,这是目前 C++ IMPL 中非常常见的一种调用方法。类似于 C 语言中的 void* 指针,可以在需要的时候转换成任意对象。
完全隐藏成员变量
但是上一种模式还是会有 private 的成员变量,如果是想要完全隐藏,只保留接口呢?我在学习上交所 CTP 接口的时候,还看见过一种新的方法,核心思想是 使用虚函数和继承 。这种模式头文件中只保留接口,不会有任何的成员变量,代码如下:
// network.h
// 版本3
class NetworkV3 {
public:
virtual int Send(const std::string str) = 0;
virtual int Recv(std::string &str) = 0;
// 创建和销毁函数
static NetworkV3* New();
static void Delete(NetworkV3 *net);
};
// network.cpp
// 版本3
class NetworkV3Impl final : public NetworkV3 {
public:
int Send(const std::string str) override {
std::cout << "NetworkV3Impl::Send: " << str << std::endl;
return str.size();
}
int Recv(std::string &str) {
str = "ok";
std::cout << "NetworkV3Impl::Recv: " << str << std::endl;
return str.size();
}
};
// 创建和销毁函数
NetworkV3* NetworkV3::New() {
return (new NetworkV3Impl());
}
void NetworkV3::Delete(NetworkV3 *net) {
delete (NetworkV3Impl*)net;
}
虽然消除了 private 成员变量,但增加了两个静态成员函数 New 和 Delete ,用于创建和销毁对象,也不需要用户自己管理内存,使用上很方便,像 CTP 的 C++ 接口就是采用的这种模式。但后来查资料,发现这种方法有两个主要的弊端:
虚函数开销:虚函数需要使用虚函数表指针间接调用,运行时才能确定调用哪一个函数,无法在编译期间内联优化。在上一版中,在编译期就能确定调用哪一个函数,根本用不到虚函数的特性。
二进制兼容:虚函数是按照索引查询虚函数表来调用的,新增或调整虚函数顺序会造成索引变化,导致新接口在二进制层面不能兼容老接口,就是在末尾增加虚函数,也会有风险。