BUAA软件工程结对项目
小组成员:16005001,17373192
1、教学班级和项目地址
项目 | 内容 |
---|---|
这个作业属于哪个课程 | 博客园班级连接 |
这个作业的要求在哪里 | 结对项目作业 |
我在这个课程的目标是 | 提高软件开发能力、团队协作能力 |
这个作业在哪个具体方面帮助我实现目标 | 感受结对开发 |
项目代码 | https://github.com/monokuma-zhuo/Intersect |
教学班级 | 006 |
2、PSP表格
SP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
· Estimate | · 估计这个任务需要多少时间 | 30 | 30 |
Development | 开发 | ||
· Analysis | · 需求分析 (包括学习新技术) | 100 | 180 |
· Design Spec | · 生成设计文档 | 20 | 30 |
· Design Review | · 设计复审 (和同事审核设计文档) | 20 | 20 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 5 | 5 |
· Design | · 具体设计 | 40 | 60 |
· Coding | · 具体编码 | 180 | 200 |
· Code Review | · 代码复审 | 30 | 60 |
· Test | · 测试(自我测试,修改代码,提交修改) | 60 | 100 |
Reporting | 报告 | ||
· Test Report | · 测试报告 | 10 | 10 |
· Size Measurement | · 计算工作量 | 10 | 10 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 30 |
合计 | 535 | 735 |
3、看教科书和其它资料中关于 Information Hiding,Interface Design,Loose Coupling 的章节,说明你们在结对编程中是如何利用这些方法对接口进行设计的
接口设计时主要是减少接口之间的耦合度,确保每个接口都只完成各自的一项功能,避免出现冗杂,从而实现松耦合。核心模块和ui之间的接口设计就是这样实现的,从ui程序的角度看我只需要知道接口名字,拿到接口返回的数据即可,而不需要关注接口内部是如何设计的。在类中没有写私有属性,但是封装成dll文件后供ui使用本身也是一种信息隐藏,因为ui程序并不能知道接口的源码设计,只通过接口名字调用即可。
4、计算模块接口的设计与实现过程。设计包括代码如何组织,比如会有几个类,几个函数,他们之间关系如何,关键函数是否需要画出流程图?说明你的算法的关键(不必列出源代码),以及独到之处
本次作业共有直线类,射线类,线段类,圆类,函数除了每个类中求交点的方法外,还写了几个供ui调用的接口函数。
几何对象的表示
上次作业中,由于直线与圆无明显的联系,故我采用结构体来表示,但本次作业新增了射线与线段,射线与线段明显与直线有联系,可以看作是特殊的有界的直线,故采用继承的方式。分别建立直线类与圆类,射线类与线段类继承直线类。对于交点的存储,本次作业中采用pair<double,double>的形式。
对于几何对象,采用vector进行存储,交点采用set进行存储。
交点的求解
参考上次作业的求解方法,在本次作业中依旧使用类似的方法,在类中编写与其他几何对象计算交点的方法。
在计算交点之前,还需要判断交点是否存在。判断直线与直线是否平行的方法,依旧采取上次作业中提到的方法,比较$A1B2$与$A2B1$是否一致。直线与圆则采取圆心到直线的距离与半径的关系,圆与圆采取比较圆心距离与两圆半径之间的关系。线段与射线当作直线来考虑。考虑到double的精度问题,在判断相等时采取$fabs(a-b)<1e-10$的方式进行判断。
求解交点的方式依旧采取上次作业的方式,关于线段与射线,采取上次作业所述求解方法得到交点后,还需判断交点是否在线段或射线上,具体做法为对求解出的点进行范围判断,同时还需要考虑到斜率不存在的情形。由于线段与射线与其他图形求解得到的结果可能不存在,故在返回值中增加标记位用来描述此解是否存在。
值得注意的是,线段与射线在共线的情况下依旧可能有解,这是与直线有着很大的区别,故需要对线段、射线的判断平行函数进行重写,维护一个全局变量用于存储这个可能存在的解。
交点的去重
采用set进行去重,重写了存储交点的pair<double,double>的operator<的方法来保证去重的正确性,同时还考虑到了精度问题,在重写<的时候均使用$fabs(a-b)<1e-10$先判断是否等于关系成立。
5、阅读有关 UML 的内容。画出 UML 图显示计算模块部分各个实体之间的关系(画一个图即可)。
如图由VS生成的uml图
6、计算模块接口部分的性能改进。记录在改进计算模块性能上所花费的时间,描述你改进的思路,并展示一张性能分析图(由VS 2015/2017的性能分析工具自动生成),并展示你程序中消耗最大的函数。
采用10000个随机生成的几何图形得到以下结果。
程序中消耗最大的为set的去重工作。曾经考虑过使用unordered_set来代替set,但是在换成unordered_set编写hash函数时遇到了有效数字以及精度问题和冲突问题,最终还是决定使用set来保证正确性。
7、看 Design by Contract,Code Contract 的内容描述。这些做法的优缺点,说明你是如何把它们融入结对作业中的
DBC要求使用者和被调用者地位平等,双方必须彼此履行义务。
优点:1、减少因自然语言的歧义等带来的不必要麻烦。
2、保证了双方代码的质量。
3、提高了软件工程的效率和质量 。
缺点:1、对程序语言有一定要求,需要支持断言。
2、写起来比较繁琐,可能会带来不必要的麻烦。
8、计算模块部分单元测试展示。展示出项目部分单元测试代码,并说明测试的函数,构造测试数据的思路。并将单元测试得到的测试覆盖率截图,发表在博客中。要求总体覆盖率到 90% 以上,否则单元测试部分视作无效。
对于程序正确性、各个方法的正确性,斜率不存在等特殊情况以及各种异常进行了测试。
这是其中一个测试正确性的函数
TEST_METHOD(TestCorrectness)
{
set<pair<double, double>> Array_dot;
Line l1 = Line(-1, 4, 5, 2);
Cycle c1 = Cycle(3, 3, 3);
Ray r1 = Ray(2, 5, -1, 2);
Segment s1 = Segment(2, 4, 3, 2);
pair<pair<double, double>, int> dot1 = l1.intersect(r1);
if (dot1.second == 1) {
Array_dot.insert(dot1.first);
}
dot1 = l1.intersect(s1);
if (dot1.second == 1) {
Array_dot.insert(dot1.first);
}
pair<pair<double, double>, pair<double, double>> dot2 = c1.line_cycle_instere(l1);
Array_dot.insert(dot2.first);
Array_dot.insert(dot2.second);
pair<pair<pair<double, double>, pair<double, double>>, pair<int, int>> dot3 = c1.ray_cycle_instere(r1);
if (dot3.second.first == 1) {
Array_dot.insert(dot3.first.first);
}
if (dot3.second.second == 1) {
Array_dot.insert(dot3.first.second);
}
dot1 = r1.intersect(s1);
if (dot1.second == 1) {
if (r1.x1 < r1.x2) {
if (r1.x1 <= dot1.first.first) {
Array_dot.insert(dot1.first);
}
}
else if (r1.x1 > r1.x2) {
if (r1.x1 >= dot1.first.first) {
Array_dot.insert(dot1.first);
}
}
else {
if (r1.y1 > r1.y2) {
if (r1.y1 >= dot1.first.second) {
Array_dot.insert(dot1.first);
}
}
else if (r1.y1 < r1.y2) {
if (r1.y1 <= dot1.first.second) {
Array_dot.insert(dot1.first);
}
}
}
}
Assert::AreEqual(5, (int)Array_dot.size());
}
这是斜率不存在的测试
TEST_METHOD(TestAequals0)
{
Line l1 = Line(0, 0, 1, 1);
Ray r1 = Ray(1, 0, 2, 0);
pair<pair<double, double>, int> dot1 = l1.intersect(r1);
Assert::AreEqual(0, (int)dot1.first.first);
Assert::AreEqual(0, (int)dot1.first.second);
Assert::AreEqual(0, (int)dot1.second);
}
对各种异常的测试采取读入预先写好的存在错误的文件的方式
TEST_METHOD(TestOperationException) {
auto func = [] {test("input1.txt"); };
Assert::ExpectException<OperatorException>(func);
/*try {
test("input1.txt");
}
catch (OperatorException& e) {
Assert::AreEqual("Operation Exception", e.what());
}*/
}
TEST_METHOD(TestEndException) {
auto func = [] {test("input2.txt"); };
Assert::ExpectException<EndException>(func);
/*try {
test("input2.txt");
}
catch (EndException& e) {
Assert::AreEqual("End Exception", e.what());
}*/
}
TEST_METHOD(TestDefectException) {
auto func = [] {test("input3.txt"); };
Assert::ExpectException<DefectException>(func);
/*try {
test("input3.txt");
}
catch (DefectException& e) {
Assert::AreEqual("Defect Exception", e.what());
}*/
}
TEST_METHOD(TestNumberException) {
auto func = [] {test("input4.txt"); };
Assert::ExpectException<NumberException>(func);
/*try {
test("input4.txt");
}
catch (NumberException& e) {
Assert::AreEqual("Number Exception", e.what());
}*/
}
代码覆盖率
单元测试
9、计算模块部分异常处理说明。在博客中详细介绍每种异常的设计目标。每种异常都要选择一个单元测试样例发布在博客中,并指明错误对应的场景。
设计了如下异常
错误类型 | 输入(其中一种) | 描述 |
---|---|---|
操作符错误 | 2 L 0 0 1 1 Z 0 0 -1 -1 | 操作符出入错误 |
有多余字符 | 2 C 1 1 1 C 1 1 1 L 1 1 1 0 | 几何对象超出数目 |
缺少字符 | 2 L 1 1 1 1 | 缺少几何对象或数字 |
数字格式错误 | 1 L 10000 1000a 1 1 | 数字格式错误 |
数字越解 | 1 L 10000000 1 1 0 | 输入数字超过100000范围 |
直线重复 | 2 L 1 1 0 0 L 0 0 1 1 | 直线出现重合导致无数交点,射线与线段的无数交点情形也算作此异常 |
圆重复 | 2 C 1 1 1 C 1 1 1 | 圆重复导致无数交点 |
直线非法 | 1 L 1 1 1 1 | 直线的两个端点相同构不成直线 |
圆非法 | 1 C 1 1 0 | 圆的半径非法 |
异常均会在控制台输出并且同时输出行号方便定位
10、界面模块的详细设计过程。在博客中详细介绍界面模块是如何设计的,并写一些必要的代码说明解释实现过程。
界面模块我们使用Qt进行编写,由于Qt的窗格是以左上角为原点,向下为y轴正方向,所以需要重新绘制一个坐标系。我们圈了600*600区域为坐标系,同时绘制了x轴和y轴。之后设置了4个按钮,分别为添加图形、从文件中添加、删除、求交点。
添加图形时会弹出一个新窗格来输入图形类型和点坐标,确定后进行绘制,并调用core模块的接口函数返回当前交点集合并存储。从文件中添加也是同理,只不过输入的数据是从文件中读取。
信息发送与接收的部分代码
void MainWindow::on_pushButton_clicked()
{
dialog1=new Dialog;
connect(dialog1,SIGNAL(sendData(int,QString,QString,QString,QString,QString)),this,SLOT(receiveData(int,QString,QString,QString,QString,QString)));
dialog1->show();
}
void MainWindow::receiveData(int type,QString name,QString x1,QString y1,QString x2,QString y2)
{
if(type==0){
Paint_Line(x1.toDouble(),y1.toDouble(),x2.toDouble(),y2.toDouble());
}
else if(type==1){
Paint_Ray(x1.toDouble(),y1.toDouble(),x2.toDouble(),y2.toDouble());
}
else if(type==2){
Paint_Segment(x1.toDouble(),y1.toDouble(),x2.toDouble(),y2.toDouble());
}
else if(type==3){
Paint_Cycle(x1.toDouble(),y1.toDouble(),x2.toDouble());
}
}
绘制直线的代码
void MainWindow::Paint_Line(double x1,double y1,double x2,double y2)
{
intersect_point=input_line(std::make_pair(std::make_pair(x1,y1),std::make_pair(x2,y2)));
QPainter painter(&image);
QPen pen;
pen.setColor(Qt::blue);
painter.setPen(pen);
painter.setRenderHint(QPainter::Antialiasing, true);
if(x1==x2)
{
painter.drawLine(QPointF(pointx0+x1,0),QPointF(pointx0+x2,600));
}
else if(y1==y2)
{
painter.drawLine(QPointF(0,pointy0-y1),QPointF(600,pointy0-y2));
}
else{
double k=(y2-y1)/(x2-x1);
double b=y1-k*x1;
painter.drawLine(QPointF(0,-1*(-300*k+b)+pointy0),QPointF(600,-1*(300*k+b)+pointy0));
}
}
从文件读取的代码
void MainWindow::on_pushButton_3_clicked()
{
QString filename;
filename=QFileDialog::getOpenFileName(this,tr("文件"),"",tr("text(*.txt)"));
if(!filename.isNull())
{
QFile file(filename);
if(!file.open(QFile::ReadOnly|QFile::Text))
{
QMessageBox::warning(this,tr("error"),tr("read file error:&1").arg(file.errorString()));
return;
}
QTextStream in(&file);
while(!in.atEnd())
{
QString line=in.readLine();
QList<QString> list;
list=line.split(' ');
if(list[0]=="L")
{
Paint_Line(list[1].toDouble(),list[2].toDouble(),list[3].toDouble(),list[4].toDouble());
}
else if(list[0]=="R")
{
Paint_Ray(list[1].toDouble(),list[2].toDouble(),list[3].toDouble(),list[4].toDouble());
}
else if(list[0]=="S")
{
Paint_Segment(list[1].toDouble(),list[2].toDouble(),list[3].toDouble(),list[4].toDouble());
}
else if(list[0]=="C")
{
Paint_Cycle(list[1].toDouble(),list[2].toDouble(),list[3].toDouble());
}
}
}
}
删除时需要清空当前坐标系中的几何图形和交点坐,并调用core模块中的接口函数清除core中已有的几何图形。
void MainWindow::on_pushButton_2_clicked()
{
QList<QLabel*> array_label=this->findChildren<QLabel *>();
for(int i=0;i<array_label.size();i++)
{
array_label[i]->clear();
}
delete_all();
image.fill(Qt::white);
Paint();
update();
}
求交点时,直接根据存储的坐标信息在图上setText即可。
void MainWindow::on_pushButton_4_clicked()
{
std::set<std::pair<double, double>>::iterator it;
for(it=intersect_point.begin();it!=intersect_point.end();it++)
{
QString x=QString::number((*it).first,'f',1);
QString y=QString::number((*it).second,'f',1);
QLabel *text=new QLabel(this);
text->setText("("+x+","+y+")");
text->setGeometry(300+(*it).first,300-(*it).second,100,25);
text->show();
}
}
11、界面模块与计算模块的对接。详细地描述 UI 模块的设计与两个模块的对接,并在博客中截图实现的功能。
如下为core模块中的接口函数
IMPORT_DLL std::set<std::pair<double,double>> solve(std::vector<std::pair<char, std::pair<std::pair<int, int>, std::pair<int, int>>>> line, std::vector<std::pair<char, std::pair<std::pair<int, int>, int>>> circle);
IMPORT_DLL bool if_line_same(std::pair<std::pair<double, double>, std::pair<double, double>> a, std::pair<std::pair<double, double>, std::pair<double, double>> b);
IMPORT_DLL bool if_circle_same(std::pair<std::pair<double, double>, double> c1, std::pair<std::pair<double, double>, double> c2);
IMPORT_DLL std::set<std::pair<double, double>> input_line(std::pair<std::pair<double, double>, std::pair<double, double>> line1);
IMPORT_DLL std::set<std::pair<double, double>> input_ray(std::pair<std::pair<double, double>, std::pair<double, double>> ray1);
IMPORT_DLL std::set<std::pair<double, double>> input_segment(std::pair<std::pair<double, double>, std::pair<double, double>> segment1);
IMPORT_DLL std::set<std::pair<double, double>> input_circle(std::pair<std::pair<double, double>, double> circle1);
IMPORT_DLL long main1(int argc, char* argv[]);
IMPORT_DLL void delete_all();
在Qt的.pro工程中加入如下两行代码
LIBS+=D:/softwareproject/Gui3/untitled/core.lib
INCLUDEPATH +=D:/softwareproject/Gui3/untitled/pch
并将core.dll文件放在和intersect.exe同一目录下,即可在Qt工程中正常调用接口函数。
如图为实现的添加图形,显示坐标等功能
12、描述结对的过程,提供两人在讨论的结对图像资料(比如 Live Share 的截图)。
13、看教科书和其它参考书,网站中关于结对编程的章节,说明结对编程的优点和缺点。同时描述结对的每一个人的优点和缺点在哪里(要列出至少三个优点和一个缺点)。
结对编程:
优点:1、两人复查代码,减少bug。
2、可以互相学习对方的长处,比如一些编程小技巧,代码规范等
缺点:1、受时空影响,比如现在各自在家交流有时不方便
2、商讨可能会占据大量时间导致效率降低
我:
优点:1、能够比较快的理解问题,并想出一些解决方案
2、写代码效率还可以
3、能够比较快的学习新知识并应用
缺点:1、代码风格不好,队友读起来比较吃力。
队友
优点:1、做事认真负责
2、能够清楚的表达出自己的想法,交流比较容易
3、找bug能力强,能发现一些被忽视的bug
缺点:1、代码架构方面写的不是很好,会出现代码冗杂的情况
附上无警告的截图
附加题:松耦合
合作组同学学号:17373167,17373349
如下图为我们组的ui运行他们组的core模块的修改与结果:
由于我们两组预先并没有商量好统一的接口名字,导致并不能无任何修改的使用对方的core模块,所以在ui上我们需要修改文件名和接口名。并且由于返回的数据类型也不一样,所以需要新建一个合适的容器来存储他们的接口函数返回的交点坐标。