【大型软件开发】浅谈大型Qt软件开发(一)开发前的准备——在着手开发之前,我们要做些什么?
前言
最近我们项目部的核心产品正在进行重构,然后又是年底了,除了开发工作之外项目并不紧急,加上加班时间混不够了....所以就忙里偷闲把整个项目的开发思路聊一下,以供参考。
鉴于接下来的一年我要操刀这个主框架的开发,本着精益求精的态度,加上之前维护前辈的产品代码确实给我这个刚毕业的社畜带来了不小的震撼,我决定在这个模块的开发中优化之前的开发模式,提升整个产品的健壮性和独立性。
开发一个大型软件最重要的问题有三个,一是如何保证每个模块开发的独立性 二是如何保证数据结构的一致性 三是如何保证程序的可维护性和健壮性。这几个文章的内容我会在几篇文章中分开聊聊我的做法,做个记录。
本篇文章我们暂时只谈开发准备,虽然不涉及核心问题,但我仍然认为准备阶段也是和开发、测试一样重要的阶段。和后者不同,准备阶段一旦做好了之后可以为后续的开发提供模板,可以大大减少在实际开发中走的弯路。
这一期简单聊聊开发准备,下一期浅谈怎么在开发中我们项目如何保证模块开发的独立性。
开发准备
在大学的软件工程这门课中我们可以知道,软件开发实际上占比时间最长的是前期策划和测试阶段。
实际上开发中这两个阶段占比是最长的也是最折磨的。但是在实际的开发流程中我们会发现:
在准备阶段做的事情越多,开发阶段受的折磨就越少;在测试阶段做的事情越多,在维护阶段受的折磨就越少。
在这里我就简单聊聊在我们当前这个大型软件进行开发之前做了哪些准备,给出一些能给的范例以供参考。
一、制定开发规范
我个人认为,在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字符串中的数据覆盖
//