BUAA软工-结对项目作业
结对项目作业
项目 | 内容 |
---|---|
这个作业属于哪个课程 | 2020春季计算机学院软件工程(罗杰 任健) |
这个作业的要求在哪里 | 结对项目作业 |
我在这个课程的目标是 | 通过这门课锻炼软件开发能力和经验,强化与他人合作的能力 |
这个作业在哪个具体方面帮助我实现目标 | 体验结对编程的模式 |
1.项目Github地址
- 教学班级:006(周五)
由于GUI生成的文件过大(20MB)因此将代码和生成的GUI界面分别放在了两个仓库
- 项目地址(源代码与命令行):https://github.com/AmanogawaSaya/CoopWork.git
- 项目地址(GUI界面):https://github.com/AmanogawaSaya/IntersectGUI.git
2.程序的各个模块的开发上耗费的时间
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 5 | 5 |
· Estimate | · 估计这个任务需要多少时间 | 5 | 5 |
Development | 开发 | 1,160 | 1,160 |
· Analysis | · 需求分析 (包括学习新技术) | 180 | 180 |
· Design Spec | · 生成设计文档 | 15 | 15 |
· Design Review | · 设计复审 (和同事审核设计文档) | 10 | 10 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 | 10 |
· Design | · 具体设计 | 45 | 45 |
· Coding | · 具体编码 | 600 | 600 |
· Test | · 测试(自我测试,修改代码,提交修改 | 240 | 240 |
Reporting | 报告 | 30 | 40 |
· Test Report | · 测试报告 | 10 | 10 |
· Size Measurement | · 计算工作量 | 10 | 10 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 10 | 20 |
合计 | 1,195 | 1,205 |
这次在构思代码上想了很久,学习Qt花费的时间比较多,因此总花费的时间也比较多,时间都用在学习新知识上了。
3.看教科书和其它资料中关于 Information Hiding,Interface Design,Loose Coupling 的章节,说明你们在结对编程中是如何利用这些方法对接口进行设计的
信息隐藏、接口设计和松耦合在面向对象课程中都有学习、应用过,在这里再次应用了。
-
Information Hiding
David Parnas在1972年最早提出信息隐藏的观点。他在其论文中指出:代码模块应该采用定义良好的接口来封装,这些模块的内部结构应该是程序员的私有财产,外部是不可见的。
信息隐藏原则本意是希望类里面定义的变量和结构应当按照一定的原则分配可见性,从而防止模块内容被恶意篡改,但考虑到这次作业的规模,以及对象类型,每个几何元素更倾向于类似结构体的结构,并不是一个真正的模块,因此在结对编程中我们考虑了以后还是将类里面的属性定义成了public,减少了代码的复杂性。
-
Interface Degisn
接口设计有六大原则:
单一职责原则:应该有且仅有一个原因引起类的变更。
里氏替换原则:所有引用基类的地方必须能透明地使用其子类的对象。
依赖倒置原则:面向接口编程
接口隔离原则:建立单一接口,不要建立臃肿庞大的接口。
迪米特法则:一个类应该对自己需要耦合或调用的类知道得最少,你(被耦合或调用的类)的内部是如何复杂都和我没有关系,那是你的事情,我就调用你提供的public方法,其他一概不关心。
开闭原则:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
这次作业并没有体现出很多面向对象的特性,但是我们也遵循了迪米特法则、里氏替换原则等,除了防止一个函数过于冗长而拆分成几个小函数之外,各个模块的功能独立。
-
Loose Coupling
一个松耦合的系统中的每一个组件对其他独立组件的定义所知甚少或一无所知。
本次作业在函数调用中,每个通信的参数都是基本类型的参数,可以直接调用,没有必要为互相的实现考虑。
4.计算模块接口的设计与实现过程
//判断是否为数字,否则抛出异常 __declspec(dllexport) bool isNum(std::string s); //判断范围是否合理,否则抛出异常 __declspec(dllexport) bool rangeVaild(int n); //检查直线类型输入是否合法,是则更改x1, x2, y1, y2的值,否则抛出异常 __declspec(dllexport) void inputCheck(ifstream& fileIn, int& x1, int& y1, int& x2, int& y2); //检查圆类型的输入是否合法,是则更改x, y ,r的值,否则抛出异常 __declspec(dllexport) void inputCheck(ifstream& fileIn, int& x, int& y, int& r); // 计算两直线的交点 __declspec(dllexport) Point* calLineLineIst(Line line1, Line line2); // 计算圆与直线的交点 __declspec(dllexport) vector<Point> calLineCircleIst(Line line, Circle circle); // 计算两圆交点 __declspec(dllexport) vector<Point> calCircleCircleIst(Circle circle1, Circle circle2); //计算交点 __declspec(dllexport) MySet calculate(ifstream& fileIn, ofstream& fileOut); //line类,代表直线 class Line; //Ray类,代表射线,继承了Line类 class Ray; //Segment类,代表线段,继承了Ray类 class Segment; //Circle类,代表圆 class Circle
各个接口的作用已在注释中阐明,实现如下:
-
isNum:通过正则表达式来检查一个数字是否合法
-
rangeVaild:通过return n > -100000 && n < 100000来检查范围的合法性
-
inputCheck:通过使用以上两个函数来完成对输入数据的检查并赋值,重载了两个函数,分别针对圆和直线
-
calLineLineIst:使用公式法来求直线交点
-
calLineCircleIst:使用公式法来求直线交点
-
calCircleCircleIst:将两圆交点转换成圆与直线的交点,调用calLineCirclelst来求解
-
calculate:根据文件流处理输入、使用以上6个函数来检查输入合法性、求解交点,并输出
-
Line类:设计了两个方法,一个是检查直线是否平行,另一个是检查直线是否重合,都使用了公式法
-
Ray类:继承了Line类,重写了父类检查是否重合的方法,并增加了检查点是否在射线上的方法
-
Line类:继承了Ray类,重写了检查重合与点在线段上的方法
计算模块部分沿用了上一次作业的计算方式,因此没有变化,我采用的是我的搭档的计算方法:博客
需要增加的关键方法为检查交点是否在线段/射线上,以及两条线段\直线\射线之间重合的情况,这个比较复杂,也是通过数学的计算以后分情况讨论。
5.画出 UML 图显示计算模块部分各个实体之间的关系
考虑到射线和线段是一种特殊的直线,因为它们可以看成是直线截断形成的,因此产生了继承的想法,子类通过重写父类方法来实现自己的个性。
6.计算模块接口部分的性能改进
性能分析结果:
其中消耗最大的函数为Calculate,而unordered_set的维护了耗费了近一半的时间,为此我也查阅了关于容器效率的资料但是似乎库函数提供的已经是比较优的算法了,而计算手段上也通过减少浮点类型的运算进行了浅层的加速,inputCheck等函数则是为了异常处理而牺牲的性能。
7.看 Design by Contract,Code Contract 的内容,描述这些做法的优缺点,说明你是如何把它们融入结对作业中的
-
Design by Contract
即契约式设计,在面向对象课程中专门有一个单元让我们通过JML来体会契约式设计的思想,其强调前置条件、后置条件与不变式,是一种形式约束。也就是说,只要满足了这个条件,那么所设计的模块在理论上就一定是正确的,非常可靠,但是缺点是契约撰写的成本比较高,在复杂的模块中会很麻烦,也变得不易阅读。
-
Code Contract
和DBC的思想类似,优点也是使得模块变得可靠、安全,但是缺点是需要牺牲一部分的性能,增加代码的复杂度,降低运行效率。
本次作业中为了提高编码效率,我们没有过多地使用契约式设计的思想,在一开始就明确定义每个模块的需求,可以有更高的效率。
8.计算模块部分单元测试展示
单元测试部分和上次类似,但是增加了异常处理部分和直线与射线交点部分,例如测试数字是否合法:
TEST_METHOD(isNumTest) { Assert::IsTrue(isNum("0")); Assert::IsTrue(isNum("1")); Assert::IsTrue(isNum("100")); Assert::IsTrue(isNum("-1")); Assert::IsTrue(isNum("-100")); Assert::IsFalse(isNum("001")); Assert::IsFalse(isNum("-001")); Assert::IsFalse(isNum("a")); Assert::IsFalse(isNum("0a")); Assert::IsFalse(isNum("-0a")); }
通过构造错误的样例各1例来检查正则表达式是否正确
在其他模块的测试也是类似,构造正确样例和错误样例来检查模块功能是否正确,对于没有异常抛出计算模块则和上次作业一样分情况讨论:
单元测试覆盖率:
9.计算模块部分异常处理说明
异常处理模块我设计了8种异常
名称 定义 例子 输出 TFException 输入图形个数过少 1 请输入至少两个图形! DSException 用来确定直线两点重合 L 1 1 1 1 用来确定直线的两点不能重合! SLException 两条直线有无穷的交点 S 1 1 3 3
R 0 0 2 2有两个几何图形之间有无穷的交点 TException 图形种类错误 K 1 2 3 4 支持的图形种类仅为:C, L, S, R INException 输入非整数 L 001 a 3 2 坐标请输入一个(-100000, 100000)之间的无前导0标准整数 RIException 圆的半径不合法 C 1 1 -2 圆的半径不可以小于或等于0或者大于或等于100000 ArgumentError 参数数量不对 argc != 5 请检查命令格式: \n\tintersect.exe -i <input> -o <output>\n FileError 打开文件失败 文件不存在 打开文件失败! -
10.界面模块的详细设计过程
界面模块的详细设计过程。在博客中详细介绍界面模块是如何设计的,并写一些必要的代码说明解释实现过程。(5')
本项目的图形化界面采用 VS + Qt 进行开发的,图像的绘制主要使用 QPainter
的 paintEvent
机制。下面按照开发的时间顺序,对GUI的设计过程进行详细介绍。
-
分析需求,确定图形化界面的组件和布局
- 支持从文件导入几何对象的描述:设置文件导入按钮,触发后可选择文件并导入到指定的框目;
- 支持几何对象的添加、删除:综合考虑界面美观与用户交互的便捷性,最后选用鼠标右击菜单的解决方案;
- 支持绘制现有几何对象:考虑到点的范围可达[-100000, 100000],对应画板应满足的尺寸为400000*400000,所以采用
QScrollArea
(滚动区域)与缩放滑块相结合的方法,以达到良好的绘制效果; - 支持求解现有几何对象交点并绘制:这个属于下一部分(界面模块与计算模块的对接)要详细介绍的;
- 我们还支持了对已有图形项的双击修改,同时设置了选项,用来选择是否显示交点。
-
动手编程,实现各组件之间的触发逻辑
-
各组件的布局基本采用Qt Designer进行;
-
设置按钮,触发可导入文件到
QListWidget
//设置打开文件的按钮 connect(ui.pbtn_open, &QPushButton::clicked, [=]() { //打开文件 m_FilePath = QFileDialog::getOpenFileName(this, "Open", ":\\file-path"); //将文件的路径显示到文本条中 ui.le_filepath->setText(m_FilePath); //将文件内容逐项显示到列表中 showList(); }); //将文件内容逐项显示到列表中 void CoopWorkGUI::showList() { if (m_FilePath.size() == 0) { return; } file.open(QIODevice::ReadOnly); if (!file.atEnd()) { graphic = file.readLine(); } while (!file.atEnd()) { graphic = file.readLine(); graphic.remove('\n'); graphics << graphic; } file.close(); ui.listWidget->clear(); ui.listWidget->addItems(graphics); }
-
鼠标右击
QListWidget
的item
,弹出菜单,进行图形的添加和删除,双击可编辑//设置列表项:双击编辑 connect(ui.listWidget, &QListWidget::itemDoubleClicked, this, &CoopWorkGUI::editListItem); //设置列表项:右键菜单 ui.listWidget->setProperty("contextMenuPolicy", Qt::CustomContextMenu); QMenu* popMenu = new QMenu(this); QAction* atn_add = new QAction(tr("Add"), this); QAction* atn_delete = new QAction(tr("Delete"), this); popMenu->addAction(atn_add); popMenu->addSeparator(); popMenu->addAction(atn_delete); connect(atn_add, &QAction::triggered, this, &CoopWorkGUI::onActionAdd); connect(atn_delete, &QAction::triggered, this, &CoopWorkGUI::onActionDelete); connect(ui.listWidget, &QListWidget::customContextMenuRequested, [=]() { popMenu->exec(QCursor::pos()); isRepaint = true; update(); }); //修改 void CoopWorkGUI::editListItem(QListWidgetItem* item) { item->setFlags(item->flags() | Qt::ItemIsEditable); m_EditIndex = ui.listWidget->currentRow(); isRepaint = true; CoopWorkGUI::update(); } //删除 void CoopWorkGUI::onActionDelete() { QList<QListWidgetItem*> items = ui.listWidget->selectedItems(); if (items.count() <= 0) { return; } if (QMessageBox::Yes == QMessageBox::question(this, QStringLiteral("Remove Item") , QStringLiteral("Remove %1 items").arg(QString::number(items.count())) , QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes)) { foreach(QListWidgetItem * var, items) { ui.listWidget->removeItemWidget(var); items.removeOne(var); delete var; } } CoopWorkGUI::update(); } //添加 void CoopWorkGUI::onActionAdd() { ui.listWidget->addItem(tr("")); CoopWorkGUI::update(); }
-
创建
QSpinBox
(右上角小小的显示窗口)与QSlider
(右上角的滑动条),实现数字、滑块位置与画板大小的联系//设置缩放按钮 void(QSpinBox:: * spinboxSignal)(int) = &QSpinBox::valueChanged; connect(ui.spinBox, spinboxSignal, ui.horizontalSlider, &QSlider::setValue); connect(ui.spinBox, spinboxSignal, ui.horizontalSlider, [=]() {isRepaint = true; update(); }); connect(ui.horizontalSlider, &QSlider::valueChanged, ui.spinBox, &QSpinBox::setValue); connect(ui.horizontalSlider, &QSlider::valueChanged, this, &CoopWorkGUI::resizeWidget);
-
创建可以滚动的画板(直接在
QScollArea
上无法作画)//设置绘图区域:滚动条 QScrollArea* scrollArea = new QScrollArea(ui.widget); scrollArea->setWidget(ui.wdt_scroll); ui.wdt_scroll->setMinimumSize(1000, 1000); QHBoxLayout* pLayout = new QHBoxLayout(); pLayout->addWidget(scrollArea); pLayout->setMargin(0); pLayout->setSpacing(0); ui.widget->setLayout(pLayout);
-
绘画逻辑的实现:每次图形列表有更新(添加、删除、重新载入文件),相应地对图形进行更新
//设置事件分发器 ui.wdt_scroll->installEventFilter(this); bool CoopWorkGUI::eventFilter(QObject* obj, QEvent* ev) { if (obj == ui.wdt_scroll && ev->type() == QEvent::Paint && isRepaint) { //画图 isRepaint = false; painteGraphics(); return true; } else { return QWidget::eventFilter(obj, ev); } }
-
-
测试与美化
-
主要测试:列表的增删改、图形的绘制、各按钮的触发是否与预期一致。
测试结果:列表增删改无问题;图形绘制中增加或修改列表项时图形的重绘不及时;各按钮触发与预期一致,但是同样存在图形重绘不及时的情况。
错误溯因:
paintEvent
是由update()
函数所发送的信号触发的,但是当我们手动调用update()
函数时,Qt 并不会立即调用paintEvent
进行重绘,它会先自动进行一个需不需要重绘的判断,决定是否重绘,从而导致图形绘制与预期不符。 -
美化:主要对界面的控件进行了二次调整,增加了显示交点个数的小显示窗口,并调整了绘制窗口的背景颜色。
最终版的GUI -
11.界面模块与计算模块的对接
界面模块与计算模块的对接。详细地描述 UI 模块的设计与两个模块的对接,并在博客中截图实现的功能。(4')
-
DLL调用:DLL调用花费了较大的精力,最初我们企图使用 .dll+.h 的显示调用方法,利用
QLibrary
的load
方法加载DLL库,但是很不幸,失败了。于是我们选用了 .dll+.h+.lib 的隐式调用方法,最终成功加载了DLL,具体调用方法如下,-
核心模块的接口定义,加关键字
__declspec(dllexport)
__declspec(dllexport) MySet result(vector<string>) fileIn;
-
VS项目属性中,配置属性 - 常规 - 配置类型 改为
动态链接库(.dll)
,重新生成解决方案后,在相应目录就会有.lib
和.dll
文件; -
将
.lib
.dll
和 (需要用到的).h
文件拷贝到GUI项目的目录中; -
在GUI的头文件中添加下述语句,隐式调用DLL库
#pragma comment(lib, "CoopWork.lib")
-
将需要的.h文件包含进GUI项目中,并对接口定义进行相应的修改
__declspec(dllimport) MySet result(vector<string>) fileIn;
-
现在就可以直接使用接口函数进行计算啦!
-
-
完成计算:这一步很简单,就是调用计算模块已经封装好的接口函数,输入图形容器,输出交点容器,遍历交点容器进行交点绘制即可,代码如下
if (!ui.radioButton->isChecked()) { return; } //s_graphics中存放的是当前列表中的图形参数 if (s_graphics.size() > 0) { s_graphics.insert(s_graphics.begin(), to_string(s_graphics.size())); for (int i = 0; i < s_graphics.size(); i++) { qDebug() << QString::fromStdString(s_graphics.at(i)); } try { m_Points = result(s_graphics); qDebug() << m_Points.size(); for each (Point var in m_Points) { painter.drawPoint(QPointF(var.x * m_scale, var.y * m_scale)); qDebug() << var.x << var.y; } ui.label->setText("Total: " + QString::number(m_Points.size())); } catch (const std::exception& e) { qDebug() << e.what(); } }
-
最终实现的功能
-
支持从文件导入几何对象的描述
-
支持几何对象的添加、删除、修改
-
支持绘制现有几何对象(请见上图)
-
支持求解现有几何对象交点并绘制
-
其他功能:右上角缩放滑块可以调整画布大小,滑动条可以调整画布交点。
-
12.描述结对的过程
我们使用了腾讯会议进行交流:
13.看教科书和其它参考书,网站中关于结对编程的章节,说明 结对编程的优点和缺点。同时描述结对的每一个人的优点和缺 点在哪里(要列出至少三个优点和一个缺点)
结对编程 | 我 | 同伴 | |
---|---|---|---|
优点 | 提高编码效率、在开发阶段能尽早发现bug、交流效率较高 | 编码效率高、编程基础扎实、思考较仔细 | 乐于学习新知识、接受能力强、交流积极 |
缺点 | 容易造成思维定势,两人都无法发现bug、出现分歧较难处理 | 耐心不足 | 有点粗心 |
14.附加题
模块之间的松耦合
在博客中指明合作小组两位同学的学号,截图展示互换后的运行结果和测试结果。此外,博客中还需分析两组不同的模块合并之后出现的问题,为何会出现这样的问题,以及是如何根据反馈改进自己模块的。
-
合作小组同学的学号
我方 对方 LJC: 17373456 WXC: 17373459 SYB: 17373452 SXD: 17231151 -
互换后的运行结果与测试结果
完成对接的项目地址:https://github.com/AmanogawaSaya/IntersectGUI
在项目文件中的 “DLL_对接” 文件夹中,分开存放了我方的DLL文件与对方的DLL文件,将想要测试的DLL文件复制到与 CoopWorkGUI.exe 同级的目录下,双击.exe即可运行.
-
A 的核心模块,加上 B 的测试模块和用户界面模块(命令行和 GUI)
-
B 的核心模块,加上 A 的测试模块和用户界面模块(命令行和 GUI)
由于双方的接口函数都是void类型的函数,因此我们以最终结果为依据进行了测试,测试结果均正确。
-
-
合并后出现的问题分析
-
接口不一致
俗话说,“凡事预则立,不预则废”,诚不我欺。最初进行项目规划的时候,没能正确理解作业要求中 “松散耦合” 的含义,没有提前找好对接的小组并对接口进行统一约定。这直接的后果就是,在进行对接的时候,面临了极大的问题:为了降低项目内各函数的耦合度,我们小组在开发时尽量避免 全局变量 的使用,采用函数传参的方式进行各个容器和特征值的修改;但是对方小组在开发时大量采用了全局变量,包括图形容器等。
为了实现 “DLL直接交换”,我们不得不选择了妥协,在新的分支中,增加了对方小组的所有接口。
同时,为了GUI也能进行匹配,我们重新改写了GUI的计算模块,调用了新的接口。
-
DLL加载失败
最初DLL加载失败,抛出异常,经过探索,发现这一异常是由X86与X64不同编译环境有关,我们在统一的
Release X64
环境下重新构建了项目,同时选择同一版本的 Qt,最后成功调用了对方的 DLL!
最初对接DLL时,两个小组都出现了同样的异常
-