项目
内容
所属课程
作业要求
课程目标
培养软件开发能力
本作业对实现目标的具体作用
培养结对编程开发项目的能力
教学班级
006
github项目地址
结对伙伴博客
PSP2.1
Personal Software Process Stages
预估耗时(分钟)
实际耗时(分钟)
Planning
计划
· Estimate
· 估计这个任务需要多少时间
10
10
Development
开发
· Analysis
· 需求分析 (包括学习新技术)
40(需求理解)+200(学习技术)
60+300
· Design Spec
· 生成设计文档
20
40
· Design Review
· 设计复审 (和同事审核设计文档)
30
30
· Coding Standard
· 代码规范 (为目前的开发制定合适的规范)
20
30
· Design
· 具体设计
150
200
· Coding
· 具体编码
300
500
· Code Review
· 代码复审
60
80
· Test
· 测试(自我测试,修改代码,提交修改)
240
600
Reporting
报告
· Test Report
· 测试报告
30
30
· Size Measurement
· 计算工作量
15
10
· Postmortem & Process Improvement Plan
· 事后总结, 并提出过程改进计划
20
60
合计
1135
1950
信息隐藏(Information Hiding)原则
In computer science, information hiding is the principle of segregation of the design decisions in a computer program that are most likely to change, thus protecting other parts of the program from extensive modification if the design decision is changed. The protection involves providing a stable interface which protects the remainder of the program from the implementation (the details that are most likely to change)
信息隐藏是指在设计和确定模块时,使得一个模块内包含的特定信息(过程或数据),对于不需要这些信息的其他模块来说,是不可访问的。因此,我们设计的计算类中所有属性都是private,所有访问都是通过访问函数实现的。所有容器访问改为迭代器访问。
接口设计(Interface Design)
参考了一些相关的博客,详见接口设计六大原则
单一责任原则(Single Responsibility Principle, 简称SRP)
There should never be more than one reason for a class to change
考虑到这一原则,我们封装了图形对象的相关类、计算交点的类,异常处理的类等,将各个功能分离。
在Calculator
类中,我们将计算分为了多个层次,分写了多个成员函数。分别对点在线上、点在圆内、平行判断、交点计算(线与线、线与圆、圆与圆、汇总计算)进行各模块的编写
松耦合(Loose Coupling)
In computing and systems design a loosely coupled system is one in which each of its components has, or makes use of,little or no knowledge of the definitions of other separate components.
我们的接口设计遵从高内聚,低耦合的设计思路。在附加题部分,我与我的队友对于接口的设计讨论了很久。我们这次的计算核心模块和GUI以及命令行的对接,增加了中间转换层,来实现系统的核心模块和用户交互层的彻底解耦。下面的代码就是我们核心模块面向GUI和cmd的中间层设计。内部计算实现彻底隐藏,只注明外部调用接口规范,这个内容会在GUI设计部分详细说明。
// IOinterface.h
IMPORT_DLL int guiProcess(std::vector<std::pair<double, double>>* points, std::string msg);//计算模块与GUI的接口
IMPORT_DLL void cmdProcess(int argc, char* argv[]);//计算模块与命令行的接口
我们这次的项目是在上一次的个人项目框架上进行扩展的。
在其基础上,我们做了如下改进:
将原有的Line和Circle结构体(原来的Line和Circle只存储数据,故采用结构体)封装为类。为Line类添加type属性,分别可表示直线,射线,线段。由于这三种线的交点计算步骤较为统一,故在较少改动框架的基础上,我们将这三种线融为一个类型。除此之外,运用叉乘计算,实现了pOnLine(Point p, Line l)
方法来判断点是否在线上。
具体设计如下:
一共六个类
类与类之间的关系
graph LR
A[Point] --> B[Line]
A --> C[Circle]
A --> D[Calcalator]
B --> D
C --> D
D --> E[IOinterface]
D --> F[Exception]
E --> F
E --> G[GUI调用]
E --> H[cmd调用]
关键思路依然是几何对象两两求解交点,其中通过一些预判剪枝优化。由于交点计算的关键函数实现都在上一次作业博客中说明了,这次不做特别阐述,着重说明新增功能的拓展。
Calculator 类
这是由于新增射线和线段需要做比较多改进的地方
其中Line与Line的交点计算的预判(用到叉乘的方法) 参见 博客,无法预判的内容比如射线,则先计算交点,再通过pOnLine(Point p, Line l)
方法来判断点是否在线上。
直线和射线与圆的问题处理,计划先算出交点,再判断点是否在line上
线段与圆关系进行预判,比如线段两点都在圆内时必不存在交点。一定程度的降低时间复杂度
圆与圆的交点无需改动
class Calculator {
public:
Calculator();
inline double xmult(Point v1, Point v2);
double xmult(Point o, Point a, Point b);
//判断点是否在line上 在line上则return true
bool pOnLine(Point p, Line l);
// 圆内 return true; 相切/相离 return false;
bool pInCircle(Point p, Circle c);
bool isParallel(Line l1, Line l2);//判断两线是否平行 (并捕捉 重叠的异常)
int haveIntersection(Line l1, Line l2, set<Point>& nodeSet);
int haveIntersection(Circle c, Line l, set<Point>& nodeSet);
int haveIntersection(Circle c1, Circle c2, set<Point>& nodeSet);
//计算全部交点
int countAllinsect(vector<Line> lVec, vector<Circle> cVec, set<Point> &nodeSet);
};
Line类
新增射线和线段的功能
// 'L' -> line;
// 'R' -> radio;
// 'S' -> segment;
class Line
{
public:
Line();
Line(char newType, double x1, double y1, double x2, double y2);
char getType();
double getA();
double getB();
double getC();
double getSlope();
Point getP1();
Point getP2();
private:
char type;
Point p1;
Point p2;
double A;
double B;
double C;
double slope;
};
最终性能分析图片如下:
占用性能最多的函数还是求直线交点函数,该函数中交点集合的插入部分是占用CPU最多的数据操作。这部分由于set的红黑树原理决定的,无法改善。
契约式设计和代码协定,这种方法要求软件设计者为软件组件定义正式的,精确的并且可验证的接口,这样,为传统的抽象数据类型又增加了先验条件、后验条件和不变式。这与我在OO课程中接触到的jml类似。
优点
缺点
应用
这个方法是很有利于多人协作开发的。虽然我们没有严格按照规范来执行这个方法,但我们也在结对编程中运用到了其思想。在初期,我和我的队友对设计进行了讨论,并确定了设计文档。我俩在里面详细标注了各个类的分工,以及各个函数的作用、输入参数与返回值。
在网上查找,VS的单元测试覆盖率可能需要旗舰版才能完成,所以目前没有得出单元测试的结果,但是我们自己的单元测试编写的有层次有条理,尽可能做到了全面的覆盖。
整体测试框架
根据设计的几大类,采用bottom-up的方式进行测试程序的编写
对于每个类的每一个函数都进行了细密的测试
比如下面展示的对于直线类的测试,细致到每一个函数
TEST_CLASS(LineTest) {
public:
TEST_METHOD(lineTestBasic) {
Line l1('L', 1, 1, 2, 2);
Line l2('R', 0, 2, 1, 0);
Line l3('S', 1, 0, 5, 0); // test getType
Assert::IsTrue(l1.getType() == 'L');
Assert::IsTrue(l2.getType() == 'R');
Assert::IsTrue(l3.getType() == 'S');
// test get abc
Assert::IsTrue((l1.getA() == 1 && l1.getB() == -1) ||
(l1.getA() == -1 && l1.getB() == 1)
&& l1.getC() == 0);
Assert::IsTrue((l2.getA() == -2 && l2.getB() == -1 && l2.getC() == 2) ||
(l2.getA() == 2 && l2.getB() == 1 && l2.getC() == -2));
// test get p1 p2;
Point p1(1, 1);
Point p2(1, 0);
Point p3(5, 0);
Assert::IsTrue(l1.getP1() == p1);
Assert::IsTrue(l2.getP2() == p2);
Assert::IsTrue(l3.getP2() == p3);
}
};
对于求交点的重要复杂部分,我们的测试也做的更细致
比如直线相交的测试,我们对于几种直线间的情况比如相交、平行、重叠,以及三种直线的情况(线段、射线、直线)都做了非常细致的测试
// test parallel
TEST_METHOD(LinePrl)
{
Calculator* calc = new Calculator();
// 三种线段
char line = 'L';
char radio = 'R';
char segment = 'S';
Line lTD(line, 1, 3, 2, 3);
Line rTD(radio, 2, 5, 4, 5);
Line sTD(segment, 51, 6, 24, 6);
Calculator* cal = new Calculator();
Assert::IsTrue(cal->isParallerl(lTD, rTD));
Assert::IsTrue(cal->isParallerl(lTD, sTD));
Assert::IsTrue(cal->isParallerl(rTD, sTD)); Line l1(line, 3, 3, 5, 5);
Line r1(radio, 6, 5, -100, -101);
Line s1(segment, 0, 1, 100, 101);
Assert::IsTrue(cal->isParallerl(l1, r1));
Assert::IsTrue(cal->isParallerl(l1, s1));
Assert::IsTrue(cal->isParallerl(r1, s1));
Assert::IsFalse(cal->isParallerl(l1, sTD));
Assert::IsFalse(cal->isParallerl(r1, sTD));
Assert::IsFalse(cal->isParallerl(s1, rTD));
}</code></pre></li>
1.1. 命令行输入——参数异常
intersect.exe -n
intersect.exe -i input.txt -o output.txt -h
1.2. 命令行输入——文件名异常
intersect.exe -i test.txt -o output.txt
intersect.exe -i input.txt -o out.txt
2.1. 输入文件内容——输入曲线不符合格式
## 1. 直线输入错误
R 0 43 9 -3 98
# 2. 输入几何对象参数含有前导0
S 09 12 45 89
# 3. 多个字母
S S 3 2 1
# 4. 只有数字
3 1 5 2 76
# 5. 字母数字乱序
5 L 1 4 6
# 6. -后不接数字
L - - - -
# 7. 错误数字
L 98-736 92 0 82
2.2. 输入线段数目异常
# 1. 输入线段 < 1
0
-94
# 2. 输入线段与实际线段数不同
1
L 0 10 8 83
R 91 46 2 0
4
L 56 28 82 4
R 19 41 34 56
C 56 168 5
2.3.曲线输入文件无法打开
3.1. 直线不符合标准
## 1. 输入两点重合
L 0 1 0 1
## 2. 输入数字超范围
R 100000 0 0 0
L -100000 4897 278 1
S -100005 3784 942 61
3.2.有无穷多交点
#1. 正确情况
3
S 1 1 3 3
S 5 5 100 100
R 0 0 -55 -55
# 2. 异常
2
S 0 1 7 8
R -4 -3 -3 -2
2
S 0 1 7 8
L 9 10 100 101
2
R -4 -5 0 -1
L -99 -100 -50 -51
2
S 1 0 3 0
S 2 0 4 0
2
S 1 0 3 0
S 2 0 3 0
2
S 1 0 3 0
S 1 0 5 0
2
S 1 0 3 0
S 0 0 5 0
4.1. 圆不符合标准
## 1. 输入圆半径小于1
C 0 0 0
C 84 72 -23
## 2. 输入数字超范围
C -100000 4897 278
对于以上的样例,分别写了测试样例,得到测试结果如下:
这次的GUI我们是用Qt实现的。Qt的良好封装机制使得Qt的模块化程度非常高,可重用性较好,对于用户开发来说比较方便。 Qt提供了一种称为signals/slots的安全类型来替代 callback,使得各个元件之间的协同工作变得十分简单。
我们先通过集成在 Qt Creator 中的 Qt Designer 对窗体进行可视化设计。
最终界面如下:
为了方便用户操作、减少用户记忆负担、减少用户错误信息,我们的设计做了如下改良:
界面使用注意点:
在点击求解交点后,方有交点的数据,才能绘制出交点。
在点击绘制几何图形和交点后,才会更新当前几何图形的绘制。
Qt使用了信号和槽来代替回调函数,实现对象间的通信。当一个特定的事件发生时,信号会被发送出去。Qt的窗体部件(widget)拥有众多预先定义好的信号。槽,则是对一个特定的信号进行的反馈。我们这次的实现主要是创建窗体部件(widget)的子类并添加自定义槽,以便对感兴趣的信号进行处理。
我们实现的类中,有两个重要的属性vector figures和vector points,分别存放当前几何对象和当前交点。
a.文件导入
//点击"..."按钮,浏览文件夹
void IntersectGUI::on_findFileButton_clicked()
{
filePath =
QDir::toNativeSeparators(QFileDialog::getOpenFileName(this, tr("Save path"), QDir::currentPath())); //文件路径
if (!filePath.isEmpty())
{
if (ui.fileBox->findText(filePath) == -1)
ui.fileBox->addItem(filePath);//在comboBox中显示文件路径
}
}
//点击"输入文件"按钮,导入文件数据
void IntersectGUI::on_infileButton_clicked()
{
QFile* file = new QFile; //申请一个文件指针
file->setFileName(filePath); //设置文件路径
bool ok = file->open(QIODevice::ReadOnly);
if (ok)
{
……//读入文件并将文件中的数据处理后存入figures中
}
file->close();
}
}
b.求解交点
点击“求解交点”按钮,将当前几何体的数据转换成相应的接口处的数据,调用dll中的函数,计算交点并返回。具体接口设计,下一部分详细介绍。
int IntersectGUI::on_calcPButton_clicked()
{
points.clear();
std::string input;
size_t n = figures.size();
……//将figures中的几何体数据转换成相应的接口中的数据input
int cnt = 0;
//cnt = guiProcess(&points,figures);
try {
cnt = guiProcess(&points, input);
}
catch (std::exception e) {
QString dlgTitle = QString::fromLocal8Bit("计算出现错误");
QMessageBox::critical(this, dlgTitle, e.what());
return 0;
} {
}
ui.lineEdit->setText(QString::number(cnt));//反馈交点数
return cnt;
}
c.图形绘制
这一部分,我们重写了paintEvent()方法。点击“绘制几何图形和交点”的按钮时,调用update()函数,重新绘制。
void IntersectGUI::paintEvent(QPaintEvent*)
{
init_canvas(); //初始化画布 (底色和坐标轴)
if (figures.size() != 0) {
for (vector<string>::iterator iter = figures.begin(); iter != figures.end(); ++iter) {
draw_figures_from_str(*iter);//绘制几何图形
}
draw_points();//绘制交点
}
}
void IntersectGUI::on_drawFigureButton_clicked()
{
update();
}
//将不同的string数据绘制成不同的几何图形
void IntersectGUI::draw_figures_from_str(string str)
{
QStringList list = (QString::fromStdString(str)).split(" ");
QString type = list.at(0);
……
if (type == QString::fromLocal8Bit("L")) {
draw_line(x1, y1, x2, y2);
}
else if (type == QString::fromLocal8Bit("S")) {
draw_seg(x1, y1, x2, y2);
}
else if (type == QString::fromLocal8Bit("R")) {
draw_ray(x1, y1, x2, y2);
}
else {
draw_circle(x1, y1, r);
}
}
由于Qt中的drawLine()方法,只能绘制两点间的线段。所以在实现绘制直线和射线的时候,我们计算了当前线与画布边界的交点。代码简单,但是很长,在这里就不展示了。
计算模块与界面模块的对接,用到了此接口:
int guiProcess(std::vector<std::pair<double, double>> * points, std::string msg);
msg存放的是当前几何图形的信息,数据格式与文件中读取的格式相同。如:
4
C 3 3 3
S 2 4 3 2
L -1 4 5 2
R 2 5 -1 2
points存放求解的交点。
#pragma comment(lib,"calcInterface.lib")
_declspec(dllexport) extern "C" int guiProcess(std::vector<std::pair<double, double>> * points, std::string msg);
GUI相关的代码只在求解交点处调用了dll的guiProcess()方法。
int IntersectGUI::on_calcPButton_clicked()
{
points.clear();
std::string input;
size_t n = figures.size();
//转换数据
input += std::to_string(n) + "\n";
for (size_t i = 0; i < n; i++) {
input += figures.at(i) + "\n";
}
int cnt = 0;
try {
cnt = guiProcess(&points, input);
}
catch (std::exception e) {
...
} {
}
ui.lineEdit->setText(QString::number(cnt));//界面反馈交点总数
return cnt;
}
IntersectGUI类中的vector
guiProcess()代码如下:
int guiProcess(std::vector<std::pair<double, double>>* points,
std::string msg) {
try {
vector<Line> lVec;
vector<Circle> cVec;
//将msg中的几何信息解析并存入lVec和cVec中
istringstream input(msg);
fileExcHandler(input, lVec, cVec);
//计算交点
set<Point> pointSet = getAllIntersect(lVec, cVec);
//将交点信息写入points中
for (auto iter = pointSet.begin(); iter != pointSet.end(); iter++) {
Point p = (Point)* iter;
points->push_back(make_pair(p.getX(), p.getY()));
}
//返回交点总数
return (int)pointSet.size();
} catch (fileException& fileError) {
cout << fileError.what() << endl;
}
catch (lineException& lineError) {
cout << lineError.what() << endl;
}
catch (CircleException& circleError) {
cout << circleError.what() << endl;
}
return -1;
}
未点击“求解交点”按钮时,绘制的几何图形。
点击“求解交点”按钮后,再绘制
绘制交点并返回交点数。
时间表
时间
事项
3.12晚
讨论结对编程总体规划
3.12-3.14
独立进行需求分析、学习和设计的工作,辅以资源交流
3.14下午
讨论代码设计,确定最终设计和实际结对编程的各种方法和规范
3.14晚-3.16上午
进行合作编程,完成step1的功能及其测试
3.18-3.21
改进接口设计,并完成GUI部分和异常处理部分
3.23-3.24
完成博客部分
工具使用
结对编程本身
优点
缺点
本人在结对编程中的表现
优点
缺点
队友在结对编程中的表现
优点
缺点
由于疫情原因,我们并不能在编程时实时保持交流,加上我电脑有一天晚上坏了。所以对于接口的设计部分她与合作小组确定了方案,却未能与我及时沟通。这也不是缺点,只是结对过程中的一点小矛盾。里面也有我的错,我太执着己见了。不过好在我们愉快得解决了这个问题,最终达成共识。
pch.h
文件中导入头文件,并在每一个*.cpp
文件中include "pch.h
文件,之后生成dll即可A组dll导入B组cmd程序
B组dll导入A组GUI程序
A组dll导入B组GUI程序
由于我们两个小组在项目初期就确定了合作,所以接口设计是在双方商量好的情况下早早决定了的。对接过程中,碰到的唯一问题是编译版本问题。B组的dll是Release X64编译的,而我们是Debug X64编译的,导致开始对接时总是导入不成功。后来我们统一了编译版本(Release X64),问题也就解决了。
由于我们之前没有将dll同名,现已经改成统一名字。只需互换dll无需再编译即可运行,更改后的GUI版本和dll已经发布到github上了。
手机扫一扫
移动阅读更方便
你可能感兴趣的文章