最近我们项目部的核心产品正在进行重构,然后又是年底了,除了开发工作之外项目并不紧急,加上加班时间混不够了....所以就忙里偷闲把整个项目的开发思路聊一下,以供参考。
鉴于接下来的一年我要进行这个主框架的开发,本着精益求精的态度,加上之前维护前辈的产品代码确实给我这个刚毕业的社畜带来了不小的震撼,我决定在这个模块的开发中优化之前的开发模式,提升整个产品的健壮性和独立性。
开发一个大型软件最重要的问题有三个,一是如何保证每个模块开发的独立性 二是如何保证数据结构的一致性 三是如何保证程序的可维护性和健壮性。这几个文章的内容我会在几篇文章中分开聊聊我的做法,做个记录。
本篇文章聊聊如何保证各个模块开发的独立性——怎么让功能模块、教学模块的开发独立于主框架本身。让不同的模块之间尽量通过接口的形式进行交互,而抛弃传统的中转消息码->调用模块的模式,让实际功能以接口形式暴露。
这一期来聊聊开发中遇到的一些问题:QtActive Server如何通过COM口传递自定义结构体?如何通过一个COM口来获得所有COM接口?
浏览本文前,请务必查看前置文章以获得更好的阅读体验,避免你不知道我在说什么:
Qt开发Active控件:如何使用ActiveQt Server开发大型软件的主框架
【大型软件开发】浅谈大型Qt软件开发(一)开发前的准备——在着手开发之前,我们要做些什么?
一、如何让ActiveServer的接口以树形结构暴露
就我在开发的过程中发现了一个问题,就我的命名格式是以类似sig_SeatManager_GetAllSeatInfo()这样的方式命名的,虽然看上去结构清晰,但是总的来说不够简洁。在面对长时间的开发和维护,一个COM类暴露的接口和信号可能直接多达上百个,这显然是极大的影响了程序的维护效率。也就是类似图下:几乎所有的功能模块都通过Kernel 去调用了,这显然是不合适的。
最好的情况肯定是:我们所有的功能都通过每个功能模块的单例去调用。然后每个暴露的接口都是根据各个不同的类分门别类来处理功能的,也就是说,每个类都能有一个自己单独的暴露COM接口的类型,Interface_Kernel类只需要提供向各个接口类的重定向就好。
那么怎么做呢?其实也很简单,COM接口除了最基本的数据类:
其实还可以直接传递指针,
注:这个似乎不一定要使用Q_PROPERTY注册相关的属性,当然了也不一定,需要自己去测试一下,我反正写完了我就懒得管了
Q_PROPERTY(SeatManager* GetSeatManager1 READ GetSeatManager)
SeatManager* GetSeatManager() {
this->test = &SeatManager::Singleton();
return this->test;
}
在调用方就可以这样调用:
Interface = new QAxObject();
if (!Interface->setControl("LBD_VS19.ILBD_CloudNetIntelClassroom.1")) {
//获取失败
this->Add("COM Interface Load Failed! Check ActiveQt Server is Exist.");
}
//用于获取SeatManager的指针
QAxObject* Interface2 = new QAxObject();
Interface2 = Interface->querySubObject("GetSeatManager1");
//获取SeatManager类的接口文档
QFile docs2("AX_Interfaces.html");
docs.open(QIODevice::ReadWrite | QIODevice::Text);
QTextStream TS2(&docs);
TS << str_interfaces << endl;
另外需要注意的一点是,这个SeatManager也需要在开头声明以下宏:
Q_OBJECT
Q_CLASSINFO("ClassID", "{2642F93D-069A-420C-A309-5E4F1808320B}")
Q_CLASSINFO("InterfaceID", "{20F4EA3B-A8AD-42C0-8AAA-1C97F1BD35CD}")
Q_CLASSINFO("EventsID", "{3C1458B9-C236-48BF-A9C0-2BEB0221C173}")
Q_CLASSINFO("RegisterObject", "yes")
但是这个SeatManager不需要继承QAxBindable类,因为这个类需要提供功能但是并不是直接对外暴露给系统去调用的。由上就可以通过一个接口将几乎所有的接口类全部通过COM接口及文档的方式暴露给客户,以供调用。
二、如何传递自定义结构体或者类
这个在网上也是没说,Qt的官方文档写的也是一坨稀烂,报的相关错误更是重量级。
一开始我想的是直接通过QVarirant类直接将我的自定义类型转换一下,比如类似使用Q_DECLARE_METATYPE(test_struct)这样的宏直接进行转换。但是在我多次尝试之后一直会报错
QAxBase: Error calling IDispatch member getvseat_info: Type mismatch in parameter -1
后来我才意识到,这样的数据可以在一个进程内部*流动,但是QVariant定义了一个自定义结构是不能直接在COM接口之间*流动的,这部分需要去稍微了解一下COM的定义及内部结构才能更好的明白,总之你只需要知道并不能在ActiveServer这边定义一个接口,然后在调用方去直接获得这个QVariant对象,然后再强制转换回来,这样的操作是非法的。
怎么做?
其实我们能有一个相当简单粗暴的方式,也是一个可以体现cpp优越性的方式:直接强行把对象转成二进制流,然后通过COM口返回,再让调用方去转换这个二进制流。
我们来看下代码,其实比较简单:
ActiveServer:
//在此转换结构体
QByteArray myStructMethod() {
QByteArray send;
send.resize(sizeof(test_struct));
std::memcpy(send.data(), &testinstance, sizeof(test_struct));
return send;
}
调用方:
resultarr = Interface->dynamicCall("getseat_info()").toByteArray();
SeatInfo* tseatinfo = (SeatInfo*)resultarr.data();
这样就是相当于把在ActiveServer中的一个类直接转换成QByteArray,然后发送给调用方去转换这个QByteArray
这个做法和Json的方式比,有优点也有缺点
优点:
使用方便,只需要两边有对其的头文件就可以直接转换类或者结构体,直接跨线程无损传递数据,比JSON方便得多还少很多步骤
缺点:
1.几乎是不可维护的,因为两边的类类型必须对齐,也就是说两边的数据类型都完全无法二次加工,最好是只存放数据,如果需要自定义的化只能自己新开一个类。
2.不同的语言之间不能协调,因为我们原来的这个类是继承了QObject类,如果我们换一种语言用不到QObejct,那么这个类就变成不可获取的接口了。
注:
尽管缺点非常明显,我们还是选择了使用此方式。
1.因为QObject类可以提供QJson和QObject的转换--详情看我的*QJson和QObject的转换--*,在面对不同语言时其实struct的兼容性也并不好,所以思来想去还是直接传指针算了,除了指针之外还需要另外提供一套Json的方法,以供一些非Qt的教学模块以及第三方的进程使用---并不是只有我们内部使用的东西,我们只提供JSON字符串!
2.不得不说,这样做可以极大的减少Qt开发子模块的工作量,也是我们主要重做这个框架的重要目的之一。