最近我們專案部的核心產品正在進行重構,然後又是年底了,除了開發工作之外專案並不緊急,加上加班時間混不夠了....所以就忙裡偷閒把整個專案的開發思路聊一下,以供參考。
鑑於接下來的一年我要進行這個主框架的開發,本著精益求精的態度,加上之前維護前輩的產品程式碼確實給我這個剛畢業的社畜帶來了不小的震撼,我決定在這個模組的開發中優化之前的開發模式,提升整個產品的健壯性和獨立性。
開發一個大型軟體最重要的問題有三個,一是如何保證每個模組開發的獨立性 二是如何保證資料結構的一致性 三是如何保證程式的可維護性和健壯性。這幾個文章的內容我會在幾篇文章中分開聊聊我的做法,做個記錄。
本篇文章聊聊如何保證各個模組開發的獨立性——怎麼讓功能模組、教學模組的開發獨立於主框架本身。讓不同的模組之間儘量通過介面的形式進行互動,而拋棄傳統的中轉訊息碼->呼叫模組的模式,讓實際功能以介面形式暴露。
這一期來聊聊開發中遇到的一些問題:QtActive Server如何通過COM口傳遞自定義結構體?如何通過一個COM口來獲得所有COM介面?
瀏覽本文前,請務必檢視前置文章以獲得更好的閱讀體驗,避免你不知道我在說什麼:
Qt開發Active控制元件:如何使用ActiveQt Server開發大型軟體的主框架
【大型軟體開發】淺談大型Qt軟體開發(一)開發前的準備——在著手開發之前,我們要做些什麼?
就我在開發的過程中發現了一個問題,就我的命名格式是以類似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開發子模組的工作量,也是我們主要重做這個框架的重要目的之一。