软件工程基础 结对项目作业
一点说明
这篇博客是软件工程基础(罗杰、任建)的第三次课程作业(个人项目作业)
项目 | 内容 |
---|---|
这个作业属于哪个课程 | 软件工程基础(罗杰,任建) |
这个作业的要求在哪里 | 结对项目作业 |
我在这个课程的目标是 | 提升对软件工程的宏观和微观的全面认识,并加以实践 |
作业在哪些方面帮我实现目标 | 亲身实践结对编程开发的完整流程 |
我的教学班级 | 006 |
GitHub项目地址 | 项目地址(源代码与命令行):https://github.com/AmanogawaSaya/CookWork.git 项目地址(GUI界面):https://github.com/AmanogawaSaya/IntersectGUI.git |
PSP表格
在开始实现程序之前,在下述 PSP 表格记录下你估计将在程序的各个模块的开发上耗费的时间。(0.5')
在你实现完程序之后,在下述 PSP 表格记录下你在程序的各个模块上实际花费的时间。(0.5')
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 20 |
· Estimate | · 估计这个任务需要多少时间 | 30 | 20 |
Development | 开发 | 880 | 1350 |
· Analysis | · 需求分析 (包括学习新技术) | 400 | 600 |
· Design Spec | · 生成设计文档 | 30 | 20 |
· Design Review | · 设计复审 (和同事审核设计文档) | 10 | 10 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 20 | 20 |
· Design | · 具体设计 | 30 | 60 |
· Coding | · 具体编码 | 300 | 400 |
· Test | · 测试(自我测试,修改代码,提交修改 | 90 | 240 |
Reporting | 报告 | 75 | 110 |
· Test Report | · 测试报告 | 45 | 60 |
· Size Measurement | · 计算工作量 | 10 | 10 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 20 | 40 |
Total | 合计 | 985 | 1480 |
阅读教材
看教科书和其它资料中关于 Information Hiding,Interface Design,Loose Coupling 的章节,说明你们在结对编程中是如何利用这些方法对接口进行设计的。(5')
- 信息隐藏(Information Hiding):在GUI的设计中,所有的方法都设置在私有域中;在核心模块中,采用的是结构体进行定义点、线,为了使代码简洁,采用的Public的方法,而没有将其隐藏;
- 接口设计(Interface Design):本次的项目的核心模块设计中,我们采用的其实是 面向过程 的解法,同时遵循迪米特法则(一个类应该对自己需要耦合或调用的类知道得最少,被耦合或调用的类的内部是如何复杂与被调用者无关)、里氏替换原则(所有引用基类的地方必须能透明地使用其子类的对象)等;
- 松散耦合(Loose Coupling):这一点主要体现在核心计算模块与图形界面模块的分离实现,方便了各自的开发与测试,也提高了模块的可移植性。
计算模块接口的设计与实现
设计包括代码如何组织,比如会有几个类,几个函数,他们之间关系如何,关键函数是否需要画出流程图?说明你的算法的关键(不必列出源代码),以及独到之处。(7')
各个接口的作用已在注释中阐明,实现如下:
-
isNum:通过正则表达式来检查一个数字是否合法
-
rangeVaild:通过return n > -100000 && n < 100000来检查范围的合法性
-
inputCheck:通过使用以上两个函数来完成对输入数据的检查并赋值,重载了两个函数,分别针对圆和直线
-
calLineLineIst:使用公式法来求直线交点
-
calLineCircleIst:使用公式法来求直线交点
-
calCircleCircleIst:将两圆交点转换成圆与直线的交点,调用calLineCirclelst来求解
-
calculate:根据文件流处理输入、使用以上6个函数来检查输入合法性、求解交点,并输出
-
Line类:设计了两个方法,一个是检查直线是否平行,另一个是检查直线是否重合,都使用了公式法
-
Ray类:继承了Line类,重写了父类检查是否重合的方法,并增加了检查点是否在射线上的方法
-
Line类:继承了Ray类,重写了检查重合与点在线段上的方法
计算模块部分沿用了本人上一次作业的计算方式,博客
需要增加的关键方法为检查交点是否在线段/射线上,以及两条线段\直线\射线之间重合的情况,这个比较复杂,也是通过数学的计算以后分情况讨论。
//判断是否为数字,否则抛出异常 __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; //CircleL类,代表圆 class Circle
UML图
阅读有关 UML 的内容:https://en.wikipedia.org/wiki/Unified_Modeling_Language。画出 UML 图显示计算模块部分各个实体之间的关系(画一个图即可)。(2’)
计算模块接口部分的性能改进
记录在改进计算模块性能上所花费的时间,描述你改进的思路,并展示一张性能分析图(由VS 2015/2017的性能分析工具自动生成),并展示你程序中消耗最大的函数。(3')
性能分析结果:
其中消耗最大的函数为Calculate,而unordered_set的维护了耗费了近一半的时间,为此我也查阅了关于容器效率的资料但是似乎库函数提供的已经是比较优的算法了,而计算手段上也通过减少浮点类型的运算进行了浅层的加速,inputCheck等函数则是为了异常处理而牺牲的性能。
阅读博客
看 Design by Contract,Code Contract 的内容:
- http://en.wikipedia.org/wiki/Design_by_contract
- http://msdn.microsoft.com/en-us/devlabs/dd491992.aspx
描述这些做法的优缺点,说明你是如何把它们融入结对作业中的。(5')
-
契约式设计(Design by Contract)
在面向对象课程中,我们曾经体验过契约式编程的一部分。为什么说是一部分呢?因为当时助教们已经将 JML 写好了,我们只需要遵守规则规范,填入代码就可以了。那体验到底好不好呢?当然好!在JML的引导下,我们很轻松地完成了一个可能自己写起来挺有难度的编程题目。但是,这其中还有一个问题,因为这是一种形式约束,包括了前置条件、后置条件和不变式,其实这些设计将浪费很大的时间和精力,对于小工程小项目来说,可能并不适用。
-
Code Contract
意译过来是“代码合约”,其实这与契约式设计的理念类似,长期来看。在良好的合约之下,开发的效率应该是提高的,同时,合约最大的作用是能够较大程度地确保程序的正确性。
-
如何融入
在本次结对项目中,我们没有编写具体的约束文件,在分工上,我主要负责编写GUI,另一位组员主要负责核心模块的扩充,因为工程需求相对简单,只需要互相明确接口,了解各自的设计即可。
计算模块部分单元测试展示
展示出项目部分单元测试代码,并说明测试的函数,构造测试数据的思路。并将单元测试得到的测试覆盖率截图,发表在博客中。要求总体覆盖率到 90% 以上,否则单元测试部分视作无效。(6')
单元测试部分和上次类似,但是增加了异常处理部分和直线与射线交点部分,例如测试数字是否合法:
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例来检查正则表达式是否正确
在其他模块的测试也是类似,构造正确样例和错误样例来检查模块功能是否正确,对于没有异常抛出计算模块则和上次作业一样分情况讨论:
单元测试覆盖率:
计算模块部分异常处理说明
在博客中详细介绍每种异常的设计目标。每种异常都要选择一个单元测试样例发布在博客中,并指明错误对应的场景。(5')
异常处理模块我们设计了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 | 打开文件失败 | 文件不存在 | 打开文件失败! |
界面模块的详细设计过程
在博客中详细介绍界面模块是如何设计的,并写一些必要的代码说明解释实现过程。(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 -
界面模块与计算模块的对接
详细地描述 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(); } }
-
最终实现的功能
-
支持从文件导入几何对象的描述
-
支持几何对象的添加、删除、修改
-
支持绘制现有几何对象(请见上图)
-
支持求解现有几何对象交点并绘制
-
其他功能:右上角缩放滑块可以调整画布大小,滑动条可以调整画布交点。
-
描述结对的过程
提供两人在讨论的结对图像资料(比如 Live Share 的截图)。关于如何远程进行结对参见作业最后的注意事项。(1')
我们采用的结对编程方式是腾讯会议+Live Share,主要采用的腾讯会议,因为Live Share实在是太不稳定了!
结对的优缺点
看教科书和其它参考书,网站中关于结对编程的章节,例如:http://www.cnblogs.com/xinz/archive/2011/08/07/2130332.html ,说明结对编程的优点和缺点。同时描述结对的每一个人的优点和缺点在哪里(要列出至少三个优点和一个缺点)。(5')
-
结对编程的优缺点
-
优点
一个看起来棘手的问题,简单交流一下,可能就会从对话中获取到灵感;
如果两个人的能力和兴趣比较互补,可以起到事半功倍的效果!
心理负担小,写代码比较有底气,遇到困难不怂……
-
缺点
有时候会出现一个人被另一个人的错误想法带进坑里,两个人都出不来的那种……
-
-
描述每一个人的优缺点
我 同伴 优点 乐于学习新知识,积极同组员以及其他组进行交流沟通,有耐心 编码效率高,编程基础扎实,思考较仔细 缺点 编程效率低 耐心不足
* 附加题 模块之间的松耦合
在博客中指明合作小组两位同学的学号,截图展示互换后的运行结果和测试结果。此外,博客中还需分析两组不同的模块合并之后出现的问题,为何会出现这样的问题,以及是如何根据反馈改进自己模块的。
-
合作小组同学的学号
我方 对方 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)
-
-
合并后出现的问题分析
-
接口不一致
俗话说,“凡事预则立,不预则废”,诚不我欺。最初进行项目规划的时候,没能正确理解作业要求中 “松散耦合” 的含义,没有提前找好对接的小组并对接口进行统一约定。这直接的后果就是,在进行对接的时候,面临了极大的问题:为了降低项目内各函数的耦合度,我们小组在开发时尽量避免 全局变量 的使用,采用函数传参的方式进行各个容器和特征值的修改;但是对方小组在开发时大量采用了全局变量,包括图形容器等。
为了实现 “DLL直接交换”,我们不得不选择了妥协,在新的分支中,增加了对方小组的所有接口。
同时,为了GUI也能进行匹配,我们重新改写了GUI的计算模块,调用了新的接口。
-
DLL加载失败
最初DLL加载失败,抛出异常,经过探索,发现这一异常是由X86与X64不同编译环境有关,我们在统一的
Release X64
环境下重新构建了项目,同时选择同一版本的 Qt,最后成功调用了对方的 DLL!
最初对接DLL时,两个小组都出现了同样的异常
-