[框架设计之道(一)]软件基础框架
前言
中间经历了几次波折,最终才算是有时间把软件开发的框架确定下来了。现在开发才终于算是开始有了个起头。
其实在使用Qt做大型软件的时候会遇到一些问题,为此也要不得不做一些妥协。关于这个,我觉得你可能需要看一下这两篇文章
越是深入开发越需要注意一点:Qt只是一个库,一个开发框架,它和boost库,MFC库并没有本质区别,它并不一定能撑起你整个的开发框架,你甚至可能要中途引入很多各种各样的库。
唯一的区别是,Qt算是一个相当重型的库,其中很多内容的生态都是封闭的(当然了,这都是建立在C++狗屎一样的String,如果string的问题可以解决,那么我相信这些库不能兼容的问题都将不复存在),这就意味着如果你使用了Qt的库,你大部分时间都将在这些Q打头的头文件里跌跌撞撞。
做出你想要做的事情,很难走出去或者走进来。这也是为什么大部分情况下用了Qt就不会用别的库了,即使你知道会有这样那样的问题:一是没必要,二是懒得弄。
说明
目前TSG项目开发使用Qt 5.14.2 + vs2019,请按照指定配置来开发,否则出现编译不过的情况请及时更换配置。
本文件将简单介绍一下软件框架及开发要求。框架没什么好说的,软件的要求是:所有新开发的模块都应该继承trunk\TSG_Base\PublicTemplate\TSG_DeviceTemplate.h中给定的指定类型,并在指定应用程序的kernel层中调用其父类,而不关心类的细节。
比如Device类型有很多很多,但是我在应用程序层中并不关心你的Device类型的细节,不管你是Faro,HikCam还是Imu,我只需要用QList将你所有的设备类型给打包,然后只需要管我的Device接口即可。
框架 TSG_Framework
一、底层信号机制 TSG_Caller
请所有的类至少需要继承…/Framework/TSG_Framework/目录下的TSG_Framework.h,其中包含了基本的信号和槽,本框架所有消息转发机制都是通过此回调函数进行的:
class TSG_Framework
{
public:
using CallMethod = void(*)(const QString& sModule, const QString& sDescribe, const QString& sVariable, const QVariant& extra);
using SendCMD = void(*)(const QString& sModule, const QString& sDescribe, const QString& sVariable, const QVariant& extra);
void RegisterCallMethod(CallMethod callback) {
callbacks_.append(callback);
}
void RegisterSendCMD(SendCMD callback) {
sendcmds_.append(callback);
}
void Signal_CallMethod(const QString& sModule, const QString& sDescribe, const QString& sVariable, const QVariant& extra) {
for (CallMethod callback : callbacks_) {
if (callback) {
callback(sModule, sDescribe, sVariable, extra);
}
}
}
void Signal_SendCMD(const QString& sModule, const QString& sDescribe, const QString& sVariable, const QVariant& extra) {
for (SendCMD callback : callbacks_) {
if (callback) {
callback(sModule, sDescribe, sVariable, extra);
}
}
}
virtual qint32 slot_ReturnValue(const QString& sModule, const QString& sDescribe, const QString& sVariable, const QVariant& extra) { return -1; };
virtual qint32 slot_GetCMD(const QString& sModule, const QString& sDescribe, const QString& sVariable, const QVariant& extra) { return -1; };
private:
QList<CallMethod> callbacks_;
QList<SendCMD> sendcmds_;
};
在这个父类中,提供了一个最基础的回调函数注册机制和发送控制命令的函数,后续所有继承这个类的子类都需要覆写这个方法,以便能够获得上层的消息。
这里我可能需要做出一些解释:
这就是CallMethod和ReturnValue在上下级中的映射关系,整套系统的控制实际上相当简单,上层控制层(后续包括业务层)通过CallMethod(回调)和ReturnValue方法在Kernel中交换消息,来实现控制层对底层的调用。
也就是说,逻辑上层的设备类如果需要调用底层的功能DLL,比如日志,网络通信等DLL,则需要使用CallMethod方法,如果底层DLL或者是从网络层发来的消息,则需要通过ReturnValue方法向上反应。
这一套信号槽机制构成了这个框架的基本,而上下信息尽量传递Json字符串,而不是一个简单的字符串,这个我们后面设计的时候会提到。
为什么不用信号和槽?因为信号和槽在继承中会出很多问题,而且会导致很多编译问题,这些问题我在做了大量调研后发现是解决不了的,所以只能用回调函数。
除了最基础的类,以下的类大多是建立在TSG_Framework的基石上。
我要先给出其他几个类之间的关系,再来说明这几个类的细节
一个设备的控制大体如上,Interface类实际上只是一个转发和接口,提供给主要进程一个最主要的控制接口,其实主要是信号收发机制。ConfigController更多的是像一个控制类内的Kernel类,但是Device类的成员并不是放在ConfigController类内。
ConfigController是一个真正承上启下的部分,管理设备信息的本地登记、各种信息的获取和修改,都留在本地,然后通过GetCMD获得各种各样的修改,否则都是默认值。这么做是为了保证在没有设置参数的前提下也能够正常执行任务。
当然了,Device并不会是一个单独的,而是会分为两部分,一个是设备本身的控制,第二个是业务的流程控制。原因很简单,设备并不一定是只有一个,而可能是一组设备。
接下来的介绍不分先后,名称也并不会按照上面的来。
二、参数类型声明 TSG_Params
class TSG_Params :public QObject {
public:
QMap<QString, QString> getKeys() {
QMap<QString, QString> ret;
const QMetaObject* metaObject = this->metaObject();
int propertyCount = metaObject->propertyCount();
for (int i = 0; i < propertyCount; ++i)
{
QMetaProperty property = metaObject->property(i);
if (property.isValid() && property.isReadable() && property.isWritable())
{
qDebug() << "Property name: " << property.name();
qDebug() << "Property type: " << property.typeName();
ret.insert(property.typeName(), property.name());
}
}
return ret;
}
QString toJson() {
return Lev_Json::JsonSerialization(this);
}
bool isValid(QString strMessage) {
return Lev_Json::ValidateJsonKeys(strMessage, this);
}
};
实际上对于一个参数类,并没有做什么实际上的操作,因为每一个参数类都是通过元对象去操作这个Object本身,所以这个类可以无限制的拓展。当然了,至于想拓展成什么样还得看在开发中的具体需求。
要求所有在设备控制层中都必须传递参数类,比如
但是在控制层之上的管理层中却无法使用模板类规范输入,这是因为QObject无法支持模板类继承导致的,详情见文章开头的两篇文章
这导致了部分在管理层中只能通过Json字符串来控制控制层中的参数传递,所以必须要对所有的参数传递提供一定的验证手段和转换手段,详情见下图:
另外,在Lev_Json模块中,我提供了一整套的检查方法,可以直接将Json字符串和类内的所有成员变量之间做一个基本的筛查,这也是TSG_Params的基石。没有注释,函数名称就 是注释:
三、设备类声明 TSG_Device
不管设备内部的运行细节,一个设备都应该有以下这些接口:这里不关心设备的具体细节,但是设备本身都必须为这一套工作提供流程,因为这些接口都是在上层中需要被调用的,以这个类为基础去注册具体设备操作。
当然了这样的声明仅限于在框架内部操作,实际上只有业务层提供一个操作接口。
四、设备配置文件控制 TSG_ConfigHelper
ConfigHelper类只需要给定一个路径和指定设备的名称组就可以去自己初始化和获得设备的配置文件了。
class TSG_ConfigHelper : public TSG_Framework {
public:
virtual bool Init(const QString& path, const QList<QString>& list_device_names) = 0;
virtual bool Init(const QString& path) = 0;
virtual QList<QString> getConfigFiles() = 0;
virtual QJsonObject getConfigContain(const QString& config_name, const QString& device_name) = 0;
virtual bool setConfigContain(const QString& config_name, const QString& contains, const QString& device_name) = 0;
virtual QList<QString> getDeviceNames() = 0;
virtual qint32 slot_ReturnValue(const QString& sModule, const QString& sDescribe, const QString& sVariable, const QVariant& extra) { return -1; };
virtual qint32 slot_GetCMD(const QString& sModule, const QString& sDescribe, const QString& sVariable, const QVariant& extra) { return -1; };
};
注:对每一个设备来说,其设备的配置文件类型和配置文件模式都应该是已经写死写固定了的,所以只给定一个应用程序当前的运行路径,然后自己去其中检索目录。
五、设备管理类 TSG_Device_Controller
这个就是Interface,没什么好说的,每个模块都是自洽的,so,不用传参了,直接来吧。
class TSG_Device_Controller : public TSG_Framework {
//输入当前进程的路径作为根路径,用于初始化控制类,完全是交由自己控制的
virtual bool Init(const QString& ApplicationPath) = 0;
virtual QString getDeviceType() = 0;
virtual bool PreStartMission() = 0;
virtual bool StartMission() = 0;
virtual bool PauseMission() = 0;
virtual bool EndMission() = 0;
virtual WorkState Get_WorkState() = 0;
qint32 slot_ReturnValue(const QString& sModule, const QString& sDescribe, const QString& sVariable, const QVariant& extra) { return -1; };
qint32 slot_GetCMD(const QString& sModule, const QString& sDescribe, const QString& sVariable, const QVariant& extra) { return -1; };
};