【大型軟體開發】淺談大型Qt軟體開發(一)開發前的準備——在著手開發之前,我們要做些什麼?

2023-01-11 21:00:29

前言

最近我們專案部的核心產品正在進行重構,然後又是年底了,除了開發工作之外專案並不緊急,加上加班時間混不夠了....所以就忙裡偷閒把整個專案的開發思路聊一下,以供參考。

鑑於接下來的一年我要操刀這個主框架的開發,本著精益求精的態度,加上之前維護前輩的產品程式碼確實給我這個剛畢業的社畜帶來了不小的震撼,我決定在這個模組的開發中優化之前的開發模式,提升整個產品的健壯性和獨立性。

開發一個大型軟體最重要的問題有三個,一是如何保證每個模組開發的獨立性 二是如何保證資料結構的一致性 三是如何保證程式的可維護性和健壯性。這幾個文章的內容我會在幾篇文章中分開聊聊我的做法,做個記錄。

本篇文章我們暫時只談開發準備,雖然不涉及核心問題,但我仍然認為準備階段也是和開發、測試一樣重要的階段。和後者不同,準備階段一旦做好了之後可以為後續的開發提供模板,可以大大減少在實際開發中走的彎路。

這一期簡單聊聊開發準備,下一期淺談怎麼在開發中我們專案如何保證模組開發的獨立性。

開發準備

在大學的軟體工程這門課中我們可以知道,軟體開發實際上佔比時間最長的是前期策劃和測試階段。

實際上開發中這兩個階段佔比是最長的也是最折磨的。但是在實際的開發流程中我們會發現:

在準備階段做的事情越多,開發階段受的折磨就越少;在測試階段做的事情越多,在維護階段受的折磨就越少。

在這裡我就簡單聊聊在我們當前這個大型軟體進行開發之前做了哪些準備,給出一些能給的範例以供參考。

一、制定開發規範

我個人認為,在C++程式的聯合開發中,除非完全不需要維護別人程式碼或產品(當然這個在實際情況下是不可能的),否則一份開發規範和定期的Code Review是必要的。一個好的規範是嚴肅且必要的,好的編碼習慣是決定一個程式設計師產品健壯性和可讀性的關鍵。

我在這裡可以列出一份我給部門內部定義的簡單開發規範:雲網路智慧課堂-Qt程式程式碼開發規範,內容如下:
序言:

 程式設計規範可以提升程式碼可讀性,提高可維護性。

目錄:

一、命名規範

二、記憶體管理規範

三、函數方法規範

四、控制語句規範  

五、註釋規範

六、排版規範

七、版本管理規範

八、介面程式設計

詞義解釋:強制,推薦,參考分別表示規範的三個等級。

一、命名規範:

【強制】1.類、函數、變數及引數採用[谷歌式命名約定](https://zh-google-styleguide.readthedocs.io/en/latest/google-cpp-styleguide/naming)。

【強制】2.常數(包含對話方塊ID)命名所有字母大寫。

【強制】3.介面類在前+字元(純虛類)I。基礎類別則在前+Base。

【強制】4.函數命名規範:獲取,查詢用Get,設定用Set,增加用Add,插入用Insert,刪除用Delete。儲存用Save。

【強制】5.槽函數的命名:每個函數字首是slot_,如:slot_SendData();

【強制】6.訊號的命名:每個函數字首是sig_,如:sig_SendData();

【強制】7.如果是全域性變數,請在前面加上this->以示區分。
二、記憶體管理規範

【強制】1.記憶體誰申請,誰釋放

【強制】2.不允許使用CPP自帶智慧指標shared_ptr,share_ptr和qobject搭配使用會出現一些意料之外的問題。

【強制】3.使用指標請一定要使用Qt自帶的智慧指標QPointer和QSharedPointer,不允許出現裸露在外的普通指標。

【強制】4.new申請記憶體之後。使用try catch捕獲申請記憶體是否成功。原因:new申請記憶體可能失敗

【強制】5.變數(普通變數和指標)必須初始化。

【強制】6.使用指標前必須檢查指標是否為空。

【強制】7.指標new後必須delete且將指標賦值為nullptr。

【強制】8.函數中分配的記憶體,函數退出之前要釋放。

【強制】9.多執行緒讀寫共用變數要加鎖。

【推薦】10.對可能的跨執行緒訊號槽函數需要在connect函數中加入Qt::QueuedConnection引數

【強制】11.程式內部的所有資料流動,除了自定義的型別,系統型別比如String,int等全部使用Qt內部型別QString,qint32等。

【推薦】12.在編寫類的時候最好保留呼叫方引數,以方便使用Qt自帶GC

三、函數方法規範

   【強制】1.函數引數必須在使用前校驗(建議放在函數第一行)。包括資料範圍校驗,資料越界校驗,異常指標校驗。

   【推薦】2.增加函數錯誤處理流程,try catch,asset

   【參考】3.函數引數比較多時,應考慮用結構體代替

   【推薦】4.函數體長度應在80行內,且保證函數功能的單一性。

   【推薦】5.函數內程式碼層次應保持一致。

四、控制語句

 【強制】1.儘量上的使用if else 語句,多采用衛語句。

 【強制】2.不要在條件推斷中執行其他複雜的語句。將復 雜邏輯推斷的結果賦值給一個有意義的布林變數名。以提高可讀性。

五、註釋規範

 【強制】1.模組註釋包含資訊:作者,日期,功能,依賴模組,呼叫流程

 【強制】2.類註釋包含資訊:作者,日期,功能,依賴類,呼叫流程

 【強制】3.函數註釋包含資訊:作者,日期,功能,引數含義,返回值,其他。

 【強制】4.變數註釋:註解內容要清楚準確不能有歧義。

六、排版規範

 【推薦】1.左大括號前不換行,左大括號後換行;右大括號前換行,右大括號後還有 else 等程式碼則不換行;表示終止右大括號後必須換行。

【推薦】 2.左括號和後一個字元之間不出現空格。相同,右括號和前一個字元之間也不出現空格。

 【推薦】 3.if/for/while/switch/do 等保留字與左右括號之間都必須加空格

 【推薦】4.不論什麼運運算元左右必須加一個空格。

 【強制】 5.單行字元數限制不超過 120 個,超出須要換行,換行時遵循例如以下原則:

    運運算元與下文一起換行,方法呼叫的點符號與下文一起換行,在括號前不要換行。

  【強制】6.使用空格進行對齊,禁止使用tab對齊。

七、版本管理規範

 1.VXX.XX.XX.XXXXXX.XXXXXX使用四位數進行版本管理,1-2位為主版本號,3-4位元為分支版本號,5-6為次版本號,7-10為修訂號1,11-15。

   【強制】主版本號:從1開始,產品更新換代時+1。之後版本號清零。

   【強制】分支版本號:從0開始,新建分支時+1,之後版本號清零。

   【強制】次版本號:從0開始,新增功能時+1,之後版本號清零

   【推薦】修訂號1:年月日

   【推薦】修訂號2: 小時分

注:實際開發規範和這篇檔案中會有所出入,這篇檔案會在實際的開發中動態修改,請以實際情況為準。

二、繪製功能流程圖

這個功能流程圖指的是當前開發的軟體整體的功能,你也可以理解為從程式載入開始,到程式結束,中間可能會經過哪些事,可能會儲存哪些資料,可能會呼叫哪些介面。需要提前把這些東西規劃清楚。

這一步並不需要你精確到每一個方法或者屬性,而只需要確定步驟內容即可。即你可以不需要知道每個類的內容,只需要知道每個類要做什麼,能做什麼,為什麼要這麼做即可。這麼做的目的是為了給開發指明一條道路,接下來的開發就可以根據這個流程圖從初始化開始一步步向下開發下去。也可以在繪製這個流程圖的時候劃分模組,進行分工,指定開發計劃。

為了更加具象化這部分內容,我可以拿我之前繪製的功能流程圖來作為參考

這部分繪圖可讀性不強,主要原因是為了列印好看,兩張紙貼在一起可以展示給領導看哈哈。就意思意思就行。

這樣一幅圖就把整個框架內部提供的類、功能模組劃分清楚了,並提供了一個大概的開發方向。後續的開發就按圖索驥即可。這樣不僅對流程更加清除,同時也能好地指定計劃。至於每個模組類內提供的方法和屬性是可以在開發中慢慢商榷的。

不足的是,我並不是軟體工程科班出生的學生,對於UML圖和功能流程圖的繪製並不清楚,只能大致的畫一下我想要表達的內容。之後的時間裡我會抽空好好學習一下如何做專案管理。

三、進行合理分工

這個要根據實際模組開發進行分工,這裡不做討論。實際在進度管理中要善用甘特圖。

四、自定義開發原則

在實際的開發中,我們可能需要根據這個產品的實際應用場景或者開發背景,來決定這個產品的開發原則。如果我現在開發的這個產品是一個需要保證保密性的產品,那這個產品就需要以穩定、可靠為第一要義;如果是以長期開發、長期維護為第一要義,那麼產品的健壯性、可維護性就必須要在設計之初就考慮清楚了。

我們這個專案維護了差不多十年,到我手上重新開發,那麼這個產品很顯然就是一個需要長期維護的程式碼。那麼這個產品中模組的獨立開發性、程式碼的可讀性、介面呼叫的簡便性就是我們程式開發中主要關注的地方。

除了開發的原則,我們還需要確定資料的流動原則。我們這個產品現在是多模組開發,原來的框架中資料流動比較自由(亂),導致維護的時候非常難找資料的流向,有可能這個資料一下子就走到百八十里外的模組裡面去了,但是去找是非常困難的。

所以我們需要在開發前規定好資料的原則:

一:與其他終端的資訊交流中,可以不必表明來源,但必須標明終點

二、內部資料統一採用Qt自帶的系統資料型別,比如QString qint32等,陣列統一用QList。對外介面的資料統一使用標準系統型別,比如int string bool等

三、引數命名的一致性,詳情參考前面開發規範

除了基本原則以外,我們還可能會遇到對外發布結構體等情況。但是我們的專案實際上為了保證介面的便捷性,採取了COM元件對外暴露介面工具資料的方式,這麼做的話就會涉及到一個類物件資料的封裝和解析,這裡我造了個輪子如下【QtJson】用Qt自帶的QJson,直接一步到位封裝和解析一個類的範例物件!

內容大致如下:

我們現在的要求就是直接在不知道類成員的情況下,把一個類丟進去就能生成一個Json字串,也可以把一個字串和一個類成員丟進去就能根據成員變數名匹配到元素並賦值,大概就這樣

中心思想就是Q_PROPERTY宏提供了一個property型別,可以直接通過變數名稱獲得一個變數名稱對應的字串,比如int a;可以直接獲得一個"a"的字串,而且還可以知道這個a 的型別。並據此來進行字串的封裝和解析。

主要是為了開發方便,就可以直接把一個QObject物件扔進去返回一個字串,也可以把一個Json字串和指定類的物件扔進去就直接自動把類中對應的屬性修改了,總的來說隨拿隨用。

#pragma region JsonMaker
	//JsonMaker類使用方法:


//Json相關
//給定任意模板類,將其公開屬性打包成一個Json字串,使用此方法需要所有的公開屬性均為Q_PROPERTY宏宣告,該類提供單例。
//序列化類Q_PROPERTY宏宣告的屬性 set/get函數命名規則:get/set+屬性名 如getBirthday setList,大小寫不限,如果是set方法需要在set方法前面加上Q_INVOKABLE 宏
//如果需要反序列化陣列,請保證陣列中的所有資料結構是同一個型別,否則可能會出錯
//注:請儘量使用int不要使用qint32,使用double不要使用float
	class JsonMaker :public QObject {
		JsonMaker();
		//提供單例
	public:
		static JsonMaker& JsonMaker::Singleton() {
			static JsonMaker Instance;
			return Instance;
			// TODO: 在此處插入 return 語句
		}

		//序列化類Q_PROPERTY宏宣告的屬性,如果有陣列型別,請使用QList
		template<class T1>
		QString JsonSerialization(T1& T_Class_1) {
			auto T_Class = dynamic_cast<QObject*>(&T_Class_1);
			QJsonObject jsonObject;
			//通過元物件定義成員
			const QMetaObject* metaObject = T_Class->metaObject();
			for (int i = 0; i < metaObject->propertyCount(); ++i) {
				QMetaProperty property = metaObject->property(i);

				if (!property.isReadable()) {
					continue;
				}
				//這個不知道是什麼,暫時需要先遮蔽掉

				if (QString(property.name()) == "objectName") {
					continue;
				}

				//如果是QList

				if (QString(property.typeName()).contains("QList")) {
					//這裡可能要根據常見型別進行一下分類
					QJsonArray jsonListArray;
					//輸入一個模板類型別,輸出一個jsonObject
					if (QString(property.typeName()) == "QList<QString>") {
						QList<QString> str_message = property.read(T_Class).value<QList<QString>>();
						jsonListArray = QListToJsonArray(str_message);
					}
					else if (QString(property.typeName()) == "QList<qint32>" || QString(property.typeName()) == "QList<int>") {
						QList<qint32> str_message = property.read(T_Class).value<QList<qint32>>();
						jsonListArray = QListToJsonArray(str_message);
					}
					else if (QString(property.typeName()) == "QList<qint64>") {
						QList<qint64> str_message = property.read(T_Class).value<QList<qint64>>();
						jsonListArray = QListToJsonArray(str_message);
					}
					else if (QString(property.typeName()) == "QList<int>") {
						QList<int> str_message = property.read(T_Class).value<QList<int>>();
						jsonListArray = QListToJsonArray(str_message);
					}
					else if (QString(property.typeName()) == "QList<bool>") {
						QList<bool> str_message = property.read(T_Class).value<QList<bool>>();
						jsonListArray = QListToJsonArray(str_message);
					}
					else if (QString(property.typeName()) == "QList<double>") {
						QList<double> str_message = property.read(T_Class).value<QList<double>>();
						jsonListArray = QListToJsonArray(str_message);
					}
					else if (QString(property.typeName()) == "QList<Float>") {
						QList<float> str_message = property.read(T_Class).value<QList<float>>();
						jsonListArray = QListToJsonArray(str_message);
					}
					else if (QString(property.typeName()) == "QList<QByteArray>") {
						QList<QByteArray> str_message = property.read(T_Class).value<QList<QByteArray>>();
						jsonListArray = QListToJsonArray(str_message);
					}

					jsonObject.insert(property.name(), QJsonValue(jsonListArray));
				}
				//如果不是QList
				else {
					QVariant result = property.read(T_Class);
					jsonObject[property.name()] = QJsonValue::fromVariant(property.read(T_Class));

				}
				qDebug() << property.name();
			}

			QJsonDocument doc(jsonObject);
			return doc.toJson(QJsonDocument::Compact);
		}
		//反序列化類Q_PROPERTY宏宣告的屬性,如果有陣列型別,請使用QList
		template<class T>
		void JsonDeserialization(T& T_Class, const QString& jsonString)
		{
			auto qobject = dynamic_cast<QObject*>(&T_Class);
			QJsonDocument doc = QJsonDocument::fromJson(jsonString.toUtf8());
			QJsonObject jsonObject = doc.object();
			// 使用QMetaObject的invokeMethod()函數來呼叫模板類T的setter函數
			const QMetaObject* metaObject = qobject->metaObject();

			for (int i = 0; i < metaObject->propertyCount(); ++i) {
				QMetaProperty property = metaObject->property(i);
				if (property.isReadable() && property.isWritable()) {
					QString propertyName = property.name();
					QString str_functinoname = QString("set" + propertyName);
					//為了轉換成const char*型別必須的一箇中間步驟
					QByteArray temp_qba_functinoname = str_functinoname.toLocal8Bit();
					const char* func_name = temp_qba_functinoname.data();

					if (jsonObject.contains(propertyName)) {
						QJsonValue value = jsonObject[propertyName];
						JsonMaker temp;
						qDebug() << value;
						switch (value.type()) {
						case QJsonValue::Type::Bool:
							QMetaObject::invokeMethod(qobject, func_name, Q_ARG(bool, value.toBool()));
							break;
						case QJsonValue::Type::Double:
							QMetaObject::invokeMethod(qobject, func_name, Q_ARG(double, value.toDouble()));
							break;
						case QJsonValue::Type::String:
							QMetaObject::invokeMethod(qobject, func_name, Q_ARG(QString, value.toString()));
							break;
						case QJsonValue::Type::Array: {
							//如果是陣列則需要根據情況進行解析
							if (!value.isArray()) {
								break;
							}
							QJsonArray arr = value.toArray();
							//下面確定陣列型別
							this->JsonArrayDeserialization(qobject, func_name, arr);
						}
													break;
						case QJsonValue::Type::Object:
							QMetaObject::invokeMethod(qobject, func_name, Q_ARG(QJsonValue, value));
							break;
						default:
							break;
						}
					}
				}
			}
		}

	private:
		//將模板類QList轉換成JsonObject
		template<class T>
		QJsonArray QListToJsonArray(QList<T> list) {
			QJsonArray jsonArray;
			for each (T temp_T in list)
			{
				jsonArray.append(QJsonValue::fromVariant(temp_T));
			}

			return jsonArray;
		}

		//解析陣列並注入QObject物件
		void JsonArrayDeserialization(QObject* qobject, const char* func_name, QJsonArray arr) {
			try {
				//判斷型別
			   //QString
				if (arr[0].type() == QJsonValue::String) {
					QList<QString> list_result;
					QJsonValue value;

					for each (QJsonValue temp_value in arr)
					{
						list_result.append(temp_value.toString());
					}
					QMetaObject::invokeMethod(qobject, func_name, Q_ARG(QList<QString>, list_result));

				}
				else if (arr[0].isDouble()) {
					//若為為整形
					if (arr[0].toDouble() == arr[0].toInt()) {
						qDebug() << arr[0].toDouble() << arr[0].toInt();
						QList<qint32> list_result;
						QList<int> list_result_2;
						QJsonValue value;

						for each (QJsonValue temp_value in arr)
						{
							//int 和 qint32都需要嘗試,但請儘量嘗試使用qint32,這段程式碼佔用了兩倍的記憶體,將來可能考慮刪除
							list_result.append(temp_value.toInt());
							list_result_2.append(temp_value.toInt());
						}
						if (!QMetaObject::invokeMethod(qobject, func_name, Q_ARG(QList<qint32>, list_result))) {
							QMetaObject::invokeMethod(qobject, func_name, Q_ARG(QList<int>, list_result_2));
						}
					}
					//若為雙精度
					else {
						QList<double> list_result;
						QList<float> list_result_2;
						QJsonValue value;

						for each (QJsonValue temp_value in arr)
						{
							list_result.append(temp_value.toDouble());
						}
						//double和float都會嘗試,請儘量使用double
						if (!QMetaObject::invokeMethod(qobject, func_name, Q_ARG(QList<double>, list_result))) {
							QMetaObject::invokeMethod(qobject, func_name, Q_ARG(QList<float>, list_result_2));
						}

					}
				}if (arr[0].type() == QJsonValue::Bool) {
					QList<bool> list_result;
					QJsonValue value;

					for each (QJsonValue temp_value in arr)
					{
						list_result.append(temp_value.toBool());
					}
					QMetaObject::invokeMethod(qobject, func_name, Q_ARG(QList<bool>, list_result));
				}
			}
			catch (const QException& e) {
				WriteErrorMessage("JsonArrayDeserialization", "JsonArrayDeserialization", e.what());
			}
		}

	};
#pragma endregion

//Json相關方法呼叫範例:
//
// 如果想要呼叫JsonMaker類來把你的類成員元素,假設是A a,其中包含元素qint32 a1,QString a2,bool a3進行封裝,那麼你需要使用Q_PROPERTY來
// 宣告封裝a1,a2,a3元素和其set/get方法(如果需要解析就需要set方法,如果需要封裝就需要get方法),set/get方法命名規則為set/get+元素名稱
// 比如seta1,geta2,其中不對大小寫做規定,也可以寫成setA1,getA2
// 
// 呼叫方法如下:
// 1.封裝字串
// A a; 
// QString result = JsonMaker::Singleton().JsonSerialization<Tester1>(tester);
// 2.解析字串
// A a
// JsonMaker::Singleton().JsonDeserialization<Tester1>(a, Json);
// 呼叫完畢後a中的對應資料都會被Json字串中的資料覆蓋
//