什么是插件机制
插件是你想开发一个好的系统所需要的一种好的架构方式。C++插件是 C++ 编写的动态链接共享对象。一种可复用的、灵活管理(维护、替换或增加、删除)的功能模块儿化组件。基于插件的扩展性,进而实现业务模块儿的独立和解耦,增加可维护性和可扩展性。插件使得第三方开发人员可以为系统做增值工作,也可以使其他开发人员协同开发相互配合,增加新的功能而不破坏现有的核心功能。插件能够促进将关注点分开,保证隐藏实现细节,且可以将测试独立开来,并最具有实践意义。
比如强大的Eclipse的平台实际上就是一个所有功能都由插件提供的骨架。Eclipse IDE自身(包括UI和Java开发环境)仅仅是一系列挂在核心框架上的插件。
插件机制仍需要考虑的一些问题如错误处理,数据类型,版本控制,与框架代码以及应用代码的分离等等。或许,在应用程序框架容器内,可以借助lua脚本来动态的灵活的实现业务。
为什么要用插件机制
我们为什么要用插件架构?
现代软件工程已经从原先的通用程序库逐步过渡到应用程序框架。假设一个场景,以C++开发应用程序为例,我们的架构是基于APP+DLL的传统架构,所有的功能糅杂在一起。随着系统的日益庞大,各种模块之间耦合在一起,当修改其中一个模块时,其他模块也跟着一起受到影响。假如这两个模块式不同的开发人员负责的,那么还需要事先沟通好,这样就造成了修改维护的困难。那怎么解决这个问题?插件架构是一种选择。
“编程就是构建一个一个自己的小积木, 然后用自己的小积木搭建大系统”。但是程序还是会比积木要复杂, 我们的系统必须要保证小积木能搭建出大的系统(必须能被组合),有必须能使各个积木之间的耦合降低到最小。
传统的程序结构中也是有模块的划分,但是主要有如下几个缺点:
1: c++二进制兼容问题。
2: 模块对外暴露的东西过多,使调用者要关心的东西过多。
3: 封装的模块只是作为功能的实现者封装,而不是接口的提供者。
4: 可替换性和可扩展性差。
而插件式的系统架构就是为了解决这样的问题,插件机制也很符合设计模式的六大原则(现在是七大原则),是一种不错的设计。设计模式的六大原则里提到的单一职责原则、开放封闭原则、接口隔离原则、里氏替换原则,依赖倒转原则、迪米特法则。可以说插件机制几乎满足了这六大原则里所有的条款,当然也具备了由此带来的益处,因此学习和使用插件机制很有必要。
设计模式七大原则
都是为了更好的代码重用性,可读性,可靠性,可维护性,可扩展性。
单一职责原则:
即一个类应该只负责一项职责,降低类的复杂度,免得改了一个影响另一个。提高类的可读性,可维护性,降低变更引起的风险。插件机制的各个插件模块就是一种单一职责。
开闭原则:
一个软件实体如类,模块和函数应该对扩展开放,对修改关闭。用抽象构建框架,用实现扩展细节。当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。插件机制不正好的这个原则的实现吗。
迪米特法则:
一个对象应该对其他对象保持最少的了解。类与类关系越密切,耦合度越大。
迪米特法则又叫最少知道原则,即一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部。对外除了提供的public 方法,不对外泄露任何信息。插件机制的实现是这一法则很好的诠释。
接口隔离原则:
客户端不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上。后面的插件实现过程可以看到,插件所提供的接口是精简和必要的最小单元。
依赖倒转原则:
高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。依赖倒置原则基于这样一个事实:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。插件机制的实现上,插件提供的接口可以看作是一种高层模块,不依赖于底层实现细节。
里氏替换原则:
所有引用父类的地方必须能透明地使用其子类的对象,子类对象能够替换父类对象,而程序逻辑不变。根据这个理解,引申含义为:子类可以扩展父类的功能,但不能改变父类原有的功能。
合成复用原则:
该原则阐述的是我们应该如何复用类。复用类我们可以通过“继承”和“合成”两种方式来实现。它最大的缺点就是增加了类之间的依赖,当父类发生改变时,其子类也会被动发生改变。介于继承存在的这些缺点,我们在复用类时,要优先考虑使用“合成”进行复用。合成复用原则的核心思想是:在编写代码时如果需要使用其它类,那么两个类之间尽量使用合成/聚合的方式,而不是使用继承。我们可以通过类之间的“合成”来达到“复用”代码的诉求。
插件机制的实现原理
大致思路是应用程序提供出接口,由其他同事分工或第三方实现这些接口,并编译出相应的动态链接库(即插件);将所有插件放到某个特定目录,应用程序运行时会自动搜索该目录,并动态加载目录中的插件。
插件机制的实现过程
一、首先定义一个插件接口Iplugin.h。
这有点儿类似设计模式中的中介者模式。所有的插件模块儿之间接触,都通过这一公共的中间人交互。
#ifndef SERVICEPROJECT_IPLUGIN_H
#define SERVICEPROJECT_IPLUGIN_H
#include <QString>
#include <QStringList>
class IPlugin
{
public:
IPlugin() = default;
virtual ~IPlugin() = default;
IPlugin(const IPlugin&) = delete;
IPlugin& operator=(const IPlugin&) = delete;
public:
//获取支持的插件方法
virtual const QStringList& getSupportCommandList() const {return supportCommandList_; }
public:
virtual QString getVersionString() const = 0;
//! \brief 获取业务名称
//! \return
virtual QString getPluginName() const = 0;
enum pluginState_type
{
idle_,
starting_,
started_,
stopping_,
stopped_,
excepting_,
};
//! 获取插件的运行状态。
//! \return
virtual pluginState_type getPluginState() const = 0;
//! 获取插件的运行状态的解释
//! \param state
//! \return
static QString getPluginStateMessage(const pluginState_type& state)
{
QString msg = "no msg.";
switch (state)
{
case pluginState_type::idle_:
msg = "plugin is on idle statement.";
break;
case pluginState_type::starting_:
msg = "plugin is on starting statement.";
break;
case pluginState_type::started_:
msg = "plugin is on started statement.";
break;
case pluginState_type::stopping_:
msg = "plugin is on stopping statement.";
break;
case pluginState_type::stopped_:
msg = "plugin is on stopped statement.";
break;
case pluginState_type::excepting_:
msg = "plugin is on excepting statement.";
break;
}
return msg;
}
//! \brief 执行业务的主要接口方法
//! \param msg 由外部程序发送的通讯协议(比如json报文)
//! \return 错误信息
virtual std::error_code exec(const QString& msg) = 0;
//! \brief 业务停止运行
//! \return
virtual std::error_code stop() { return {}; }
//! \brief 释放动态库资源
//! \return 错误信息
virtual std::error_code release() = 0;
protected:
QStringList supportCommandList_;
};
#endif // SERVICEPROJECT_IPLUGIN_H
二、实现插件加载,注册等操作的管理类PluginManager。
遍历lib目录中的各个插件动态库,如plugin1.dll,plugin2.dll,等,完成插件的加载和注册。使用QT的QLibrary,(instance)lib->resolve("getInstance"),这里很关键,调用resolve()函数找到dll库中的getInstance函数,并强制转换为函数指针。后又强制转换为(IPlugin *)类型指针存储进QHash。
#include <QHash>
#include <QObject>
#if defined(PLUGIN_MANAGER_BUILD_SHARED)
#define PLUGIN_MANAGER_EXPORT __declspec(dllexport)
#else
#define PLUGIN_MANAGER_EXPORT __declspec(dllimport)
#endif
class IPlugin;
class QLibrary;
class PLUGIN_MANAGER_EXPORT PluginManager : public QObject
{
Q_OBJECT
private:
explicit PluginManager(QObject* parent = nullptr);
public:
static PluginManager* getInstance();
~PluginManager() override;
static const char* getLibraryName();
static const char* getLibraryVersion();
public:
void loadAll();
void unloadAll();
std::error_code load(const QString& name);
std::error_code unload(const QString& name);
QStringList getNames() const { return plugins_.keys(); }
IPlugin* get(const QString& name);
private:
QHash<QString, IPlugin*> plugins_;
QHash<QString, QLibrary*> libs_;
};
//......
typedef void *(*instance)();
PluginManager::PluginManager(QObject *parent) : QObject(parent)
{
}
PluginManager::~PluginManager()
{
unloadAll();
}
void PluginManager::loadAll()
{
QDir pluginDir(QCoreApplication::applicationDirPath() + R"(/../lib)");
QStringList filters;
filters << QString("*%1").arg(LIB_SUFFIX);
pluginDir.setNameFilters(filters);
auto entryInfoList = pluginDir.entryInfoList();
for (const auto &info : entryInfoList)
{
auto lib = new QLibrary(QCoreApplication::applicationDirPath() + R"(/../lib/)" + info.fileName());
if (lib->isLoaded())
{
LOGGING_WARN("%s is loaded.", info.fileName().toStdString().c_str());
continue;
}
if (lib->load())
{
auto func = (instance)lib->resolve("getInstance");
if (func)
{
auto plugin = (IPlugin *)func();
if (plugin)
{
auto pluginName = plugin->getPluginName();
if (plugins_.contains(pluginName))
{
LOGGING_WARN("%s repeated loading.", pluginName.toStdString().c_str());
lib->unload();
lib->deleteLater();
lib = nullptr;
continue;
}
plugins_.insert(pluginName, plugin);
libs_.insert(pluginName, lib);
LOGGING_DEBUG("%s version: %s", plugin->getPluginName().toStdString().c_str(),
plugin->getVersionString().toStdString().c_str());
}
else
{
LOGGING_ERROR("%s object create failed.", info.fileName().toStdString().c_str());
lib->unload();
lib->deleteLater();
lib = nullptr;
}
}
else
{
LOGGING_ERROR("%s cannot find symbol.", info.fileName().toStdString().c_str());
lib->unload();
lib->deleteLater();
lib = nullptr;
}
}
else
{
LOGGING_ERROR("%s load failed. error message: %s", info.fileName().toStdString().c_str(),
lib->errorString().toStdString().c_str());
lib->deleteLater();
lib = nullptr;
}
}
}
void PluginManager::unloadAll()
{
for (auto &item : plugins_)
{
item->release();
}
for (auto &item : libs_)
{
item->unload();
item->deleteLater();
item = nullptr;
}
plugins_.clear();
libs_.clear();
}
......
IPlugin *PluginManager::get(const QString &name)
{
if (plugins_.contains(name))
{
return plugins_.value(name);
}
LOGGING_ERROR("%s is not found.", name.toStdString().c_str());
return nullptr;
}
PluginManager *PluginManager::getInstance()
{
static PluginManager w;
return &w;
}
const char *PluginManager::getLibraryName()
{
return PROJECT_NAME;
}
const char *PluginManager::getLibraryVersion()
{
return PROJECT_VERSION;
}
三、插件模块的实现
各个插件模块对外提供一唯一的入口函数getInstance()。
注意:该函数必须为 extern "C"声明的。why?
为什么要这么做呢?原因是C++的编译器会对程序中符号进行修饰,这个过程在编译器中叫符号修饰(Name Decoration)或者符号改编(Name Mangling)。如果不改为c的方式,那么动态库resolve这种查找入口方式,会找不到句柄handle入口。
在这个getInstance函数内部,可以实例化具体的类对象(前提这类对象实现了IPlugin接口)。
//myplugin1dll.h
extern "C" __declspec(dllexport) void *getInstance();
//myplugin1dll.cpp
void* getInstance()
{
static MyPlugin1 w;
return (void*)&w;
}
//MyPlugin1.h
class MyPlugin1: public IPlugin
{
public:
MyPlugin1();
~MyPlugin1() override;
public:
QString getVersionString() const override;
//! \brief 获取业务名称
//! \return
QString getPluginName() const override;
//! 获取插件的运行状态。
//! \return
pluginState_type getPluginState() const override;
//! \brief 执行业务
//! \param msg 由外部程序发送的通讯协议
//! \return 错误信息
std::error_code exec(const QString &msg) override;
//! \brief 业务停止运行
//! \return
std::error_code stop() override;
//! \brief 释放动态库资源
//! \return 错误信息
std::error_code release() override;
};
//MyPlugin1.cpp
......
#define MY_PLUGIN_NAME "myplugin1"
MyPlugin1::MyPlugin1()
{
//关键,这里追定了本插件的插件名,插件管理类对象PluginManager会根据这个找到自己
supportCommandList_.push_back(MY_PLUGIN_NAME);
}
MyPlugin1::~MyPlugin1()
{
}
QString MyPlugin1::getVersionString() const
{
return PROJECT_VERSION;
}
QString MyPlugin1::getPluginName() const
{
return PROJECT_NAME;
}
IPlugin::pluginState_type MyPlugin1::getPluginState() const
{
return IPlugin::starting_;
}
std::error_code MyPlugin1::exec(const QString& msge)
{
//业务功能实现......
}
举例插件机制的使用
经过前面三步,准备工作做好了。如何使用看看效果呢?
我们写一个测试的do_pluginWork(const QString& msg, const QString& cmd)。
其中的cmd内容指定插件名称。
实现过程为遍历PluginManager中管理的所有插件名,找到对应的并传递调用参数msg。
假如实现了两个插件plugin1.dll和plugin2.dll ,do_pluginWork("hello","plugin1"),则会调用plugin1中的exec函数功能。
//......
do_pluginWork(const QString& msg, const QString& cmd)
{
auto allPluginNames = PluginManager::getInstance()->getNames();
for (const auto& name : allPluginNames)
{
auto pluginPtr = PluginManager::getInstance()->get(name);
if (pluginPtr)
{
if (pluginPtr->getSupportCommandList().contains(cmd))
{
auto errorCode = pluginPtr->exec(msg);
if (errorCode.value())
{
LOGGING_ERROR("[%s] result message: %s", pluginPtr->getPluginName().toStdString().c_str(),
errorCode.message().c_str());
}
LOGGING_DEBUG("%s is executed.", cmd.toStdString().c_str());
return;
}
}
}
LOGGING_ERROR("cannot found match command.");
}
最后,插件机制是不是挺不错的,愉快的使用吧。
引用:
利用C++实现插件系统_猫咪的晴天的博客-CSDN博客_c++ 插件系统
C++ 插件系统_qq_32250025的博客-CSDN博客_c++ 插件
C++插件架构浅谈与初步实现_臣有一事不知当不当讲的博客-CSDN博客_c++插件
构建自己的C/C++插件开发框架_加油努力4ever的博客-CSDN博客_c++插件框架
C/C++:构建你自己的插件框架_石头的博客-CSDN博客_c 插件框架
软件设计七大原则,看完这一篇就够了_凹凸曼蓝博one的博客-CSDN博客_合成复用原则