软件工程结对项目作业

软件工程结对项目作业

1.在文章开头给出教学班级和可克隆的 Github 项目地址。

项目 内容
课程链接 2020春季计算机学院软件工程(罗杰 任健)
作业要求 结对项目作业
教学班级 006
项目地址 https://github.com/17373432/Pairng-Project

2.在开始实现程序之前,在下述 PSP 表格记录下你估计将在程序的各个模块的开发上耗费的时间。

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划
· Estimate · 估计这个任务需要多少时间 10 10
Development 开发
· Analysis · 需求分析 (包括学习新技术) 120 120
· Design Spec · 生成设计文档 30 30
· Design Review · 设计复审 (和同事审核设计文档) 10 10
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 10 10
· Design · 具体设计 120 120
· Coding · 具体编码 300 480
· Code Review · 代码复审 240 180
· Test · 测试(自我测试,修改代码,提交修改) 420 600
Reporting 报告
· Test Report · 测试报告 30 30
· Size Measurement · 计算工作量 10 10
· Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 30 30
合计 1330 1630

​ 由于疫情原因,这次结对作业并没有像书上说的那样面对面编程,因为交流实在太不方便了,所以更偏向于两人分工,各做各的。再加上事先没有商量好编译器配置和平台,导致交接时受到了一定的影响,我主要负责封装部分和错误处理,但是在把编译器的平台从x86改成x64之后不知道改了什么设置,就再也无法生成.dll文件了,所以每次有改动都得把原码发给队友,让他帮忙生成.dll文件再发过来,十分不便。

3.看教科书和其它资料中关于 Information Hiding,Interface Design,Loose Coupling 的章节,说明你们在结对编程中是如何利用这些方法对接口进行设计的。

  • 信息隐藏:

    类中的属性全为私有属性,对于其他类来说属性不可见。如果其他类要访问类中属性,需通过get()方法来实现。只暴露必要接口,核心部分中的类以及各种属性对外不可见。

  • 接口设计:

    核心部分只留了一个getPoints()的接口,通过传入一个文本信息流,返回这些对象的交点集合或者是异常信息。

  • 松耦合:

    类与类之间独立,只能通过调用方法传递信息来产生联系,所以修改一个类对其他的类影响不大。核心部分与外部只通过接口产生联系,所以修改一方对另一方影响不大。

4.模块接口的设计与实现过程。设计包括代码如何组织,比如会有几个类,几个函数,他们之间关系如何,关键函数是否需要画出流程图?说明你的算法的关键(不必列出源代码),以及独到之处。

  • 主类:将各个类联系起来,完成基本功能以及错误处理。

    • 属性:

      • 线的集合,用于存储所有的直线、线段、射线的信息,用vector储存。
      • 圆的集合,用于储存所有的圆的信息,用vector储存。
      • 点的集合,用于储存所有交点的信息,用unordered_set储存。
      • 其他用于辅助判断数据是否合法的属性。
    • 方法:求解交点,判断两条线是否重合等方法。

  • Line类:采用一般式\(Ax+By+C=0\),避免了斜率为正无穷的讨论。

    • 属性:基本的A,B,C的值,线的种类,线的两个端点坐标,以及其他简化计算的属性。
    • 方法:求与直线的交点,判断点是否在线上,以及构造函数,返回属性的值,重载小于运算符等方法。
  • Point类:

    • 属性:横坐标x,纵坐标y,交于此点的直线集合。
    • 方法:构造函数,返回属性的值,添加经过这个点的直线,重载小于、等于运算符等方法。
  • Circle类:

    • 属性:圆心(使用Point),半径,以及其他简化计算的属性。
    • 方法:求与直线的交点,求与圆的交点,以及构造函数,返回属性的值,重载小于运算符等方法。

    其他方法:计算Point类的hash值,计算pair的hash值,保留四位有效数字,浮点数比较等方法。

关键函数:

  • 判断两条线是否重合:

    因为对于线来说只有截距相同才可能会重合,所以事先储存线的截距到线的映射,通过截距将线分组,这样只用同一组的线是否重合,具体判断如下:

    result = false;
    if (两条线都为线段) {
    	if (某线段某端点在另一条线段两端点之间) {
            if (两线段只有一个点p重合) {
                pointSet.insert(p);
            }
            else {
            	result = true;
            }
        }
    }
    else if (两条线都为射线) {
    	if (某射线端点在另一条射线上) {
            if (两射线只有一个点p重合) {
                pointSet.insert(p);
            }
            else {
            	result = true;
            }
        }
    }
    else if (两条线分别为一条射线与一条线段) {
    	if (线段某端点在射线上) {
            if (两条线只有一个点p重合) {
                pointSet.insert(p);
            }
            else {
            	result = true;
            }
        }
    }
    return result;
    

    伪代码看起来很简单,但实现起来十分繁琐,这个函数写了140多行,通过画图将各种情况画出来,然后再写的代码,这样写条理十分清楚,也并没有写出bug。

    更重要一点,在这里将所有截距相同的线的交点全部算出来了,所以之后算线与线的交点时可以不必考虑平行的线的交点,可直接复用上一次代码,也节省了计算。

  • 判断点是否在线上:

    因为我设计的射线和线段只是在直线的基础上加了一个属性来判断线的种类,所以算点是否在线上是通过先假定该线为直线,计算出直线的交点坐标(x, y),之后需比较坐标是否在范围内即可,具体实现过程如下:

    bool isOnLine(double x, double y) {
    	bool result = true;
    	if (是线段) {
    		if (线段的两端点横坐标不相等) {
    			result = (x是否在线段两端点横坐标之间);
    		}
    		else {
    			result = (y是否在线段两纵坐标之间);
    		}
    	}
    	else if (是射线) {
    		if (构造射线的两端点横坐标不相等) {
    			result = (x是否在射线延伸的一边);
    		}
    		else {
    			result = (y是否在射线延伸的一边);
    		}
    	}
    	return result;
    }
    

    这里我一开始只传了横坐标x,认为只用x就能判断点是否在线上,这样就导致了当线段或者射线两端点横坐标相同时,我会把这些交点全当成在线上的点(应为两端点横坐标相同,所以交出来的点横坐标一定在两点之间),这样就导致我的点算多了,我花了很久才定位出这个bug,所以说不能为了省时间没考虑全面就想当然的写代码,否则debug浪费的时间就更多。

    另外,我在这个函数中加入了点是否在直线上的判断后,最终求得的点数量变少了,从理论上进行分析加不加这个判断对结果应该是没有影响的,通过调试才发现浮点误差通过加减乘除运算之后,误差会变得更大,导致超过了一开始设置的浮点精度\(10^{-12}\),所以才判断出计算出直线的交点不在直线上的情况,通过计算器验算,误差达到了\(0.03\),说明四则运算对浮点误差的影响很大,然而我只考虑了比较时的浮点误差,没有考虑计算时误差会变化的情况,这个地方虽然可以不用加点是否在直线上的判断从而忽略这个问题,但这个问题是普遍存在的,也不好避免,我想到最后也只有用不同的精度:计算前取高精度,比较时用低精度。但这样也不能完全解决问题,这次作业数据范围是\(10^5\),假设double类型精度为\(10^{-15}\),计算直线时直线的参数\(a,b\)的范围会变成\(10^5\),精度变成\(10^{-14}\)\(c\)的范围变成\(10^{10}\),精度变成\(10^{-9}\),然后在计算交点时,分子的范围会变成\(10^{15}\),精度变成\(10^{-3}\),这已经到了无法忽视的地步了,因为没考虑精度变化,所以很有可能两个点相同但是判为不同或者是不同的点判为相同的情况。这个问题我并没有想到一个好的方法解决。

  • 浮点数的hash函数:

    注意浮点数在hash前要保留有效数字,防止浮点误差对hash值产生影响。

  • 接口:

    通过读取数据信息,返回交点个数或者抛出异常。

    extern "C" void __declspec(dllexport) 
        getPoints(vector<pair<double, double>> & points, stringstream & in);
    

    一开始的想法是设计addObj(),deleteObj()这些接口的,但是考虑到删除可能和重新导入一遍数据的复杂度差不了多少,所以干脆就将删除操作定义为先删除数据再将数据重新导入,这样设计了之后,感觉单单为了一个增加操作而在GUI上再写一遍错误处理不值得,所以索性就把增加操作也换成导入数据信息,最后将几个接口合而为一,变成上述单一的接口,虽然在改动时开销更大,但是实现起来简单了许多,也减少了代码的冗余。

5.阅读有关 UML 的内容:https://en.wikipedia.org/wiki/Unified_Modeling_Language。画出 UML 图显示计算模块部分各个实体之间的关系(画一个图即可)。

6.计算模块接口部分的性能改进。记录在改进计算模块性能上所花费的时间,描述你改进的思路,并展示一张性能分析图(由VS 2015/2017的性能分析工具自动生成),并展示你程序中消耗最大的函数。

这是\(6000\)个数据,最终交点个数为\(10146161\)个点的性能分析图:

因为只有一个接口,所以这个接口开销最大:

7.看 Design by Contract,Code Contract 的内容:

描述这些做法的优缺点,说明你是如何把它们融入结对作业中的。

DbC的主要思想是如果一个方法在编程中提供一种特定的功能,它可以满足:

  • 任何调用它的模块在输入时都会保证一定的条件,即该方法的前置条件。
  • 保证退出时具有一定的属性,即方法的后置条件。
  • 维护一个确定的属性,该属性假定是在进入时假定的并且在退出时保证的,即不变式。

优点:

  1. 提供必要的说明,调用者无需知道这个方法实现过程,只需知道这个方法的具体功能。
  2. 明确输入和输出信息,便于调试。
  3. 各方法之间独立,修改部分方法对其他部分影响不大。
  4. 写代码的人可直接根据契约的要求来进行实现,无需考虑其他。

缺点:

  1. 契约有时候甚至比代码本身更加复杂。

这种契约例如JML之前在面向对象课程中已经体验过了,虽然实现起来十分复杂,但是一旦实现后后续工作会十分轻松,因为本次就一个接口,只需保证输入一个字符串流,就能返回交点集合或者抛出异常,在GUI或命令行程序调用接口时遵守这个契约,就能保证这部分运行的正确性。

8.计算模块部分单元测试展示。展示出项目部分单元测试代码,并说明测试的函数,构造测试数据的思路。并将单元测试得到的测试覆盖率截图,发表在博客中。要求总体覆盖率到 90% 以上,否则单元测试部分视作无效。

部分单元测试代码以及测试的函数如下所示。

	TEST_METHOD(TestMethod14)
	    {
			//测试平行于x轴直线,测试line类
			Proc p;
			string s = "2 L 1 0 2 0\n L -1 1 -1 2";
			std::stringstream ss(s);
			p.process(ss);
			vector <pair<double, double>> result;
			p.getPointSet(result);

			Assert::AreEqual(1, (int)result.size());
		}
	TEST_METHOD(TestMethod15)
		{
			//测试直线、线段、射线之间冲突,测试line类
			Proc p;
			string s = "7 L 1 3 4 2\n L -1 4 5 2\n S 2 4 3 2\n R 2 5 -1 2\n C 3 3 3\n C 2 							2 1\n C 3 2 2";
			std::stringstream ss(s);
			p.process(ss);
			vector <pair<double, double>> result;
			p.getPointSet(result);

			Assert::AreEqual(20, (int)result.size());
		}
	TEST_METHOD(TestMethod16)
		{
			//大型暴力测试,主要测试proc类
			Proc p;
			string s = "34\nL 1 3 4 2\nL -1 4 5 2\nS 2 4 3 2\nR 2 5 -1 2\nC 3 3 3\nC 2 2 						1\nC 3 2 2\nL 99999 99999 -99999 -99999\nL -99998 99998 99998 							-99998\nR 0 99 -1 100\nS 0 99 1 98\nS 2 97 1 98\nS 2 97 3 96\nS 4 						95 3 96\nS 4 95 5 94\nS 6 93 5 94\nR 99 0 100 -1\nR 99 0 100 1\nR 0 						 99 -1 -100\nS 0 -99 1 -98\nS 1 -98 2 -97\nS 99 0 98 -1\nS 3 -96 						4 -95\nS 2 -97 3 -96\nS 99 0 98 1\nS 11 88 10 89\nS 12 87 11 88\nS 						  10000 10000 99999 10000\nS 10000 9999 10000 10000\nR 8888 8888 							8888 8889\nS 1245 1245 1244 1247\nS 1244 1244 1243 1246\nS 2444 						2444 2443 2447\nS 2442 2442 2443 2445\n\n\n\n\n\n\n\n";
			std::stringstream ss(s);
			p.process(ss);
			vector <pair<double, double>> result;
			p.getPointSet(result);

			Assert::AreEqual(54, (int)result.size());
		}

​ 我们构造单元测试的思路主要是首先针对小数据,特定情况,对特定情况的测试大概覆盖完全后进行更大规 模的测试。

​ 单元测试通过截图:

​ 单元测试覆盖率截图:

9.计算模块部分异常处理说明。在博客中详细介绍每种异常的设计目标。每种异常都要选择一个单元测试样例发布在博客中,并指明错误对应的场景。

公共父类

class InputException :public exception {
private:
	string msg;
public:
	InputException(string e) {
		msg = e;
	}
	const char* what() const throw() {
		cout << msg << endl;
		return msg.data();
	}
	string getMsg() const {
		return msg;
	}
};

N不在范围内:

\(1<=N<=500000\)

class NumberException :public InputException {
public:
	NumberException() :
		InputException("N is out of range!\n") {};
	const char* what() const throw() {
		cout << getMsg() << endl;
		return getMsg().data();
	}
};

样例1:

0

输出1:

N is out of range!

样例2:

500001

输出2:

N is out of range!

参数不在范围内:

范围为 \((-100000, 100000)\)

class OutOfRangeException :public InputException {
public:
	OutOfRangeException(int i, string str) :
		InputException("Line " + to_string(i) + ": \"" + str + "\"\nThe parameter(s) is(are) out of range!\n") {};
	const char* what() const throw() {
		cout << getMsg() << endl;
		return getMsg().data();
	}
};

样例3:

4
C 3 3 3
S 222222 4 3 2
L -1 4 5 2
R 2 5 -1 2

输出3:

Line 2: "S 222222 4 3 2"
The parameter(s) is(are) out of range!

构造线的两点重合:

class CoincideException :public InputException {
public:
	CoincideException(int i, string str) :
		InputException("Line " + to_string(i) + ": \"" + str + "\"\nThe two points coincide!\n") {};
	const char* what() const throw() {
		cout << getMsg() << endl;
		return getMsg().data();
	}
};

样例4:线段两点重合

4
C 3 3 3
S 2 4 2 4
L -1 4 5 2
C 2 5 1

输出4:

Line 2: "S 2 4 2 4"
The two points coincide!

图形重叠:

class CoverException :public InputException {
public:
	CoverException(int i, string str) :
		InputException("Line " + to_string(i) + ": \"" + str + "\"\nOverlap with added drawings!\n") {};
	const char* what() const throw() {
		cout << getMsg() << endl;
		return getMsg().data();
	}
};

样例5:圆与圆重合

5
C 3 3 3
S 2 4 3 2
L -1 4 5 2
R 2 5 -1 2
C 3 3 3

输出5:

Line 5: "C 3 3 3"
Overlap with added drawings!

样例6:以线段与线段重合为例

5
C 3 3 3
S 2 4 4 0
L -1 4 5 2
R 2 5 -1 2
S 3 2 5 -2

输出6:

Line 5: "S 3 2 5 -2"
Overlap with added drawings!

对象数目不等于N:

class NumOfObjException :public InputException {
public:
	NumOfObjException() :
		InputException("The number of geometric objects is not equal to N!\n") {};
	const char* what() const throw() {
		cout << getMsg() << endl;
		return getMsg().data();
	}
};

样例7:对象数大于N

3
C 3 3 3
S 2 4 3 2
L -1 4 5 2
R 2 5 -1 2

输出7:

The number of geometric objects is not equal to N!

样例8:对象数小于N

5
C 3 3 3
S 2 4 3 2
L -1 4 5 2
R 2 5 -1 2

输出8:

The number of geometric objects is not equal to N!

输入格式错误:

class FormatException :public InputException {
public:
	FormatException(int i, string str) :
		InputException("Line " + to_string(i) + ": \"" + str + "\"\nThe format of input is illgal!\n") {};
	const char* what() const throw() {
		cout << getMsg() << endl;
		return getMsg().data();
	}
};

样例9:参数有前导零

4
C 3 3 3
S 2 4 3 2
L -01 4 5 2
R 2 5 -1 2

输出9:

Line 3: "L -01 4 5 2"
The format of input is illgal!

样例10:标识字母小写

4
C 3 3 3
s 2 4 3 2
L -1 4 5 2
R 2 5 -1 2

输出10:

Line 2: "s 2 4 3 2"
The format of input is illgal!

圆的半径不大于零:

class LessThanZeroException :public InputException {
public:
	LessThanZeroException(int i, string str) :
		InputException("Line " + to_string(i) + ": \"" + str + "\"\nRadius of circle must be greater than zero!\n") {};
	const char* what() const throw() {
		cout << getMsg() << endl;
		return getMsg().data();
	}
};

样例11:半径小于0

4
C 3 3 3
S 2 4 3 2
L -1 4 5 2
C 2 5 -1

输出11:

Line 4: "C 2 5 -1"
Radius of circle must be greater than zero!

样例12:半径等于0

4
C 3 3 3
S 2 4 3 2
L -1 4 5 2
C 2 5 0

输出12:

Line 4: "C 2 5 0"
Radius of circle must be greater than zero!

空文件:

class EmptyFileException :public InputException {
public:
	EmptyFileException() :
		InputException("The input file is empty!\n") {};
	const char* what() const throw() {
		cout << getMsg() << endl;
		return getMsg().data();
	}
};

样例13:


输出13:

The input file is empty!

文件第一行不是N:

class NoNException :public InputException {
public:
	NoNException() :
		InputException("The first line of input file is not a number!\n") {};
	const char* what() const throw() {
		cout << getMsg() << endl;
		return getMsg().data();
	}
};

样例14:

C 3 3 3
S 2 4 2 5
L -1 4 5 2
C 2 5 1

输出14:

The first line of input file is not a number!

命令行参数有误:

样例15:

./intersect.exe

输出15:

Right Format: intersect.exe -i <path to input file> -o <path to output file>

文件无法打开:

样例16:

./intersect.exe -i dir -o output.txt

输出16:

Input File Cannot Open!

最后错误处理的单元测试:

10.界面模块的详细设计过程。在博客中详细介绍界面模块是如何设计的,并写一些必要的代码说明解释实现过程。

界面模块利用VSQt插件开发。GUI共含4个窗口。分别如下所示:

第一个窗口为主窗口,在其中我们进行交互、输出图像信息。剩下三个窗口全部用来进行手动导入数据。

主窗口的图像绘制是我们自己手绘的,因为没有找到很好的绘制图像的类。我们将paintEvent函数重定向到widget组件中,使widget组件收到重绘的指令后执行widgetPaint函数对整个画面重新绘制。

绘制过程中比较重要的是坐标与像素之间的转换。

double QtPairProject::transferX(double x) {
	int pointx = 20;
	int width = length - 20 - pointx;
	double kx = (double)width / (x_max - x_min );
	return pointx + kx * (x - x_min);
}

double QtPairProject::transferY(double y) {
	int pointy = wide - 20;
	int height = wide - 40;
	double ky = (double)height / (y_max - y_min );
	return pointy - (y - y_min) * ky + offset;
}

交互控件主要控制以下功能,修改坐标轴的范围、从文件导入数据、手动导入数据、删除lineWidget中被选中的数据、清空lineWidget,坐标图和输出栏、以及绘制交点功能。右下角的输出框用于程序运行情况的输出,例如异常报告、绘制交点情况反馈。

其余三个窗口用于手动导入数据。在这四个窗口之间我们借助signals从子窗口向父窗口发送信息。

例如在主窗口QtPairProject.h中使用

signals:
	void sendsignal(QString);

QtPairProject.cpp中使用

void QtPairProject::OnOpenButtonClick() {
	win->show();
}

void QtPairProject::getData(QString data) {
	this->show();
}

接收子窗口向父窗口发送的信息。

在子窗口NewQtGuiClass.h中定义

public slots:
	void sendData();

NewQtGuiClass.cpp使用

NewQtGuiClass::NewQtGuiClass(QWidget *parent)
	: QWidget(parent)
{
	ui.setupUi(this);
	connect(ui.pushButton, SIGNAL(clicked()), this, SLOT(pushButton()));
}

void NewQtGuiClass::sendData() {
	emit sendsignal(buf);
	buf = "";
	this->close();
}

向父窗口传递信息。

在子窗口的数据输入以及修改坐标范围的输入中,我们采用了正则表达式检验以及整型数验证控件来限制输入的范围。

QRegExp rx("^-?\\d{1,6}$");
QRegExpValidator* v = new QRegExpValidator(rx, this);
QIntValidator *intValidator = new QIntValidator;
intValidator->setRange(-100000, 100000);
ui.lineEdit->setValidator(v);
ui.lineEdit_2->setValidator(v);
intValidator->setRange(0, 100000);
ui.lineEdit_3->setValidator(intValidator);

12.界面模块与计算模块的对接。详细地描述 UI 模块的设计与两个模块的对接,并在博客中截图实现的功能。

我们通过输入流来实现界面模块与计算模块的对接。

extern "C" void __declspec(dllexport) getPoints(vector<pair<double, double>> & points, stringstream & in);

即GUI组件将输入打包成控制台输入的格式通过流传输给计算模块,由计算模块传回计算得到的交点数组。

实现的功能:

  • 修改坐标范围

  • 从文件导入

  • 手动导入

  • 删除所选数据

  • 绘制交点

  • 清空

12.描述结对的过程,提供两人在讨论的结对图像资料(比如 Live Share 的截图)。关于如何远程进行结对参见作业最后的注意事项。

QQ屏幕分享:

微信交流:

13.看教科书和其它参考书,网站中关于结对编程的章节,例如:http://www.cnblogs.com/xinz/archive/2011/08/07/2130332.html ,说明结对编程的优点和缺点。同时描述结对的每一个人的优点和缺点在哪里(要列出至少三个优点和一个缺点)。

结对编程 结对伙伴
优点 1.使代码得到充分复审,及时找出bug;2.有人监督写代码不容易分心;3.两个人合作比一个人单干效率高 1.能想到巧妙的方法简化问题;2.有耐心;3.及时回复队友消息 1.善于发现bug;2.学习新知识快;3.代码基本功好
缺点 两个人同时有空的时间不多,沟通不充分会阻碍进度 考虑问题不周到,写了很多bug 有时候回复消息不及时

消除所有警告

附加题

  • 支持圆(已实现)
posted @ 2020-03-24 17:22  cc17373432  阅读(204)  评论(2编辑  收藏  举报