什么是插件机制
插件是你想开发一个好的系统所需要的一种好的架构方式。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博客_合成复用原则
C++实现插件化开发_gnr_123的博客-CSDN博客_c++ 插件化